¿Cómo encontrar patrones en varias líneas usando grep?


208

Quiero encontrar archivos que tengan "abc" Y "efg" en ese orden, y esas dos cadenas están en líneas diferentes en ese archivo. Por ejemplo: un archivo con contenido:

blah blah..
blah blah..
blah abc blah
blah blah..
blah blah..
blah blah..
blah efg blah blah
blah blah..
blah blah..

Debería coincidir.


Respuestas:


225

Grep no es suficiente para esta operación.

pcregrep, que se encuentra en la mayoría de los sistemas Linux modernos, puede usarse como

pcregrep -M  'abc.*(\n|.)*efg' test.txt

donde -M, --multiline permitir que los patrones coincidan con más de una línea

También hay un pcre2grep más nuevo . Ambos son proporcionados por el proyecto PCRE .

pcre2grep está disponible para Mac OS X a través de puertos Mac como parte del puerto pcre2:

% sudo port install pcre2 

y vía Homebrew como:

% brew install pcre

o para pcre2

% brew install pcre2

pcre2grep también está disponible en Linux (Ubuntu 18.04+)

$ sudo apt install pcre2-utils # PCRE2
$ sudo apt install pcregrep    # Older PCRE

11
@StevenLu -M, --multiline: permite que los patrones coincidan con más de una línea.
portador del anillo

77
Tenga en cuenta que. * (\ N |.) * Es equivalente a (\ n |.) * Y este último es más corto. Además, en mi sistema, el "error pcre_exec () -8" ocurre cuando ejecuto la versión más larga. ¡Entonces intente 'abc (\ n |.) * Efg' en su lugar!
daveagp

66
Debe hacer que la expresión no sea codiciosa en ese ejemplo de caso:'abc.*(\n|.)*?efg'
portador del anillo el

44
y puede omitir el primero .*-> 'abc(\n|.)*?efg'para acortar la expresión regular (y ser pedante)
Michi

66
pcregrepfacilita las cosas, pero greptambién funcionará. Por ejemplo, consulte stackoverflow.com/a/7167115/123695
Michael Mior el

113

No estoy seguro de si es posible con grep, pero sed lo hace muy fácil:

sed -e '/abc/,/efg/!d' [file-with-content]

44
Esto no encuentra archivos, devuelve la parte correspondiente de un solo archivo
shiggity

11
@Lj. por favor, ¿puedes explicar este comando? Estoy familiarizado sed, pero si nunca antes había visto una expresión así.
Anthony

1
@ Anthony, está documentado en la página de manual de sed, bajo dirección. Es importante darse cuenta de que / abc / & / efg / es una dirección.
Calamar

49
Sospecho que esta respuesta habría sido útil si tuviera un poco más de explicación, y en ese caso, la habría votado una vez más. Sé un poco de sed, pero no lo suficiente como para usar esta respuesta para producir un código de salida significativo después de media hora de tocar el violín. Consejo: 'RTFM' rara vez recibe votos positivos en StackOverflow, como muestra su comentario anterior.
Michael Scheper

25
Explicación rápida con el ejemplo: sed '1,5d': borra las líneas entre 1 y 5. sed '1,5! D': borra las líneas que no estén entre 1 y 5 (es decir, mantén las líneas entre ellas) y luego, en lugar de un número, puedes busca una línea con / patrón /. Vea también el más simple a continuación: sed -n '/ abc /, / efg / p' p es para imprimir y la bandera -n no muestra todas las líneas
phil_w

87

Aquí hay una solución inspirada en esta respuesta :

  • si 'abc' y 'efg' pueden estar en la misma línea:

    grep -zl 'abc.*efg' <your list of files>
  • si 'abc' y 'efg' deben estar en diferentes líneas:

    grep -Pzl '(?s)abc.*\n.*efg' <your list of files>

Parámetros:

  • -zTrate la entrada como un conjunto de líneas, cada una terminada por un byte cero en lugar de una nueva línea. es decir, grep trata la entrada como una línea grande.

  • -l imprimir el nombre de cada archivo de entrada desde el que normalmente se habría impreso la salida.

  • (?s)active PCRE_DOTALL, lo que significa que '.' encuentra cualquier personaje o nueva línea.


@syntaxerror No, creo que es solo una minúscula l. AFAIK no hay -1opción de número .
Sparhawk

Parece que tienes razón después de todo, tal vez cometí un error al probar. En cualquier caso, perdón por dejar un rastro falso.
syntaxerror

66
Esto es excelente. Solo tengo una pregunta con respecto a esto. Si las -zopciones especifican grep para tratar las nuevas líneas como zero byte charactersentonces, ¿por qué necesitamos (?s)la expresión regular? Si ya es un personaje que no .es de nueva línea, ¿no debería poder emparejarlo directamente?
Durga Swaroop

1
-z (también conocido como --null-data) y (? s) son exactamente lo que necesita para combinar varias líneas con un grep estándar. ¡Gente en MacOS, deje comentarios sobre la disponibilidad de las opciones -z o --null-data en sus sistemas!
Zeke Fast

44
-z definitivamente no está disponible en MacOS
Dylan Nicholson

33

sed debería ser suficiente como el póster LJ mencionado anteriormente,

en lugar de! d simplemente puede usar p para imprimir:

sed -n '/abc/,/efg/p' file

16

Confié mucho en pcregrep, pero con grep más nuevo no necesita instalar pcregrep para muchas de sus características. Solo usagrep -P .

En el ejemplo de la pregunta del OP, creo que las siguientes opciones funcionan bien, con la segunda mejor coincidencia de cómo entiendo la pregunta:

grep -Pzo "abc(.|\n)*efg" /tmp/tes*
grep -Pzl "abc(.|\n)*efg" /tmp/tes*

Copié el texto como / tmp / test1 y eliminé la 'g' y lo guardé como / tmp / test2. Aquí está el resultado que muestra que el primero muestra la cadena coincidente y el segundo muestra solo el nombre del archivo (típico -o es para mostrar coincidencia y típico -l es para mostrar solo el nombre del archivo). Tenga en cuenta que la 'z' es necesaria para multilínea y '(. | \ N)' significa que coincide con 'cualquier cosa que no sea nueva línea' o 'nueva línea', es decir, cualquier cosa:

user@host:~$ grep -Pzo "abc(.|\n)*efg" /tmp/tes*
/tmp/test1:abc blah
blah blah..
blah blah..
blah blah..
blah efg
user@host:~$ grep -Pzl "abc(.|\n)*efg" /tmp/tes*
/tmp/test1

Para determinar si su versión es lo suficientemente nueva, ejecute man grepy vea si algo similar a esto aparece cerca de la parte superior:

   -P, --perl-regexp
          Interpret  PATTERN  as a Perl regular expression (PCRE, see
          below).  This is highly experimental and grep -P may warn of
          unimplemented features.

Eso es de GNU grep 2.10.


14

Esto se puede hacer fácilmente usando primero trpara reemplazar las nuevas líneas con algún otro carácter:

tr '\n' '\a' | grep -o 'abc.*def' | tr '\a' '\n'

Aquí, estoy usando el carácter de alarma \a(ASCII 7) en lugar de una nueva línea. Esto casi nunca se encuentra en su texto, y greppuede coincidir con .o específicamente con \a.


1
Este era mi enfoque, pero lo estaba usando \0y, por lo tanto, necesitaba grep -ay coincidía con \x00... ¡Me ha ayudado a simplificar! echo $log | tr '\n' '\0' | grep -aoE "Error: .*?\x00Installing .*? has failed\!" | tr '\0' '\n'es ahoraecho $log | tr '\n' '\a' | grep -oE "Error: .*?\aInstalling .*? has failed\!" | tr '\a' '\n'
Charlie Gorichanaz

1
Uso grep -o.
kyb

7

awk one-liner:

awk '/abc/,/efg/' [file-with-content]

44
Esto se imprimirá felizmente abchasta el final del archivo si el patrón final no está presente en el archivo o si falta el último patrón final. Puede solucionarlo, pero complicará la secuencia de comandos de manera bastante significativa.
tripleee

¿Cómo excluir /efg/de la salida?
kyb

6

Puede hacerlo muy fácilmente si puede usar Perl.

perl -ne 'if (/abc/) { $abc = 1; next }; print "Found in $ARGV\n" if ($abc && /efg/); }' yourfilename.txt

También puede hacerlo con una sola expresión regular, pero eso implica tomar todo el contenido del archivo en una sola cadena, lo que podría terminar ocupando demasiada memoria con archivos grandes. Para completar, aquí está ese método:

perl -e '@lines = <>; $content = join("", @lines); print "Found in $ARGV\n" if ($content =~ /abc.*efg/s);' yourfilename.txt

La segunda respuesta encontrada fue útil para extraer un bloque completo de varias líneas con coincidencias en un par de líneas: tuvo que usar una coincidencia no codiciosa ( .*?) para obtener una coincidencia mínima.
RichVel

5

No sé cómo haría eso con grep, pero haría algo así con awk:

awk '/abc/{ln1=NR} /efg/{ln2=NR} END{if(ln1 && ln2 && ln1 < ln2){print "found"}else{print "not found"}}' foo

Sin embargo, debes tener cuidado de cómo lo haces. ¿Desea que la expresión regular coincida con la subcadena o la palabra completa? agregue \ w etiquetas según corresponda. Además, si bien esto se ajusta estrictamente a la forma en que mencionó el ejemplo, no funciona cuando abc aparece por segunda vez después de efg. Si desea manejar eso, agregue un if según corresponda en el / abc / case, etc.


3

Lamentablemente, no puedes. De los grepdocumentos:

grep busca en los ARCHIVOS de entrada con nombre (o en la entrada estándar si no se nombra ningún archivo, o si se da un solo guión menos (-) como nombre de archivo) en busca de líneas que contengan una coincidencia con el PATRÓN dado.


¿Qué pasa?grep -Pz
Navaro

3

Si está dispuesto a usar contextos, esto podría lograrse escribiendo

grep -A 500 abc test.txt | grep -B 500 efg

Esto mostrará todo entre "abc" y "efg", siempre que estén dentro de 500 líneas entre sí.


3

Si necesita que ambas palabras estén cercanas entre sí, por ejemplo, no más de 3 líneas, puede hacer esto:

find . -exec grep -Hn -C 3 "abc" {} \; | grep -C 3 "efg"

El mismo ejemplo pero solo filtrando archivos * .txt:

find . -name *.txt -exec grep -Hn -C 3 "abc" {} \; | grep -C 3 "efg"

Y también puede reemplazar el grepcomando con el egrepcomando si también desea encontrar con expresiones regulares.


3

Lancé una alternativa grep hace unos días que admite esto directamente, ya sea a través de la coincidencia multilínea o usando condiciones; espero que sea útil para algunas personas que buscan aquí. Así es como se verían los comandos para el ejemplo:

Multilínea:

sift -lm 'abc.*efg' testfile

Condiciones:

sift -l 'abc' testfile --followed-by 'efg'

También podría especificar que 'efg' debe seguir a 'abc' dentro de un cierto número de líneas:

sift -l 'abc' testfile --followed-within 5:'efg'

Puede encontrar más información en sift-tool.org .


No creo que el primer ejemplo sift -lm 'abc.*efg' testfilefuncione, porque la coincidencia es codiciosa y engulle todas las líneas hasta la última efgen el archivo.
Dr. Alex RE

2

Si bien la opción sed es la más simple y fácil, la única frase de LJ no es la más portátil. Aquellos atrapados con una versión de C Shell deberán escapar de su explosión:

sed -e '/abc/,/efg/\!d' [file]

Esto desafortunadamente no funciona en bash et al.


1
#!/bin/bash
shopt -s nullglob
for file in *
do
 r=$(awk '/abc/{f=1}/efg/{g=1;exit}END{print g&&f ?1:0}' file)
 if [ "$r" -eq 1 ];then
   echo "Found pattern in $file"
 else
   echo "not found"
 fi
done

1

puede usar grep en caso de que no le guste la secuencia del patrón.

grep -l "pattern1" filepattern*.* | xargs grep "pattern2"

ejemplo

grep -l "vector" *.cpp | xargs grep "map"

grep -lencontrará todos los archivos que coincidan con el primer patrón, y xargs buscará el segundo patrón. Espero que esto ayude.


1
Sin embargo, eso ignoraría el orden "patrón1" y "patrón2" que aparecen en el archivo: OP especifica específicamente que solo los archivos donde "patrón2" aparece DESPUÉS de que "patrón1" deben coincidir.
Emil Lundberg

1

Con buscador de plata :

ag 'abc.*(\n|.)*efg'

similar a la respuesta del portador del anillo, pero con ag en su lugar. Las ventajas de velocidad del buscador de plata posiblemente podrían brillar aquí.


1
Parece que esto no funciona. (echo abctest; echo efg)|ag 'abc.*(\n|.)*efg'no coincide
phiresky

1

Utilicé esto para extraer una secuencia fasta de un archivo multi fasta usando la opción -P para grep:

grep -Pzo ">tig00000034[^>]+"  file.fasta > desired_sequence.fasta
  • P para búsquedas basadas en perl
  • z para hacer que una línea finalice en 0 bytes en lugar de una nueva línea char
  • o simplemente capturar lo que coincide, ya que grep devuelve la línea completa (que en este caso desde que lo hizo -z es todo el archivo).

El núcleo de la expresión regular es el [^>]que se traduce como "no mayor que el símbolo"


0

Como alternativa a la respuesta de Balu Mohan, es posible hacer cumplir la orden de los patrones usando solamente grep, heady tail:

for f in FILEGLOB; do tail $f -n +$(grep -n "pattern1" $f | head -n1 | cut -d : -f 1) 2>/dev/null | grep "pattern2" &>/dev/null && echo $f; done

Sin embargo, este no es muy bonito. Formateado de forma más legible:

for f in FILEGLOB; do
    tail $f -n +$(grep -n "pattern1" $f | head -n1 | cut -d : -f 1) 2>/dev/null \
    | grep -q "pattern2" \
    && echo $f
done

Esto imprimirá los nombres de todos los archivos donde "pattern2"aparece después "pattern1", o donde ambos aparecen en la misma línea :

$ echo "abc
def" > a.txt
$ echo "def
abc" > b.txt
$ echo "abcdef" > c.txt; echo "defabc" > d.txt
$ for f in *.txt; do tail $f -n +$(grep -n "abc" $f | head -n1 | cut -d : -f 1) 2>/dev/null | grep -q "def" && echo $f; done
a.txt
c.txt
d.txt

Explicación

  • tail -n +i - imprime todas las líneas después de i th, inclusive
  • grep -n - anteponer líneas coincidentes con sus números de línea
  • head -n1 - imprime solo la primera fila
  • cut -d : -f 1 - imprima la primera columna cortada usando : como delimitador
  • 2>/dev/null- tailsalida de error de silencio que ocurre si el$() expresión vuelve vacía
  • grep -q- Silencio grepy regreso inmediatamente si se encuentra una coincidencia, ya que solo estamos interesados ​​en el código de salida

¿Alguien puede explicar por favor &>? También lo estoy usando, pero nunca lo vi documentado en ningún lado. Por cierto, ¿por qué tenemos que silenciar grep de esa manera, en realidad? grep -qno hará el truco también?
syntaxerror

1
&>le dice a bash que redirija tanto la salida estándar como el error estándar, vea REDIRECCIÓN en el manual de bash. Tienes mucha razón en que podríamos hacerlo en grep -q ...lugar de grep ... &>/dev/null, ¡buena captura!
Emil Lundberg

Pensado así. Eliminará el dolor de muchos tipos extra de escritura incómoda. Gracias por la explicación, así que debo haberme saltado un poco en el manual. (Busqué algo relacionado remotamente en él hace algún tiempo.) --- Incluso podría considerar cambiarlo en su respuesta. :)
syntaxerror

0

¿Esto también debería funcionar?

perl -lpne 'print $ARGV if /abc.*?efg/s' file_list

$ARGVcontiene el nombre del archivo actual cuando se leen file_list /sbúsquedas de modificadores en la nueva línea.


0

El patrón de archivos *.shes importante para evitar que los directorios sean inspeccionados. Por supuesto, algunas pruebas también podrían evitarlo.

for f in *.sh
do
  a=$( grep -n -m1 abc $f )
  test -n "${a}" && z=$( grep -n efg $f | tail -n 1) || continue 
  (( ((${z/:*/}-${a/:*/})) > 0 )) && echo $f
done

los

grep -n -m1 abc $f 

busca un máximo de 1 coincidencia y devuelve (-n) el número de lino. Si se encontró una coincidencia (prueba -n ...) encuentre la última coincidencia de efg (encuentre todo y tome la última con la cola -n 1).

z=$( grep -n efg $f | tail -n 1)

De lo contrario continuar.

Como el resultado es algo así 18:foofile.sh String alf="abc";, necesitamos cortar ":" hasta el final de la línea.

((${z/:*/}-${a/:*/}))

Debería devolver un resultado positivo si la última coincidencia de la segunda expresión ha pasado la primera coincidencia de la primera.

Luego informamos el nombre del archivo echo $f.


0

¿Por qué no algo tan simple como:

egrep -o 'abc|efg' $file | grep -A1 abc | grep efg | wc -l

devuelve 0 o un entero positivo.

egrep -o (solo muestra coincidencias, truco: varias coincidencias en la misma línea producen una salida de varias líneas como si estuvieran en líneas diferentes)

  • grep -A1 abc (imprima abc y la línea que sigue)

  • grep efg | wc -l (0-n recuento de líneas efg encontradas después de abc en la misma línea o en las siguientes, el resultado puede usarse en un 'si')

  • grep se puede cambiar a egrep, etc. si se necesita la coincidencia de patrones


0

Si tiene alguna estimación acerca de la distancia entre las 2 cadenas 'abc' y 'efg' que está buscando, puede usar:

grep -r . -e 'abc' -A num1 -B num2 | grep 'efg'

De esa manera, el primer grep devolverá la línea con el 'abc' más # num1 líneas después de él, y # num2 líneas después, y el segundo grep tamizará todos esos para obtener el 'efg'. Entonces sabrás en qué archivos aparecen juntos.


0

Con ugrep lanzado hace unos meses:

ugrep 'abc(\n|.)+?efg'

Esta herramienta está altamente optimizada para la velocidad. También es compatible con GNU / BSD / PCRE-grep.

Tenga en cuenta que deberíamos usar una repetición perezosa +?, a menos que desee hacer coincidir todas las líneas efgjuntas hasta la última efgen el archivo.


-3

Esto debería funcionar:

cat FILE | egrep 'abc|efg'

Si hay más de una coincidencia, puede filtrar usando grep -v


2
Si bien este fragmento de código es bienvenido y puede proporcionar algo de ayuda, mejoraría enormemente si incluyera una explicación de cómo y por qué esto resuelve el problema. ¡Recuerde que está respondiendo la pregunta para los lectores en el futuro, no solo la persona que pregunta ahora! Por favor, editar su respuesta para agregar explicación y dar una indicación de lo que se aplican limitaciones y supuestos.
Toby Speight

1
En realidad, eso no busca en varias líneas , como se indica en la pregunta.
n.st
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.