Correspondência não gananciosa com o regex do SED (emule o perl. *?)


21

Eu quero usar sedpara substituir qualquer coisa em uma seqüência de caracteres entre a primeira ABe a primeira ocorrência de AC(inclusive) por XXX.

Por exemplo , eu tenho essa sequência (essa sequência é apenas para um teste):

ssABteAstACABnnACss

e eu gostaria de saída semelhante a esta: ssXXXABnnACss.


Eu fiz isso com perl:

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

mas eu 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

2
Isso não faz sentido. Você tem uma solução funcional em Perl, mas deseja usar o Sed, por quê?
Kusalananda

Respostas:


14

Regexes 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 quantia que não seja AC,
    seguida por
  3. AC

Infelizmente, sednão é possível fazer o número 2 - pelo menos não para uma expressão regular com vários caracteres. Obviamente, para uma expressão regular de um caractere, como @(ou mesmo [123]), podemos fazer [^@]*or [^123]*. E assim podemos contornar as limitações do sed, alterando todas as ocorrências ACpara @e, em seguida, à procura de

  1. AB,
    seguido por
  2. qualquer número que não seja @,
    seguido de
  3. @

como isso:

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

A última parte altera instâncias sem correspondência de @volta para AC.

Mas, é claro, essa é uma abordagem imprudente, porque a entrada já pode conter @caracteres; assim, combinando-os, podemos obter falsos positivos. No entanto, como nenhuma variável do shell terá um caractere NUL ( \x00), é provável que o NUL seja um bom caractere a 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 GNU sed. (Para garantir que os recursos GNU estejam ativados, o usuário não deve ter definido a variável de shell POSIXLY_CORRECT.)

Se você estiver usando sed com o -zsinalizador 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 do padrão e NUL é uma boa opção para a substituição aqui.

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


Acabei de editar sua resposta para adicionar uma explicação longa; fique à vontade para apará-lo ou revertê-lo.
G-Man diz 'Reinstate Monica'

@ G-Man Essa é uma excelente explicação! Muito bem feito. Obrigado.
John1024

Você pode echoou printfum `\ 000 'bem no bash (ou a entrada pode vir de um arquivo). Mas, em geral, é claro que uma sequência de texto provavelmente não possui NULs.
23416 ilkkachu

@ilkkachu Você está certo sobre isso. O que eu deveria ter escrito é que nenhuma variável ou parâmetro do shell pode conter NULs. Resposta atualizada.
John1024

Isso não seria muito mais seguro se você mudasse ACpara AC@e para trás novamente?
Michael Vehrs

7

Algumas sedimplementações têm suporte para isso. ssedtem um modo PCRE:

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

A AT&T ast sed possui conjunção e negação ao usar regexps aumentados :

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

Portably, você pode usar esta técnica: substitua a sequência final (aqui AC) por um único caractere que não ocorre na sequência inicial ou final (como :aqui) para que você possa fazê-lo s/AB[^:]*://, e caso esse caractere possa aparecer na entrada , use um mecanismo de escape que não colidir com as seqüências de início 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 sedprocessa uma linha de cada vez, a nova linha nunca ocorre no espaço do padrão; portanto, é possível:

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

Isso geralmente não funciona com outras sedimplementações porque elas não suportam [^\n]. Com o GNU, sedvocê deve garantir que a compatibilidade do POSIX não esteja ativada (como na variável de ambiente POSIXLY_CORRECT).


6

Não, as expressões regulares sed não têm correspondência não gananciosa.

Você pode corresponder todo o texto até a primeira ocorrência ACusando "qualquer coisa que não contenha AC" seguida de AC, que faz o mesmo que o Perl .*?AC. O fato é que "qualquer coisa que não contenha AC" não pode ser expressa facilmente como uma expressão regular: sempre há uma expressão regular que reconhece a negação de uma expressão regular, mas o regex de negação fica complicado rapidamente. E no 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/'

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

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

No entanto, lembre-se de que a barra invertida-newline não funciona em um conjunto de caracteres com algumas versões sed. Em particular, isso não funciona no GNU sed, que é a implementação sed no Linux não incorporado; No GNU sed, você pode usar \n:

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

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

Uma abordagem mais poderosa no sed é salvar a linha no espaço de espera, remover tudo, exceto a primeira parte "interessante" da linha, trocar o espaço de espera e o espaço do padrão ou anexar o espaço do padrão ao espaço de espera e repetir. No entanto, se você começar a fazer coisas complicadas, pense em mudar para o awk. O Awk também não possui correspondência não gananciosa, mas você pode dividir uma string e salvar as partes em variáveis.


@ilkkachu Não, não é. s/\n//gremove todas as novas linhas.
Gilles 'SO- stop being evil'

asdf. Certo, meu mal.
21916 ilkkachu

3

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

O truque para obter uma correspondência não gananciosa no sed é corresponder a todos os caracteres, exceto aquele que termina a correspondência. Eu sei, um acéfalo, mas desperdicei minutos preciosos e os scripts de shell devem ser, afinal, rápidos e fáceis. Portanto, caso outra pessoa precise:

Correspondência gananciosa

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

Correspondência não gananciosa

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


2
O termo "acéfalo" é ambíguo. Nesse caso, não está claro que você (ou Christoph Sieghart) pensou nisso. Em particular, seria bom se você tivesse mostrado como resolver o problema específico da pergunta (onde o zero de mais expressão é seguido por mais de um caractere ) . Você pode achar que essa resposta não funciona bem nesse caso.
Scott

A toca do coelho é muito mais profunda do que me pareceu à primeira vista. Você está certo, que a solução alternativa não funciona bem para expressões regulares com vários caracteres.
Gresolio # 15/17

0

No seu caso, você pode simplesmente negar o fechamento do char desta maneira:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'

2
A pergunta diz: "Quero substituir qualquer coisa entre a primeira ABe a primeira ocorrência de ACcom XXX..." e fornece ssABteAstACABnnACsscomo exemplo de entrada. Esta resposta funciona para esse exemplo , mas não responde à pergunta em geral. Por exemplo, ssABteCstACABnnACsstambém deve produzir a saída aaXXXABnnACss, mas seu comando passa essa linha inalterada.
G-Man diz 'Reinstate Monica'

0

A solução é bastante simples. .*é ganancioso, mas não é absolutamente ganancioso. Considere a correspondência ssABteAstACABnnACsscom a regexp AB.*AC. O ACque se segue .*deve realmente ter uma correspondência. O problema é que, por .*ser ganancioso, o subsequente ACcorresponderá ao último AC e não ao primeiro. .*come o primeiro ACenquanto o literal ACno regexp corresponde ao último em ssABteAstACABnn AC ss. Para impedir que isso aconteça, basta substituir o primeiro ACpor algo ridículo para diferenciá-lo do segundo e de qualquer outra coisa.

echo ssABteAstACABnnACss | sed 's/AC/-foobar-/; s/AB.*-foobar-/XXX/'
ssXXXABnnACss

O ganancioso .*vai agora parar no pé de -foobar-no ssABteAst-foobar-ABnnACssporque não há nenhum outro -foobar-do que isso -foobar-, eo regexp -foobar- deve ter um jogo. O problema anterior era que o regexp ACtinha duas correspondências, mas por .*ser ganancioso, a última correspondência ACfoi selecionada. No entanto, com -foobar-, apenas uma correspondência é possível, e esta prova que .*não é absolutamente gananciosa. A parada de ônibus para .*ocorre onde resta apenas uma correspondência para o restante da regexp a seguir .*.

Observe que esta solução falhará se um ACaparecer antes da primeira ABporque o errado ACserá substituído por -foobar-. Por exemplo, após a primeira sedsubstituição, ACssABteAstACABnnACsstorna-se -foobar-ssABteAstACABnnACss; portanto, não é possível encontrar uma correspondência AB.*-foobar-. No entanto, se a sequência for sempre ... AB ... AC ... AB ... AC ..., essa solução será bem-sucedida.


0

Uma alternativa é mudar a string para que você queira a combinação gananciosa

echo "ssABtCeCAstACABnnACss" | rev | sed -E "s/(.*)CA.*BA(.*)/\1CA+-+-+-+-BA\2/" | rev

Use revpara inverter a corda, inverta seus critérios de correspondência, use sedda maneira usual e depois inverta o resultado ....

ssAB-+-+-+-+ACABnnACss
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.