Correspondência não-gulosa com regex SED (emular perl's. *?)

11

Eu quero usar sed para substituir qualquer coisa em uma string entre a primeira AB e a primeira ocorrência de AC (inclusive) com XXX .

Para exemplo , eu tenho essa string (essa string é apenas para um teste):

ssABteAstACABnnACss

e gostaria de um resultado semelhante a este: ssXXXABnnACss .

Eu fiz isso com perl :

$ echo 'ssABteAstACABnnACss' | perl -pe 's/AB.*?AC/XXX/'
ssXXXABnnACss

mas quero implementá-lo com sed . O seguinte (usando o regex compatível com Perl) não funciona:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss
    
por بارپابابا 23.07.2016 / 00:30

5 respostas

8

As expressões regulares de sed correspondem à correspondência mais longa. Sed não tem equivalente de não ganancioso.

Obviamente, o que queremos fazer é combinar

  1. AB ,
    seguido por
  2. qualquer quantidade diferente de AC ,
    seguido por
  3. AC

Infelizmente, sed não pode fazer # 2 - pelo menos não para uma expressão regular de vários caracteres. Claro, para uma expressão regular de caractere único, como @ (ou mesmo [123] ), nós podemos fazer [^@]* ou [^123]* . E assim podemos contornar as limitações do sed alterando todas as ocorrências de AC para @ e pesquisando

  1. AB ,
    seguido por
  2. qualquer número diferente de @ ,
    seguido por
  3. @

assim:

sed 's/AC/@/g; s/AB[^@]*@/XXX/; s/@/AC/g'

A última parte muda instâncias não correspondidas de @ de volta para AC .

Mas, claro, essa é uma abordagem imprudente, porque a entrada já pode conter @ caracteres, então, combinando-os, poderíamos obter falsos positivos. Contudo, já que nenhuma variável shell terá um caractere NUL ( \x00 ), o NUL provavelmente é um bom caractere para ser usado na solução alternativa acima, em vez de @ :

$ echo 'ssABteAstACABnnACss' | sed 's/AC/\x00/g; s/AB[^\x00]*\x00/XXX/; s/\x00/AC/g'
ssXXXABnnACss

O uso de NUL requer o GNU sed. (Para ter certeza de que os recursos GNU estão habilitados, o usuário não deve ter definido a variável shell POSIXLY_CORRECT.)

Se você estiver usando sed com o flag -z do GNU para lidar com entradas separadas por NUL, como a saída de find ... -print0 , então NUL não estará no espaço padrão e NUL é uma boa escolha para a substituição aqui.

Embora o NUL não possa estar em uma variável bash, é possível incluí-lo em um comando printf . Se sua string de entrada puder conter qualquer caractere, incluindo o NUL, veja a resposta de Stéphane Chazelas , que adiciona um caracter inteligente método de escape.

    
por 23.07.2016 / 00:45
6

Algumas implementações sed têm suporte para isso. ssed tem um modo PCRE:

ssed -R 's/AB.*?AC/XXX/g'

AT & T ast sed tem conjunção e negação ao usar expressões aumentadas :

sed -A 's/AB(.*&(.*AC.*)!)AC/XXX/g'

Portável, você pode usar esta técnica: substitua a sequência final (aqui AC ) por um caractere único que não ocorre na cadeia inicial ou final (como : aqui) para que você possa fazer s/AB[^:]*:// , e caso esse caractere possa aparecer na entrada, use um mecanismo de escape que não colida com as sequências inicial e final.

Um exemplo:

sed 's/_/_u/g; # use _ as the escape character, escape it
     s/:/_c/g; # escape our replacement character
     s/AC/:/g; # replace the end string
     s/AB[^:]*:/XXX/g; # actual replacement
     s/:/AC/g; # restore the remaining end strings
     s/_c/:/g; # revert escaping
     s/_u/_/g'

Com o GNU sed , uma abordagem é usar a nova linha como o caractere de substituição. Como sed processa uma linha por vez, a nova linha nunca ocorre no espaço padrão, portanto, é possível fazer isso:

sed 's/AC/\n/g;s/AB[^\n]*\n/XXX/g;s/\n/AC/g'

Isso geralmente não funciona com outras implementações de sed , porque eles não suportam [^\n] . Com o GNU sed , você precisa ter certeza de que a compatibilidade POSIX não está ativada (como na variável de ambiente POSIXLY_CORRECT).

    
por 23.07.2016 / 17:39
3

Não, regexes sed não têm correspondência não-gananciosa.

Você pode combinar todo o texto até a primeira ocorrência de AC usando "qualquer coisa que não contenha AC " seguido por AC , que faz o mesmo que .*?AC de Perl. A questão é que “qualquer coisa que não contenha AC ” não pode ser expressa facilmente como uma expressão regular: há sempre uma expressão regular que reconhece a negação de uma expressão regular, mas a regex de negação é complicada rapidamente. E em sed portátil, isso não é possível, porque o regex de negação requer o agrupamento de uma alternância que está presente em expressões regulares estendidas (por exemplo, no awk), mas não em expressões regulares básicas portáteis. Algumas versões do sed, como o GNU sed, têm extensões para o BRE que permitem expressar todas as expressões regulares possíveis.

sed 's/AB\([^A]*\|A[^C]\)*A*AC/XXX/'

Por causa da dificuldade de negar um regex, isso não generaliza bem. O que você pode fazer é transformar a linha temporariamente. Em algumas implementações de sed, você pode usar novas linhas como um marcador, já que elas não podem aparecer em uma linha de entrada (e se você precisar de vários marcadores, use nova linha seguida de um caractere variável).

sed -e 's/AC/\
&/g' -e 's/AB[^\
]*\nAC/XXX/' -e 's/\n//g'

No entanto, tenha em atenção que a linha invertida-nova linha não funciona num conjunto de caracteres com algumas versões sed. Em particular, isso não funciona no GNU sed, que é a implementação do sed no Linux não embarcado; no GNU sed você pode usar \n :

sed -e 's/AC/\
&/g' -e 's/AB[^\n]*\nAC/XXX/' -e 's/\n//g'

Neste caso específico, basta substituir o primeiro AC por uma nova linha. A abordagem que apresentei acima é mais geral.

Uma abordagem mais poderosa no sed é salvar a linha no espaço de armazenamento, remover todas as partes da linha, mas a primeira parte “interessante” da linha, trocar o espaço de espera e o espaço padrão ou anexar o espaço padrão ao espaço de espera. repetir. No entanto, se você começar a fazer coisas tão complicadas, você deve pensar em mudar para o awk. O awk também não tem correspondência não-gulosa, mas você pode dividir uma string e salvar as partes em variáveis.

    
por 23.07.2016 / 16:26
2

sed - correspondência não-gananciosa de Christoph Sieghart

The trick to get non greedy matching in sed is to match all characters excluding the one that terminates the match. I know, a no-brainer, but I wasted precious minutes on it and shell scripts should be, after all, quick and easy. So in case somebody else might need it:

Greedy matching

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

Non greedy matching

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar

    
por 12.10.2017 / 23:49
0

No seu caso, você pode negar o fechamento do caractere dessa maneira:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'
    
por 23.07.2016 / 01:14