Como substituir vários padrões ao mesmo tempo pelo sed?

231

Suponha que eu tenha a string 'abbc' e queira substituir:

  • ab -> bc
  • bc -> ab

Se eu tentar duas substituições, o resultado não é o que eu quero:

echo 'abbc' | sed 's/ab/bc/g;s/bc/ab/g'
abab

Então, qual comando sed posso usar para substituir como abaixo?

echo abbc | sed SED_COMMAND
bcab

Edição : Na verdade, o texto pode ter mais de 2 padrões e não sei quantas substituições vou precisar. Como houve uma resposta dizendo que sedé um editor de fluxo e suas substituições são avidamente, acho que precisarei usar alguma linguagem de script para isso.

DaniloNC
fonte
Você precisa fazer várias substituições na mesma linha? Se não, basta soltar a gbandeira de ambos os s///comandos e isso funcionará.
Etan Reisner 26/10
Você perdeu o ponto da minha pergunta. Eu quis dizer que você precisa fazer cada substituição mais de uma vez na mesma linha. Existe mais de uma correspondência para ab ou bc na entrada original.
Etan Reisner 26/10
Desculpe @EtanReisner, eu entendi errado, a resposta é sim. o texto pode ter várias substituições.
DaniloNC # 26/14

Respostas:

342

Talvez algo parecido com isto:

sed 's/ab/~~/g; s/bc/ab/g; s/~~/bc/g'

Substitua ~por um caractere que você sabe que não estará na string.

ooga
fonte
9
O GNU sed lida com nuls, para que você possa usá \x0-lo ~~.
jthill
3
É gnecessário e o que faz?
Lee
12
O @Lee gé para global - ele substitui todas as instâncias do padrão em cada linha, em vez de apenas a primeira (que é o comportamento padrão).
precisa saber é o seguinte
1
Consulte minha resposta stackoverflow.com/a/41273117/539149 para obter uma variação da resposta de ooga que pode substituir várias combinações simultaneamente.
Zack Morris
3
que você sabe que não estará na sequência Para o código de produção, nunca faça nenhuma suposição sobre a entrada. Para testes, bem, testes nunca realmente provam correção, mas uma boa idéia para um teste é: Use o próprio script como entrada.
hagello
33

Eu sempre uso várias instruções com "-e"

$ sed -e 's:AND:\n&:g' -e 's:GROUP BY:\n&:g' -e 's:UNION:\n&:g' -e 's:FROM:\n&:g' file > readable.sql

Isso acrescentará um '\ n' antes de todos os AND, GROUP BY, UNION e FROM, enquanto '&' significa a string correspondente e '\ n &' significa que você deseja substituir a string correspondente por um '\ n' antes do 'correspondente "

Paulo Henrique Lellis Gonalves
fonte
14

Aqui está uma variação da resposta de ooga que funciona para várias pesquisas e substitui pares sem precisar verificar como os valores podem ser reutilizados:

sed -i '
s/\bAB\b/________BC________/g
s/\bBC\b/________CD________/g
s/________//g
' path_to_your_files/*.txt

Aqui está um exemplo:

antes:

some text AB some more text "BC" and more text.

depois de:

some text BC some more text "CD" and more text.

Observe que \bdenota limites de palavras, o que evita que ________interfira na pesquisa (estou usando o GNU sed 4.2.2 no Ubuntu). Se você não estiver usando uma pesquisa de limite de palavras, essa técnica poderá não funcionar.

Observe também que isso fornece os mesmos resultados que remover s/________//ge anexar && sed -i 's/________//g' path_to_your_files/*.txtao final do comando, mas não requer a especificação do caminho duas vezes.

Uma variação geral disso seria usar \x0ou _\x0_substituir, ________se você souber que nenhum nulo aparece em seus arquivos, como sugerido por jthill .

Zack Morris
fonte
Eu concordo com o comentário de hagello acima sobre não fazer suposições do que a entrada pode conter. Portanto, eu pessoalmente acho que esta é a solução mais confiável, além de tubulação SEDS em cima uns dos outros ( sed 's/ab/xy/' | sed 's/cd/ab/' .....)
leetbacoon
12

sedé um editor de stream. Ele procura e substitui avidamente. A única maneira de fazer o que você pediu é usar um padrão de substituição intermediário e alterá-lo no final.

echo 'abcd' | sed -e 's/ab/xy/;s/cd/ab/;s/xy/cd/'

kuriouscoder
fonte
4

Isso pode funcionar para você (GNU sed):

sed -r '1{x;s/^/:abbc:bcab/;x};G;s/^/\n/;:a;/\n\n/{P;d};s/\n(ab|bc)(.*\n.*:(\1)([^:]*))/\4\n\2/;ta;s/\n(.)/\1\n/;ta' file

Isso usa uma tabela de pesquisa que é preparada e mantida no espaço de espera (HS) e depois anexada a cada linha. Um marcador exclusivo (neste caso \n) é anexado ao início da linha e usado como um método para percorrer a pesquisa por todo o comprimento da linha. Quando o marcador chega ao final da linha, o processo termina e é impressa a tabela de pesquisa e os marcadores que estão sendo descartados.

NB A tabela de pesquisa é preparada no início e um segundo marcador exclusivo (neste caso :) é escolhido para não colidir com as cadeias de substituição.

Com alguns comentários:

sed -r '
  # initialize hold with :abbc:bcab
  1 {
    x
    s/^/:abbc:bcab/
    x
  }

  G        # append hold to patt (after a \n)

  s/^/\n/  # prepend a \n

  :a

  /\n\n/ {
    P      # print patt up to first \n
    d      # delete patt & start next cycle
  }

  s/\n(ab|bc)(.*\n.*:(\1)([^:]*))/\4\n\2/
  ta       # goto a if sub occurred

  s/\n(.)/\1\n/  # move one char past the first \n
  ta       # goto a if sub occurred
'

A tabela funciona assim:

   **   **   replacement
:abbc:bcab
 **   **     pattern
potong
fonte
3

Pode ser uma abordagem mais simples para ocorrência de padrão único, você pode tentar como abaixo: echo 'abbc' | sed 's / ab / bc /; s / bc / ab / 2'

Minha saída:

 ~# echo 'abbc' | sed 's/ab/bc/;s/bc/ab/2'
 bcab

Para várias ocorrências de padrão:

sed 's/\(ab\)\(bc\)/\2\1/g'

Exemplo

~# cat try.txt
abbc abbc abbc
bcab abbc bcab
abbc abbc bcab

~# sed 's/\(ab\)\(bc\)/\2\1/g' try.txt
bcab bcab bcab
bcab bcab bcab
bcab bcab bcab

Espero que isto ajude !!

dst_91
fonte
2

Tcl tem um builtin para este

$ tclsh
% string map {ab bc bc ab} abbc
bcab

Isso funciona movendo a string um caractere de cada vez, fazendo comparações de strings começando na posição atual.

Em perl:

perl -E '
    sub string_map {
        my ($str, %map) = @_;
        my $i = 0;
        while ($i < length $str) {
          KEYS:
            for my $key (keys %map) {
                if (substr($str, $i, length $key) eq $key) {
                    substr($str, $i, length $key) = $map{$key};
                    $i += length($map{$key}) - 1;
                    last KEYS;
                }
            }
            $i++;
        }
        return $str;
    }
    say string_map("abbc", "ab"=>"bc", "bc"=>"ab");
'
bcab
Glenn Jackman
fonte
0

Aqui está um awkbaseado em oogassed

echo 'abbc' | awk '{gsub(/ab/,"xy");gsub(/bc/,"ab");gsub(/xy/,"bc")}1'
bcab
Jotne
fonte