Como posso usar o sed para substituir uma string de várias linhas?

243

Percebi que, se eu adicionar \num padrão para substituir usando sed, ele não corresponde. Exemplo:

$ cat > alpha.txt
This is
a test
Please do not
be alarmed

$ sed -i'.original' 's/a test\nPlease do not/not a test\nBe/' alpha.txt

$ diff alpha.txt{,.original}

$ # No differences printed out

Como posso fazer isso funcionar?

Belmin Fernandez
fonte
Solução inteligente aqui: unix.stackexchange.com/a/445666/61742 . Claro que não é performático! Outras boas opções para executar uma substituição de acordo com suas necessidades podem ser awk, perl e python. Existem muitos outros, mas acredito que o awk é o mais universal nas várias distribuições Linux (por exemplo). Obrigado!
Eduardo Lucio

Respostas:

235

No chamado mais simples do sed , ele tem uma linha de texto no espaço padrão, ou seja. 1 linha de \ntexto delimitado da entrada. A única linha no espaço do padrão não tem \n... É por isso que o seu regex não está encontrando nada.

Você pode ler várias linhas no espaço do padrão e manipular as coisas surpreendentemente bem, mas com um esforço mais do que o normal. O Sed tem um conjunto de comandos que permitem esse tipo de coisa ... Aqui está um link para um Resumo de Comando do sed . É o melhor que eu encontrei e me fez rolar.

No entanto, esqueça a idéia "one-liner" quando você começar a usar os micro comandos do sed. É útil apresentá-lo como um programa estruturado até você sentir a sensação ... É surpreendentemente simples e igualmente incomum. Você pode pensar nisso como a "linguagem assembler" da edição de texto.

Resumo: Use sed para coisas simples, e talvez um pouco mais, mas, em geral, quando vai além de trabalhar com uma única linha, a maioria das pessoas prefere outra coisa ...
Vou deixar outra pessoa sugerir outra coisa. realmente não tenho certeza qual seria a melhor opção (eu usaria o sed, mas é porque não conheço o perl o suficiente).


sed '/^a test$/{
       $!{ N        # append the next line when not on the last line
         s/^a test\nPlease do not$/not a test\nBe/
                    # now test for a successful substitution, otherwise
                    #+  unpaired "a test" lines would be mis-handled
         t sub-yes  # branch_on_substitute (goto label :sub-yes)
         :sub-not   # a label (not essential; here to self document)
                    # if no substituion, print only the first line
         P          # pattern_first_line_print
         D          # pattern_ltrunc(line+nl)_top/cycle
         :sub-yes   # a label (the goto target of the 't' branch)
                    # fall through to final auto-pattern_print (2 lines)
       }    
     }' alpha.txt  

Aqui está o mesmo script, condensado no que é obviamente mais difícil de ler e trabalhar, mas alguns chamariam duvidosamente de uma linha

sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;ty;P;D;:y}}' alpha.txt

Aqui está o meu comando "cheat-sheet"

:  # label
=  # line_number
a  # append_text_to_stdout_after_flush
b  # branch_unconditional             
c  # range_change                     
d  # pattern_delete_top/cycle          
D  # pattern_ltrunc(line+nl)_top/cycle 
g  # pattern=hold                      
G  # pattern+=nl+hold                  
h  # hold=pattern                      
H  # hold+=nl+pattern                  
i  # insert_text_to_stdout_now         
l  # pattern_list                       
n  # pattern_flush=nextline_continue   
N  # pattern+=nl+nextline              
p  # pattern_print                     
P  # pattern_first_line_print          
q  # flush_quit                        
r  # append_file_to_stdout_after_flush 
s  # substitute                                          
t  # branch_on_substitute              
w  # append_pattern_to_file_now         
x  # swap_pattern_and_hold             
y  # transform_chars                   
Peter.O
fonte
167
Atire em mim agora. Pior sintaxe de todos os tempos!
Gili
53
Esta é uma explicação fantástica, mas estou inclinado a concordar com @Gili.
Gatoatigrado
11
Sua folha de dicas tem tudo.
Konsolebox
3
Você não precisa de um rótulo para usar o tcomando aqui - quando não recebe um rótulo, o padrão é ramificar para o final do script. O mesmo sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;t;P;D}}' alpha.txtacontece exatamente com o seu comando em todas as circunstâncias. Obviamente, para esse arquivo em particular , sed '/test/{N;s/.*/not a test\nBe/}' alpha.txtfaz o mesmo, mas meu primeiro exemplo é logicamente equivalente para todos os arquivos possíveis. Observe também que \nem uma sequência de substituição não produz uma nova linha; você precisa de uma barra invertida `\` seguida por uma nova linha real para fazer isso.
Curinga
9
Observe que essa sintaxe é específica do GNU ( #comando não separado do anterior, \nno RHS de s). Com o GNU, sedvocê também pode usar -zpara usar registros delimitados por NUL (e depois absorver toda a entrada se for texto (que por definição não contém NULs)).
Stéphane Chazelas
181

Use em perlvez de sed:

$ perl -0777 -i.original -pe 's/a test\nPlease do not/not a test\nBe/igs' alpha.txt
$ diff alpha.txt{,.original}
2,3c2,3
< not a test
< Be
---
> a test
> Please do not

-pi -eé a sequência padrão da linha de comando "substituir no local" e -0777 faz com que o perl elimine os arquivos inteiros. Veja perldoc perlrun para descobrir mais sobre isso.

codehead
fonte
3
Obrigado! Para o trabalho em várias linhas, o perl ganha as mãos! Acabei usando `$ perl -pi -e 's / bar / baz /' fileA` para alterar o arquivo no local.
Nicholas Tolley Cottrell
3
É muito comum que o pôster original solicite sede responda usando awk ou perl. Eu acho que não é sobre o assunto, portanto, desculpe, mas eu demiti um menos.
Rho Phi
68
+1 e discordo de Roberto. Muitas vezes, perguntas formuladas especificamente para a ignorância de métodos melhores. Quando não há uma diferença contextual substantiva (como aqui), as soluções ótimas devem ter pelo menos tanto perfil quanto as específicas da pergunta.
Geotheory #
56
Eu acho que a sedresposta acima prova que uma resposta Perl está no tópico.
reinierpost
7
Um pouco mais fácil: com "-p0e", o "-0777" não é necessário. unix.stackexchange.com/a/181215/197502
Weidenrinde 3/17
96

Eu acho que é melhor substituir o \nsímbolo por outro símbolo e depois trabalhar como de costume:

por exemplo, código fonte não trabalhado:

cat alpha.txt | sed -e 's/a test\nPlease do not/not a test\nBe/'

pode ser alterado para:

cat alpha.txt | tr '\n' '\r' | sed -e 's/a test\rPlease do not/not a test\rBe/'  | tr '\r' '\n'

Se alguém não souber, o \nfinal da linha UNIX é o \r\nWindows \rclássico do Mac OS. O texto normal do UNIX não usa \rsímbolo; portanto, é seguro usá-lo neste caso.

Você também pode usar algum símbolo exótico para substituir temporariamente \ n. Como exemplo - \ f (símbolo de avanço de formulário). Você pode encontrar mais símbolos aqui .

cat alpha.txt | tr '\n' '\f' | sed -e 's/a test\fPlease do not/not a test\fBe/'  | tr '\f' '\n'
xara
fonte
11
+1 para este truque inteligente! Especialmente útil é o conselho sobre o uso de um símbolo exótico para substituir temporariamente a nova linha, a menos que você esteja absolutamente certo sobre o conteúdo do arquivo que está editando.
L0j1k
Isso não funciona como está escrito no OS X. Em vez disso, é necessário substituir todas as instâncias do \rargumento por sedpor $(printf '\r').
Abeboparebop
@abeboparebop: ótimo achado! 👍 Como alternativa, instale o GNU sed usando o homebrew: stackoverflow.com/a/30005262
ssc
@abeboparebop, No OSX, você só precisa adicionar um $antes da string sed para impedir que ela converta o \rpara um r. Exemplo curto: sed $'s/\r/~/'. Exemplo completo:cat alpha.txt | tr '\n' '\r' | sed $'s/a test\rPlease do not/not a test\rBe/' | tr '\r' '\n'
wisbucky
40

Considerando tudo, devorar o arquivo inteiro pode ser o caminho mais rápido.

A sintaxe básica é a seguinte:

sed -e '1h;2,$H;$!d;g' -e 's/__YOUR_REGEX_GOES_HERE__...'

Lembre-se, devorar o arquivo inteiro pode não ser uma opção se o arquivo for tremendamente grande. Para esses casos, outras respostas fornecidas aqui oferecem soluções personalizadas que garantem o trabalho em um pequeno espaço de memória.

Para todas as outras situações de hack e slash, apenas o prefixo -e '1h;2,$H;$!d;g'seguido pelo sedargumento regex original praticamente faz o trabalho.

por exemplo

$ echo -e "Dog\nFox\nCat\nSnake\n" | sed -e '1h;2,$H;$!d;g' -re 's/([^\n]*)\n([^\n]*)\n/Quick \2\nLazy \1\n/g'
Quick Fox
Lazy Dog
Quick Snake
Lazy Cat

O que -e '1h;2,$H;$!d;g'faz?

A 1, 2,$, $!partes são linha especificadores que limite que linhas a seguinte directamente comando é executado.

  • 1: Apenas primeira linha
  • 2,$: Todas as linhas começando a partir do segundo
  • $!: Todas as linhas, exceto a última

Tão expandido, é o que acontece em cada linha de uma entrada de linha N.

  1: h, d
  2: H, d
  3: H, d
  .
  .
N-2: H, d
N-1: H, d
  N: H, g

O gcomando não recebe um especificador de linha, mas o dcomando anterior possui uma cláusula especial " Iniciar próximo ciclo " e isso impede a gexecução em todas as linhas, exceto a última.

Quanto ao significado de cada comando:

  • O primeiro hseguido por Hs em cada linha copia as referidas linhas de entrada no espaço de espera desed s . (Pense em um buffer de texto arbitrário.)
  • Depois, ddescarta cada linha para impedir que essas linhas sejam gravadas na saída. O espaço de espera, no entanto, é preservado.
  • Por fim, na última linha, grestaura o acúmulo de todas as linhas do espaço de espera, para que sedseja possível executar sua regex em toda a entrada (em vez de ser linha por vez) e, portanto, poder combinar em \ns.
Antak
fonte
38

sedtem três comandos para gerenciar operações de multi-linha: N, De P(compará-los com normalidade n , de p).

Nesse caso, você pode corresponder à primeira linha do seu padrão, use Npara anexar a segunda linha ao espaço do padrão e, em seguida, use spara fazer sua substituição.

Algo como:

/a test$/{
  N
  s/a test\nPlease do not/not a test\nBe/
}
andcoz
fonte
2
Isso é incrível! Mais simples que a resposta aceita e ainda eficaz.
Jeyk
E todos os que envolvem o espaço de espera ( G, H, x...). Mais linhas podem ser adicionadas ao espaço do padrão com o scomando também.
Stéphane Chazelas 11/08/16
esta solução não funciona com o seguinte caso "Este é \ na test \ na test \ n Por favor, não \ n se assuste"
mug896
@ mug896 você provavelmente precisará de vários Ncomandos
loa_in_
15

Você pode, mas é difícil . Eu recomendo mudar para uma ferramenta diferente. Se houver uma expressão regular que nunca corresponda a nenhuma parte do texto que você deseja substituir, você poderá usá-la como um separador de registros do awk no GNU awk.

awk -v RS='a' '{gsub(/hello/, "world"); print}'

Se nunca houver duas linhas novas na sequência de caracteres de pesquisa, você poderá usar o "modo de parágrafo" do awk (uma ou mais linhas em branco separam os registros).

awk -v RS='' '{gsub(/hello/, "world"); print}'

Uma solução fácil é usar Perl e carregar o arquivo completamente na memória.

perl -0777 -pe 's/hello/world/g'
Gilles
fonte
1
Como aplicar o comando perl a um arquivo?
sebix
2
@sebix perl -0777 -pe '…' <input-file >output-file. Para modificar um arquivo no lugar, #perl -0777 -i -pe '…' filename
247 Gilles
3
Ver também GNU sed's -zopção (adicionado em 2012 depois que a resposta foi publicado): seq 10 | sed -z 's/4\n5/a\nb/'.
Stéphane Chazelas
7

Eu acho que esta é a solução sed para duas linhas correspondentes.

sed -n '$!N;s@a test\nPlease do not@not a test\nBe@;P;D' alpha.txt

Se você quiser 3 linhas correspondentes, então ...

sed -n '1{$!N};$!N;s@aaa\nbbb\nccc@xxx\nyyy\nzzz@;P;D'

Se você quiser 4 linhas correspondentes, então ...

sed -n '1{$!N;$!N};$!N;s@ ... @ ... @;P;D'

Se a peça de substituição no comando "s" encolher linhas, será um pouco mais complicado assim

# aaa\nbbb\nccc shrink to one line "xxx"

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@xxx@;$!N;$!N};P;D'

Se a parte de substituição crescer linhas, será um pouco mais complicado assim

# aaa\nbbb\nccc grow to five lines vvv\nwww\nxxx\nyyy\nzzz

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@vvv\nwww\nxxx\nyyy\nzzz@;P;s/.*\n//M;P;s/.*\n//M};P;D'
mug896
fonte
Isso deve chegar ao topo! Eu apenas usei o "-i" em vez de "-n" para a substituição de duas linhas, porque é disso que eu preciso e, aliás, também está no exemplo do autor.
Nagev 15/05/19
5
sed -i'.original' '/a test/,/Please do not/c not a test \nBe' alpha.txt

Aqui /a test/,/Please do not/é considerado como um bloco de texto (multilinhas), cé o comando change seguido por um novo textonot a test \nBe

No caso de o texto a ser substituído ser muito longo, sugiro a sintaxe ex .

gibies
fonte
opa, o problema é que o sed substituirá todo o texto eventual entre / a test / e / Please / not not / well ... :(
noonex 24/11/16
4
sed -e'$!N;s/^\(a test\n\)Please do not be$/not \1Be/;P;D' <in >out

Apenas amplie sua janela na entrada um pouco.

É bem fácil. Além da substituição padrão; você só precisa $!N, Pe Daqui.

mikeserv
fonte
4

Além do Perl, uma abordagem geral e útil para edição de múltiplas linhas para fluxos (e arquivos também) é:

Primeiro, crie um novo separador de linhas UNIQUE como desejar, por exemplo

$ S=__ABC__                     # simple
$ S=__$RANDOM$RANDOM$RANDOM__   # better
$ S=$(openssl rand -hex 16)     # ultimate

Em seu comando sed (ou qualquer outra ferramenta), você substitui \ n por $ {S}, como

$ cat file.txt | awk 1 ORS=$S |  sed -e "s/a test${S}Please do not/not a test\nBe/" | awk 1 RS=$S > file_new.txt

(o awk substitui o separador de linhas ASCII pelo seu e vice-versa.)

convidado
fonte
2

Esta é uma pequena modificação da resposta inteligente do xara para fazê-lo funcionar no OS X (estou usando o 10.10):

cat alpha.txt | tr '\n' '\r' | sed -e 's/a test$(printf '\r')Please do not/not a test$(printf '\r')Be/'  | tr '\r' '\n'

Em vez de usar explicitamente \r, você deve usar $(printf '\r').

abeboparebop
fonte
1
Enquanto printf '\r'(ou echo -e '\r') funciona corretamente, observe que você pode apenas usar a sintaxe do shell $'\r'para se referir a literais de escape. Por exemplo, echo hi$'\n'thereecoará uma nova linha entre hie there. Da mesma forma, você pode agrupar a sequência inteira para que cada barra invertida \ escape do caractere subseqüente:echo $'hi\nthere'
Dejay Clayton 28/02
1

Eu queria adicionar algumas linhas de HTML a um arquivo usando sed (e acabei aqui). Normalmente eu usava perl, mas estava na caixa que tinha sed, bash e não muito mais. Eu descobri que se eu alterasse a string para uma única linha e deixasse bash / sed interpolar o \ t \ n tudo funcionava:

HTML_FILE='a.html' #contains an anchor in the form <a name="nchor" />
BASH_STRING_A='apples'
BASH_STRING_B='bananas'
INSERT="\t<li>$BASH_STRING_A<\/li>\n\t<li>$BASH_STRING_B<\/li>\n<a name=\"nchor\"\/>"
sed -i "s/<a name=\"nchor"\/>/$INSERT/" $HTML_FILE

Seria mais limpo ter uma função para escapar das aspas duplas e das barras, mas às vezes a abstração é a ladra do tempo.

Alexx Roche
fonte
1

O GNU sedtem uma -zopção que permite usar a sintaxe que o OP tentou aplicar. ( página de manual )

Exemplo:

$ cat alpha.txt
This is
a test
Please do not
be alarmed
$ sed -z 's/a test\nPlease do not\nbe/not a test\nBe/' -i alpha.txt
$ cat alpha.txt
This is
not a test
Be alarmed

Esteja ciente: se você usar ^e $agora eles correspondem ao início e ao fim das linhas delimitadas com um caractere NUL (não \n). E, para garantir que as correspondências em todas as suas \nlinhas ( separadas) sejam substituídas, não se esqueça de usar o gsinalizador para substituições globais (por exemplo s/.../.../g).


Créditos: @ stéphane-chazelas mencionou pela primeira vez -z em um comentário acima.

Peterino
fonte
0

Sed quebra a entrada em novas linhas. Ele mantém apenas uma linha por loop.
Portanto, não há como corresponder a \n(nova linha) se o espaço do padrão não o contiver.

Há uma maneira, porém, de fazer o sed manter duas linhas consecutivas no espaço do padrão usando o loop:

sed 'N;l;P;D' alpha.txt

Adicione qualquer processamento necessário entre N e P (substituindo o l).

Nesse caso (2 linhas):

$ sed 'N;s/a test\nPlease do not/not a test\nBe/;P;D' alpha.txt
This is
not a test
Be
be alarmed

Ou, por três linhas:

$ sed -n '1{$!N};$!N;s@a test\nPlease do not\nbe@not a test\nDo\nBe@;P;D' alpha.txt 
This is
not a test
Do
Be alarmed

Isso pressupõe que a mesma quantidade de linhas seja substituída.

Isaac
fonte