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

224

Tenho notado que, se eu adicionar \n a um padrão para substituir usando sed , ele não corresponderá. 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?

    
por Belmin Fernandez 07.12.2011 / 19:03

12 respostas

218

Na chamada mais simples de sed , tem uma linha de texto no espaço padrão, ie. 1 linha de texto delimitado por \n da entrada. A única linha no espaço padrão não tem \n ... É por isso que o seu regex não está encontrando nada.

Você pode ler várias linhas no padrão de espaço e manipular as coisas surpreendentemente bem, mas com um esforço mais que normal. Sed tem um conjunto de comandos que permitem esse tipo de coisa ... Aqui está um link para a < a href="http://docstore.mik.ua/orelly/unix/sedawk/appa_03.htm"> Resumo de Comandos para sed . É o melhor que eu encontrei e me fez rolar.

No entanto, esqueça a ideia do "one-liner" quando começar a usar os micro-comandos do sed. É útil descrevê-lo como um programa estruturado até que você perceba ... É surpreendentemente simples e igualmente incomum. Você poderia pensar nisso como a "linguagem assembler" de edição de texto.

Resumo: Use sed para coisas simples, e talvez um pouco mais, mas em geral, quando fica além de trabalhar com uma única linha, a maioria das pessoas prefere algo diferente ...
Eu vou deixar alguém sugerir outra coisa .. Eu realmente não tenho certeza qual seria a melhor escolha (eu usaria sed, mas isso é porque eu não sei perl bem 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 roteiro, condensado no que é obviamente mais difícil de ler e trabalhar, mas alguns chamariam duvidosamente de um verso

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                   
    
por 07.12.2011 / 21:45
170

Use perl em vez 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 é sua sequência de linha de comando "substituir no local" padrão e -0777 faz com que o perl sorve arquivos inteiros. Consulte perldoc perlrun para saber mais sobre isso.

    
por 07.12.2011 / 21:20
86

Acho melhor substituir o símbolo \n por outro símbolo e depois funcionar 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 sabe, \n é o final da linha do UNIX, \r\n - windows, \r - Mac OS clássico. O texto normal do UNIX não usa o símbolo \r , portanto, é seguro usá-lo para este caso.

Você também pode usar algum símbolo exótico para substituir temporariamente \ n. Por exemplo - \ f (símbolo do feed 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'
    
por 27.08.2014 / 12:03
37

sed tem três comandos para gerenciar operações de várias linhas: N , D e P (compare-os com normal n , d e p ).

Nesse caso, você pode combinar a primeira linha do seu padrão, usar N para acrescentar a segunda linha ao espaço padrão e usar s para fazer a sua substituição.

Algo como:

/a test$/{
  N
  s/a test\nPlease do not/not a test\nBe/
}
    
por 07.12.2011 / 21:17
35

Tudo considerado, devorando o arquivo inteiro pode ser o caminho mais rápido a seguir.

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 muito grande. Para esses casos, outras respostas fornecidas aqui oferecem soluções personalizadas com garantia de funcionamento em uma pequena área de memória.

Para todas as outras situações de hack and slash, apenas prefixar -e '1h;2,$H;$!d;g' seguido por seu argumento sed 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 \nLazy \n/g'
Quick Fox
Lazy Dog
Quick Snake
Lazy Cat

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

As partes 1 , 2,$ , $! são especificadores de linha que limitam em quais linhas o comando a seguir é executado.

  • 1 : somente primeira linha
  • 2,$ : todas as linhas a partir do segundo
  • $! : cada linha diferente da última

Então, expandido, isso é 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 comando g não recebe um especificador de linha, mas o comando d anterior tem uma cláusula especial " Iniciar próximo ciclo. ", e isso impede que g seja executado em todos linhas, exceto a última.

Quanto ao significado de cada comando:

  • O primeiro h seguido por H s em cada linha copia as ditas linhas de entrada para o espaço hold de sed . (Pense buffer de texto arbitrário.)
  • Depois, d descarta cada linha para evitar que essas linhas sejam gravadas na saída. O espaço de espera , no entanto, é preservado.
  • Finalmente, na última linha, g restaura o acúmulo de cada linha do espaço de espera para que sed seja capaz de executar sua regex em toda a entrada (em vez de em uma linha de moda de cada vez) e, portanto, é capaz de corresponder em \n s.
por 09.10.2015 / 14:58
15

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

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

Se nunca houver duas novas linhas consecutivas em sua string de pesquisa, você pode usar o "modo de parágrafo" do awk (uma ou mais linhas em branco separam registros).

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

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

perl -0777 -pe 's/hello/world/g'
    
por 08.12.2011 / 00:11
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 (de várias linhas), c é o comando de alteração seguido pelo novo texto not a test \nBe

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

    
por 16.08.2016 / 12:38
4

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

Primeiro, crie um novo separador de linha UNIQUE como quiser, por exemplo

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

Então, no 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 linha ASCII pelo seu e vice-versa).

    
por 08.06.2017 / 08:07
4

Acho que esta é a solução sed para 2 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 então 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 repasse crescer linhas, então um pouco mais complicado como este

# 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'
    
por 14.01.2017 / 17:55
3
sed -e'$!N;s/^\(a test\n\)Please do not be$/not Be/;P;D' <in >out

Apenas amplie sua janela para inserir um pouco.

É bem fácil. Além da substituição padrão; você precisa apenas de $!N , P e D aqui.

    
por 24.01.2016 / 20:26
2

Esta é uma pequena modificação da resposta inteligente do xara para fazê-lo funcionar no OS X (eu estou usando 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ê precisa usar $(printf '\r') .

    
por 11.02.2016 / 16:36
1

Eu queria adicionar algumas linhas de HTML a um arquivo usando sed (e terminei aqui). Normalmente eu usaria perl, mas eu estava na caixa que tinha sed, bash e não muito mais. Descobri que, se eu alterei a string para uma única linha e deixei o bash / sed interpolar o \ t \ n tudo funcionou:

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 é o ladrão do tempo.

    
por 20.09.2016 / 22:33