¿Cómo obtener múltiples líneas de un archivo por una expresión regular?


10

¿Cómo obtener múltiples líneas de un archivo por una expresión regular?

A menudo me gustaría obtener varias líneas / modificar varias líneas por una expresión regular. Un caso de ejemplo:

Estoy tratando de leer parte de un archivo XML / SGML (no están necesariamente bien formados o tienen una sintaxis predecible, por lo que una expresión regular sería más segura que un analizador adecuado. Además, me gustaría poder hacer esto también completamente archivos no estructurados donde solo se conocen algunas palabras clave.) en un script de shell (que se ejecuta en Solaris y Linux).

XML de ejemplo:

<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>

De esto me gustaría leer <tag1>si contiene fooalgún lugar dentro de él.

Un regex like (<tag1>.*?foo.*?</tag1>)debería dar la parte correcta, pero las herramientas me gustan grepy sedsolo funcionan para mí con líneas simples. Como puedo conseguir

<tag1>
 <tag2>foo</tag2>
</tag1>

¿en este ejemplo?



@evilsoup Eso es cierto, pero mi pregunta no es específicamente sobre archivos XML / SGML, sino sobre cualquier archivo de texto.
Den

Respuestas:


7

Si tiene instalado GNU grep, puede hacer una búsqueda multilínea pasando el -Pindicador (perl-regex) y activándolo PCRE_DOTALLcon(?s)

grep -oP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
<tag1>
<tag2>foo</tag2>
</tag1>

Si lo anterior no funciona en su plataforma, intente pasar la -zbandera además, esto obliga a grep a tratar NUL como separador de línea, haciendo que todo el archivo se vea como una sola línea.

grep -ozP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt

Esto no da salida en mi sistema cuando se ejecuta en el archivo de ejemplo del OP.
terdon

Funciona para mi. +1. Gracias por el (?s)consejo
Nathan Wallace

@terdon, ¿qué versión de GNU grep estás ejecutando?
iruvar

@ 1_CR (GNU grep) 2.14en Debian. Copié el ejemplo de OP como está (agregando solo la nueva línea final) y ejecuté su greppero no obtuve resultados.
terdon

1
@slm, estoy en pcre 6.6, GNU grep 2.5.1 en RHEL. ¿Te importa intentarlo en grep -ozPlugar de grep -oPen tus plataformas?
iruvar

3
#begin command block
#append all lines between two addresses to hold space 
    sed -n -f - <<\SCRIPT file.xml
        \|<tag1>|,\|</tag1>|{ H 
#at last line of search block exchange hold and pattern space 
            \|</tag1>|{ x
#if not conditional ;  clear buffer ; branch to script end
                \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
#do work ; print result; clear buffer ; close blocks
    s?*?*?;p;s/.*//;h;b}}
SCRIPT

Si hace lo anterior, dados los datos que muestra, antes de la última línea de limpieza allí, debería estar trabajando con un sedespacio de patrón que se vea así:

 ^\n<tag1>\n<tag2>foo</tag2>\n</tag1>$

Puede imprimir su espacio de patrón cuando lo desee con look. Luego puede abordar los \ncaracteres.

sed l <file

Le mostrará que cada línea la sedprocesa en la etapa en la que lse llama.

Así que lo acabo de probar y necesitaba uno más \backslashdespués ,commade la primera línea, pero por lo demás funciona como está. Aquí lo puse en un _sed_functionpara que pueda llamarlo fácilmente con fines de demostración a lo largo de esta respuesta: (funciona con comentarios incluidos, pero aquí se eliminan por razones de brevedad)

_sed_function() { sed -n -f /dev/fd/3 
} 3<<\SCRIPT <<\FILE 
    \|<tag1>|,\|</tag1>|{ H
        \|</tag1>|{ x
            \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
    s?*?*?;p;s/.*//;h;b}}
#END
SCRIPT
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
FILE


_sed_function
#OUTPUT#
<tag1>
 <tag2>foo</tag2>
</tag1>

Ahora cambiaremos el ppor un lpara que podamos ver con qué estamos trabajando mientras desarrollamos nuestro script y eliminamos la demostración no operativa s?para que la última línea de nuestro sed 3<<\SCRIPTsimplemente se vea así:

l;s/.*//;h;b}}

Luego lo volveré a ejecutar:

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

¡Okay! Así que tenía razón, es un buen sentimiento. Ahora, barajemos nuestro look para ver las líneas que tira pero elimina. Eliminaremos nuestra actual ly agregaremos una para !{block}que se vea así:

!{l;s/.*//;h;b}

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$

Eso es lo que parece justo antes de que lo eliminemos.

Una última cosa que quiero mostrarles es el Hviejo espacio a medida que lo construimos. Hay un par de conceptos clave que espero poder demostrar. Así que elimino el último look nuevamente y modifico la primera línea para agregar un vistazo al Hespacio anterior al final:

{ H ; x ; l ; x

_sed_function
#OUTPUT#
\n<tag1>$
\n<tag1>\n <tag2>bar</tag2>$
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$
\n<tag1>$
\n<tag1>\n <tag2>foo</tag2>$
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

Hel espacio antiguo sobrevive a los ciclos de línea, de ahí el nombre. Entonces, lo que la gente a menudo tropieza, está bien, lo que a menudo tropiezo es que necesita eliminarse después de usarlo. En este caso, solo xcambio e una vez, por lo que el espacio de espera se convierte en el espacio del patrón y viceversa, y este cambio también sobrevive a los ciclos de línea.

El efecto es que necesito eliminar mi espacio de espera que solía ser mi espacio de patrón. Hago esto limpiando primero el espacio de patrón actual con:

s/.*//

Que simplemente selecciona cada personaje y lo elimina. No puedo usarlo dporque esto terminaría mi ciclo de línea actual y el siguiente comando no se completaría, lo que prácticamente destruiría mi script.

h

Esto funciona de manera similar, Hpero sobrescribe el espacio de retención, por lo que acabo de copiar mi espacio de patrón en blanco sobre la parte superior de mi espacio de retención, eliminándolo efectivamente. Ahora solo puedo:

b

fuera.

Y así es como escribo sedguiones.


Gracias @slm! Eres un tipo realmente bueno, ¿lo sabes?
mikeserv

Gracias, buen trabajo, ascenso muy rápido a 3k, luego 5k 8-)
slm

No sé, @slm. Estoy empezando a ver que estoy aprendiendo cada vez menos, tal vez he superado su utilidad. Tengo que pensarlo. Apenas he llegado al sitio las últimas dos semanas.
mikeserv

Al menos llegar a 10k. Todo lo que vale la pena desbloquear está en ese nivel. Sigue ahorrando, 5k vendrá bastante rápido ahora.
slm

1
Bueno, @slm, de todos modos eres una raza rara. Sin embargo, estoy de acuerdo con las respuestas múltiples. Por eso me molesta cuando se cierran algunas preguntas. Pero eso rara vez sucede, en realidad. Gracias de nuevo, slm.
mikeserv

2

La respuesta de @jamespfinn funcionará perfectamente bien si su archivo es tan simple como su ejemplo. Si tiene una situación más compleja donde <tag1>podría abarcar más de 2 líneas, necesitará un truco un poco más complejo. Por ejemplo:

$ cat foo.xml
<tag1>
 <tag2>bar</tag2>
 <tag3>baz</tag3>
</tag1>
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
$ perl -ne 'if(/<tag1>/){$a=1;} 
            if($a==1){push @l,$_}
            if(/<\/tag1>/){
              if(grep {/foo/} @l){print "@l";}
               $a=0; @l=()
            }' foo.xml
<tag1>

  <tag2>foo</tag2>
 </tag1>
<tag1>
  <tag2>bar</tag2>

  <tag2>foo</tag2>
  <tag3>baz</tag3>
 </tag1>

El script perl procesará cada línea de su archivo de entrada y

  • if(/<tag1>/){$a=1;}: la variable $ase establece en 1si se encuentra una etiqueta de apertura ( <tag1>).

  • if($a==1){push @l,$_}: para cada línea, si $aes así 1, agregue esa línea a la matriz @l.

  • if(/<\/tag1>/) : si la línea actual coincide con la etiqueta de cierre:

    • if(grep {/foo/} @l){print "@l"}: si alguna de las líneas guardadas en la matriz @l(estas son las líneas entre <tag1>y </tag1>) coincide con la cadena foo, imprima el contenido de @l.
    • $a=0; @l=(): vaciar la lista ( @l=()) y $avolver a establecer en 0.

Esto funciona bien, excepto en el caso de que haya más de una <tag1> que contenga "foo". En ese caso, imprime todo desde el comienzo de la primera <tag1> hasta el final de la última </tag1> ...
Den

@den Lo probé con el ejemplo que se muestra en mi respuesta que contiene 3 <tag1>con fooy funciona bien. ¿Cuándo falla para ti?
terdon

se siente tan mal analizar xml usando regex :)
Braiam

1

Aquí hay una sedalternativa:

sed -n '/<tag1/{:x N;/<\/tag1/!b x};/foo/p' your_file

Explicación

  • -n significa no imprimir líneas a menos que se le indique.
  • /<tag1/ primero coincide con la etiqueta de apertura
  • :x es una etiqueta para permitir saltar a este punto más tarde
  • N agrega la siguiente línea al espacio del patrón (búfer activo).
  • /<\/tag1/!b xsignifica que si el espacio del patrón actual no contiene una etiqueta de cierre, bifurca a la xetiqueta creada anteriormente. Por lo tanto, seguimos agregando líneas al espacio del patrón hasta que encontramos nuestra etiqueta de cierre.
  • /foo/psignifica que si el espacio del patrón actual coincide foo, debe imprimirse.

1

Creo que podría hacerlo con GNU awk, tratando la etiqueta final como un separador de registros, por ejemplo, para una etiqueta final conocida </tag1>:

gawk -vRS="\n</tag1>\n" '/foo/ {printf "%s%s", $0, RT}'

o más generalmente (con una expresión regular para la etiqueta final)

gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}'

Probándolo en @ terdon foo.xml:

$ gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}' foo.xml
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>

0

Si su archivo está estructurado exactamente como se muestra arriba, puede utilizar los indicadores -A (líneas después) y -B (líneas antes) para grep ... por ejemplo:

$ cat yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
$ grep -A1 -B1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
$ grep -A1 -B1 foo yourFile.txt 
<tag1>
 <tag2>foo</tag2>
</tag1>

Si su versión de lo grepadmite, también puede usar la -Copción más simple (para el contexto) que imprime las N líneas circundantes:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>

Gracias, pero no. Este es solo un ejemplo y las cosas reales parecen bastante impredecibles ;-)
Den

1
Eso no es encontrar una etiqueta con foo, es solo encontrar foo y mostrar líneas de contexto
Nathan Wallace

@NathanWallace sí, que es exactamente lo que pedía el OP, esta respuesta funciona perfectamente en el caso dado en la pregunta.
terdon

@terdon eso no es para nada lo que hace la pregunta. Cita: "Me gustaría leer el <tag1> si contiene foo en algún lugar dentro de él". Esta solución es como "Me gustaría leer 'foo' y 1 línea de contexto, independientemente de dónde aparezca 'foo'". Siguiendo su lógica, una respuesta igualmente válida a esta pregunta sería tail -3 input_file.xml. Sí, funciona para este ejemplo específico, pero no es una respuesta útil a la pregunta.
Nathan Wallace

@NathanWallace, mi punto fue que el OP declaró específicamente que este no es un formato XML válido, en ese caso, podría haber sido suficiente para imprimir las N líneas alrededor de la cadena que el OP está buscando. Con la información disponible, esta respuesta fue lo suficientemente decente.
terdon
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.