¿Cómo puedo usar sed para reemplazar una cadena de varias líneas?


243

Me di cuenta de que, si agrego \nun patrón para sustituir el uso sed, no coincide. Ejemplo:

$ 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

¿Cómo puedo hacer que esto funcione?


Solución inteligente aquí: unix.stackexchange.com/a/445666/61742 . ¡Por supuesto que no es performatico! Otras buenas opciones para realizar un reemplazo de acuerdo a sus necesidades pueden ser awk, perl y python. Hay muchos otros, pero creo que awk es el más universal en las diversas distribuciones de Linux (por ejemplo). ¡Gracias!
Eduardo Lucio

Respuestas:


235

En la llamada más simple de sed , tiene una línea de texto en el espacio del patrón, es decir. 1 línea de \ntexto delimitado de la entrada. La línea única en el espacio del patrón no tiene \n... Es por eso que su expresión regular no encuentra nada.

Puede leer varias líneas en el espacio de patrones y manipular cosas sorprendentemente bien, pero con un esfuerzo más que normal. Sed tiene un conjunto de comandos que permiten este tipo de cosas ... Aquí hay un enlace a un Resumen de comandos para sed . Es el mejor que he encontrado, y me puso en marcha.

Sin embargo, olvide la idea de "una línea" una vez que comience a usar los microcomandos de sed. Es útil diseñarlo como un programa estructurado hasta que lo sienta ... Es sorprendentemente simple e igualmente inusual. Se podría considerar como el "lenguaje ensamblador" de la edición de texto.

Resumen: use sed para cosas simples, y tal vez un poco más, pero en general, cuando va más allá de trabajar con una sola línea, la mayoría de las personas prefieren algo más ...
Dejaré que alguien más sugiera algo más ... Estoy realmente no estoy seguro de cuál sería la mejor opción (usaría sed, pero eso es porque no conozco a Perl lo suficientemente bien).


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  

Aquí es el mismo guión, condensado en lo que obviamente es más difícil de leer y trabajar, pero algunos llamarían dudosamente una sola frase.

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

Aquí está mi comando "hoja de trucos"

:  # 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                   

167
Dispárame ahora. ¡La peor sintaxis!
Gili

53
Esta es una explicación fantástica, pero estoy de acuerdo con @Gili.
gatoatigrado

11
Tu hoja de trucos lo tiene todo.
konsolebox

3
No necesita una etiqueta para usar el tcomando aquí; cuando no se le da una etiqueta, el valor predeterminado es la bifurcación hasta el final del guión. Entonces, sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;t;P;D}}' alpha.txthace exactamente lo mismo que su comando en todas las circunstancias. Por supuesto, para este archivo en particular , sed '/test/{N;s/.*/not a test\nBe/}' alpha.txthace lo mismo también, pero mi primer ejemplo es lógicamente equivalente para todos los archivos posibles. También tenga \nen cuenta que en una cadena de reemplazo no produce una nueva línea; necesita una barra invertida `\` seguida de una nueva línea real para hacerlo.
Comodín el

99
Tenga en cuenta que esa sintaxis es específica de GNU ( #comando no separado del anterior, \nen RHS de s). Con GNU sedtambién puede usar -zpara usar registros delimitados por NUL (y luego arrastrar toda la entrada si es texto (que por definición no contiene NUL)).
Stéphane Chazelas

181

Usar en perllugar 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 -ees su secuencia de línea de comandos estándar "reemplazar en el lugar", y -0777 hace que Perl sorba los archivos completos. Consulte perldoc perlrun para obtener más información al respecto.


3
¡Gracias! Para el trabajo multilínea, ¡Perl gana sin dudas! Terminé usando `$ perl -pi -e 's / bar / baz /' fileA` para cambiar el archivo en su lugar.
Nicholas Tolley Cottrell

3
Es muy común que el póster original pida sedy que aparezcan respuestas usando awk o perl. Creo que no está en el tema, por lo tanto, lo siento, pero despedí a uno menos.
Rho Phi

68
+1 y no estoy de acuerdo con Roberto. A menudo las preguntas formuladas específicamente por ignorancia de mejores métodos. Cuando no hay una diferencia contextual sustancial (como aquí), las soluciones óptimas deberían tener al menos tanto perfil como las preguntas específicas.
geotheory

56
Creo que la sedrespuesta anterior prueba que una respuesta de Perl está en el tema.
reinierpost

77
Un poco más fácil: con "-p0e" el "-0777" no es necesario. unix.stackexchange.com/a/181215/197502
Weidenrinde

96

Creo que es mejor reemplazar el \nsímbolo con algún otro símbolo, y luego trabajar como de costumbre:

por ejemplo, código fuente no trabajado:

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

se puede cambiar a:

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

Si alguien no lo sabe, \nes el final de línea UNIX, \r\n- windows, \r- Mac OS clásico. El texto normal de UNIX no usa el \rsímbolo, por lo que es seguro usarlo para este caso.

También puede usar algún símbolo exótico para reemplazar temporalmente \ n. Como ejemplo - \ f (símbolo de alimentación de formulario). Puedes encontrar más símbolos aquí .

cat alpha.txt | tr '\n' '\f' | sed -e 's/a test\fPlease do not/not a test\fBe/'  | tr '\f' '\n'

11
¡+1 para este truco inteligente! Especialmente útil es el consejo sobre el uso de un símbolo exótico para reemplazar temporalmente la nueva línea a menos que esté absolutamente seguro sobre el contenido del archivo que está editando.
L0j1k

Esto no funciona como está escrito en OS X. En cambio, uno necesita reemplazar todas las instancias de \ren el argumento sedcon $(printf '\r').
abeboparebop

@abeboparebop: ¡gran descubrimiento! 👍 alternativamente, instale GNU sed usando homebrew: stackoverflow.com/a/30005262
ssc

@abeboparebop, en OSX, solo necesita agregar un $antes de la cadena sed para evitar que se convierta \ren un r. Ejemplo corto: sed $'s/\r/~/'. Ejemplo completo:cat alpha.txt | tr '\n' '\r' | sed $'s/a test\rPlease do not/not a test\rBe/' | tr '\r' '\n'
wisbucky

40

A fin de cuentas , engullir todo el archivo puede ser la forma más rápida de hacerlo.

La sintaxis básica es la siguiente:

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

Eso sí, engullir todo el archivo puede no ser una opción si el archivo es tremendamente grande. Para tales casos, otras respuestas proporcionadas aquí ofrecen soluciones personalizadas que están garantizadas para funcionar en una pequeña huella de memoria.

Para todas las demás situaciones de pirateo y corte, el simple hecho de anteponer -e '1h;2,$H;$!d;g'seguido de su sedargumento original de expresiones regulares prácticamente hace el trabajo.

p.ej

$ 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

¿Qué -e '1h;2,$H;$!d;g'hacer?

El 1, 2,$, $!las partes son línea de especificadores de ese límite que líneas el comando directamente siguiente se ejecuta en.

  • 1: Solo en primera línea
  • 2,$: Todas las líneas a partir de la segunda
  • $!: Cada línea que no sea la última

Ampliado, esto es lo que sucede en cada línea de una entrada de línea N.

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

El gcomando no tiene un especificador de línea, pero el dcomando anterior tiene una cláusula especial " Iniciar próximo ciclo ", y esto evita que se gejecute en todas las líneas, excepto en la última.

En cuanto al significado de cada comando:

  • El primero hseguido de Hs en cada línea copia dichas líneas de entrada en sedel espacio de espera . (Piense en un búfer de texto arbitrario).
  • Luego, ddescarta cada línea para evitar que estas líneas se escriban en la salida. El espacio de la bodega sin embargo se conserva.
  • Finalmente, en la última línea, grestaura la acumulación de cada línea desde el espacio de espera para que sedpueda ejecutar su expresión regular en toda la entrada (en lugar de una línea a la vez), y por lo tanto es capaz de partido en \ns.

38

sedtiene tres comandos para gestionar varias líneas de operaciones: N, Dy P(comparar a la normalidad n , dy p).

En este caso, puede hacer coincidir la primera línea de su patrón, usar Npara agregar la segunda línea al espacio del patrón y luego usar spara hacer su sustitución.

Algo como:

/a test$/{
  N
  s/a test\nPlease do not/not a test\nBe/
}

2
¡Esto es asombroso! Más simple que la respuesta aceptada y aún eficaz.
jeyk

Y todos los que implican el espacio de bodega ( G, H, x...). Se pueden agregar más líneas al espacio del patrón con el scomando también.
Stéphane Chazelas


esta solución no funciona con el siguiente caso "Esto es \ na prueba \ na prueba \ n Por favor, no \ n se alarme"
mug896

@ mug896 lo más probable es que necesite múltiples Ncomandos
loa_in_

15

Puedes pero es difícil . Recomiendo cambiar a una herramienta diferente. Si hay una expresión regular que nunca coincide con ninguna parte del texto que desea reemplazar, puede usarla como un separador de registro awk en GNU awk.

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

Si nunca hay dos líneas nuevas consecutivas en su cadena de búsqueda, puede usar el "modo de párrafo" de awk (una o más líneas en blanco separan los registros).

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

Una solución fácil es usar Perl y cargar el archivo completamente en la memoria.

perl -0777 -pe 's/hello/world/g'

1
¿Cómo aplicar el comando perl a un archivo?
sebix

2
@sebix perl -0777 -pe '…' <input-file >output-file. Para modificar un archivo en su lugar,perl -0777 -i -pe '…' filename
Gilles

3
Ver también GNU sed's -zopción (añadido en 2012 después de que la respuesta fue publicada): seq 10 | sed -z 's/4\n5/a\nb/'.
Stéphane Chazelas

7

Creo que esta es la solución sed para la coincidencia de 2 líneas.

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

Si quieres que coincidan 3 líneas, entonces ...

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

Si quieres que coincidan 4 líneas, entonces ...

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

Si la parte de reemplazo en el comando "s" reduce las líneas, entonces es un poco más complicado como este

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

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

Si la parte de reparación crece líneas entonces un poco más 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'

¡Esto debería llegar a la cima! Acabo de usar "-i" en lugar de "-n" para la sustitución de dos líneas, porque eso es lo que necesito, y por cierto, también está en el ejemplo del autor de la pregunta.
Nagev

5
sed -i'.original' '/a test/,/Please do not/c not a test \nBe' alpha.txt

Aquí /a test/,/Please do not/se considera como un bloque de texto (de varias líneas), ces el comando de cambio seguido de un nuevo textonot a test \nBe

En el caso de que el texto a reemplazar sea muy largo, sugeriría la sintaxis ex .


¡Vaya! El problema es que sed reemplazará todo el texto eventual entre / una prueba / y / Por favor, no / también ... :(
noonex

4
sed -e'$!N;s/^\(a test\n\)Please do not be$/not \1Be/;P;D' <in >out

Simplemente amplíe un poco su ventana de entrada.

Es muy facil. Además de la sustitución estándar; sólo es necesario $!N, Py Daquí.


4

Además de Perl, un enfoque general y útil para la edición multilínea de transmisiones (y archivos también) es:

Primero cree un nuevo separador de línea ÚNICO como desee, por ejemplo

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

Luego, en su comando sed (o cualquier otra herramienta), reemplace \ 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

(awk reemplaza el separador de línea ASCII con el suyo y viceversa).


2

Esta es una pequeña modificación de la respuesta inteligente de xara para que funcione en OS X (estoy 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'

En lugar de usar explícitamente \r, tienes que usar $(printf '\r').


1
Si bien printf '\r'(o echo -e '\r') funcionan correctamente, tenga en cuenta que solo puede usar la sintaxis de shell $'\r'para referirse a los literales escapados. Por ejemplo, echo hi$'\n'thererepetirá una nueva línea entre hiy there. Del mismo modo, puede envolver toda la cadena para que cada barra invertida \ escape de su carácter posterior:echo $'hi\nthere'
Dejay Clayton

1

Quería agregar algunas líneas de HTML a un archivo usando sed (y terminé aquí). Normalmente solo usaría perl, pero estaba en una caja que tenía sed, bash y no mucho más. Descubrí que si cambiaba la cadena a una sola línea y dejaba que bash / sed interpolara el \ t \ n todo salió bien:

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

Sería más limpio tener una función para escapar de las comillas dobles y las barras diagonales, pero a veces la abstracción es el ladrón del tiempo.


1

GNU sedtiene una -zopción que permite usar la sintaxis que el OP intentó aplicar. ( página man )

Ejemplo:

$ 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

Tenga en cuenta: si usa ^y $ahora coinciden con el principio y el final de las líneas delimitadas con un carácter NUL (no \n). Y, para garantizar que las coincidencias en todas sus \nlíneas ( separadas) estén sustituidas, no olvide usar el gindicador para las sustituciones globales (por ejemplo s/.../.../g).


Créditos: @ stéphane-chazelas mencionó por primera vez -z en un comentario anterior.


0

Sed rompe la entrada en las nuevas líneas. Mantiene solo una línea por bucle.
Por lo tanto, no hay forma de hacer coincidir una \n(nueva línea) si el espacio del patrón no lo contiene.

Sin embargo, hay una manera de hacer que sed mantenga dos líneas consecutivas en el espacio del patrón utilizando el bucle:

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

Agregue cualquier procesamiento necesario entre la N y la P (reemplazando la l).

En este caso (2 líneas):

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

O, para tres líneas:

$ 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

Eso supone que se reemplace la misma cantidad de líneas.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.