Comment trouver des modèles sur plusieurs lignes en utilisant grep?

208

Je veux trouver les fichiers qui ont "abc" ET "efg" dans cet ordre, et ces deux chaînes sont sur des lignes différentes dans ce fichier. Par exemple: un fichier avec du contenu:

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

Doit être apparié.

Saobi
la source
4
doublon possible de Comment puis-je rechercher un motif multiligne dans un fichier?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

225

Grep n'est pas suffisant pour cette opération.

pcregrep qui se trouve dans la plupart des systèmes Linux modernes peut être utilisé comme

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

-M, --multiline autoriser les motifs à correspondre à plusieurs lignes

Il existe également un nouveau pcre2grep . Les deux sont fournis par le projet PCRE .

pcre2grep est disponible pour Mac OS X via les ports Mac dans le cadre du port pcre2:

% sudo port install pcre2 

et via Homebrew comme:

% brew install pcre

ou pour pcre2

% brew install pcre2

pcre2grep est également disponible sur Linux (Ubuntu 18.04+)

$ sudo apt install pcre2-utils # PCRE2
$ sudo apt install pcregrep    # Older PCRE
porteur de l'anneau
la source
11
@StevenLu -M, --multiline- Autorise les modèles à correspondre à plusieurs lignes.
porteur de l'anneau
7
Notez que. * (\ N |.) * Est équivalent à (\ n |.) * Et que ce dernier est plus court. De plus sur mon système, "pcre_exec () error -8" se produit lorsque j'exécute la version plus longue. Essayez donc 'abc (\ n |.) * Efg' à la place!
daveagp
6
Vous devez rendre l'expression non gourmande dans cet exemple de cas:'abc.*(\n|.)*?efg'
porteur de l'anneau
4
et vous pouvez omettre le premier .*-> 'abc(\n|.)*?efg'pour raccourcir l'expression régulière (et être pédant)
Michi
6
pcregreprend les choses plus faciles, mais grepfonctionnera aussi. Par exemple, voir stackoverflow.com/a/7167115/123695
Michael Mior
113

Je ne sais pas si c'est possible avec grep, mais sed le rend très facile:

sed -e '/abc/,/efg/!d' [file-with-content]
LJ.
la source
4
Cela ne trouve pas les fichiers, il renvoie la partie correspondante à partir d'un seul fichier
shiggity
11
@Lj. s'il vous plaît pouvez-vous expliquer cette commande? Je connais bien sed, mais je n'ai jamais vu une telle expression auparavant.
Anthony
1
@Anthony, c'est documenté dans la page de manuel de sed, sous l'adresse. Il est important de réaliser que / abc / & / efg / est une adresse.
Squidly
49
Je soupçonne que cette réponse aurait été utile si elle avait été un peu plus expliquée, et dans ce cas, je l'aurais votée une fois de plus. Je connais un peu de sed, mais pas assez pour utiliser cette réponse pour produire un code de sortie significatif après une demi-heure de tripotage. Astuce: 'RTFM' obtient rarement des votes positifs sur StackOverflow, comme le montre votre commentaire précédent.
Michael Scheper
25
Explication rapide par exemple: sed '1,5d': supprimer les lignes entre 1 et 5. sed '1,5! D': supprimer les lignes non comprises entre 1 et 5 (c'est-à-dire garder les lignes entre) puis au lieu d'un nombre, vous pouvez rechercher une ligne avec / motif /. Voir aussi le plus simple ci-dessous: sed -n '/ abc /, / efg / p' p est pour l'impression et le drapeau -n n'affiche pas toutes les lignes
phil_w
87

Voici une solution inspirée de cette réponse :

  • si 'abc' et 'efg' peuvent être sur la même ligne:

    grep -zl 'abc.*efg' <your list of files>
  • si 'abc' et 'efg' doivent être sur des lignes différentes:

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

Paramètres:

  • -zTraitez l'entrée comme un ensemble de lignes, chacune se terminant par un octet zéro au lieu d'une nouvelle ligne. c'est-à-dire que grep traite l'entrée comme une seule grande ligne.

  • -l nom d'impression de chaque fichier d'entrée à partir duquel la sortie aurait normalement été imprimée.

  • (?s)activer PCRE_DOTALL, ce qui signifie que '.' trouve n'importe quel caractère ou nouvelle ligne.

atti
la source
@syntaxerror Non, je pense que c'est juste une minuscule l. AFAIK il n'y a pas d' -1option numérique .
Sparhawk
Il semble que vous ayez raison après tout, j'ai peut-être fait une faute de frappe lors des tests. En tout cas désolé d'avoir tracé une fausse piste.
erreur de syntaxe
6
C'est excellent. J'ai juste une question à ce sujet. Si les -zoptions spécifient grep pour traiter les sauts de ligne, zero byte charactersalors pourquoi avons-nous besoin de (?s)dans l'expression régulière? S'il s'agit déjà d'un caractère autre que la nouvelle ligne, ne devrait-il pas .pouvoir le faire correspondre directement?
Durga Swaroop
1
-z (aka --null-data) et (? s) sont exactement ce dont vous avez besoin pour faire correspondre plusieurs lignes avec un grep standard. Utilisateurs de MacOS, veuillez laisser des commentaires sur la disponibilité des options -z ou --null-data sur vos systèmes!
Zeke Fast
4
-z certainement pas disponible sur MacOS
Dylan Nicholson
33

sed devrait suffire comme l'a indiqué LJ ci-dessus,

au lieu de! d, vous pouvez simplement utiliser p pour imprimer:

sed -n '/abc/,/efg/p' file
Kara
la source
16

Je me suis beaucoup appuyé sur pcregrep, mais avec le grep plus récent, vous n'avez pas besoin d'installer pcregrep pour la plupart de ses fonctionnalités. Utilisez simplement grep -P.

Dans l'exemple de la question du PO, je pense que les options suivantes fonctionnent bien, avec la deuxième meilleure correspondance avec la façon dont je comprends la question:

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

J'ai copié le texte sous / tmp / test1 et supprimé le «g» et enregistré sous / tmp / test2. Voici la sortie montrant que la première affiche la chaîne correspondante et la seconde ne montre que le nom de fichier (typique -o est pour montrer la correspondance et typique -l est pour montrer seulement le nom de fichier). Notez que le «z» est nécessaire pour les multilignes et le «(. | \ N)» signifie correspondre à «autre chose que la nouvelle ligne» ou «nouvelle ligne» - c'est-à-dire n'importe quoi:

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

Pour déterminer si votre version est suffisamment nouvelle, exécutez man grepet voyez si quelque chose de similaire apparaît en haut:

   -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.

Cela vient de GNU grep 2.10.

sauge
la source
14

Cela peut être fait facilement en utilisant d'abord trpour remplacer les sauts de ligne par un autre caractère:

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

Ici, j'utilise le caractère d'alarme \a(ASCII 7) à la place d'une nouvelle ligne. Cela ne se trouve presque jamais dans votre texte et greppeut le faire correspondre avec un ., ou le faire correspondre spécifiquement avec \a.

Gavin S. Yancey
la source
1
C'était mon approche mais j'utilisais \0et donc j'avais besoin de faire des grep -aappariements \x00… Vous m'avez aidé à simplifier! echo $log | tr '\n' '\0' | grep -aoE "Error: .*?\x00Installing .*? has failed\!" | tr '\0' '\n'est maintenantecho $log | tr '\n' '\a' | grep -oE "Error: .*?\aInstalling .*? has failed\!" | tr '\a' '\n'
Charlie Gorichanaz
1
Utilisez grep -o.
kyb
7

awk one-liner:

awk '/abc/,/efg/' [file-with-content]
Swynndla
la source
4
Cela imprimera avec plaisir de la abcfin à la fin du fichier si le motif de fin n'est pas présent dans le fichier ou si le dernier motif de fin est manquant. Vous pouvez résoudre ce problème, mais cela compliquera le script de manière assez significative.
tripleee
Comment exclure /efg/de la sortie?
kyb
6

Vous pouvez le faire très facilement si vous pouvez utiliser Perl.

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

Vous pouvez également le faire avec une seule expression régulière, mais cela implique de prendre tout le contenu du fichier dans une seule chaîne, ce qui pourrait finir par occuper trop de mémoire avec des fichiers volumineux. Pour être complet, voici cette méthode:

perl -e '@lines = <>; $content = join("", @lines); print "Found in $ARGV\n" if ($content =~ /abc.*efg/s);' yourfilename.txt
Sundar - Rétablir Monica
la source
La deuxième réponse trouvée a été utile pour extraire un bloc entier de plusieurs lignes avec des correspondances sur quelques lignes - a dû utiliser une correspondance non gourmande ( .*?) pour obtenir une correspondance minimale.
RichVel
5

Je ne sais pas comment je ferais ça avec grep, mais je ferais quelque chose comme ça avec awk:

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

Cependant, vous devez faire attention à la façon dont vous procédez. Voulez-vous que l'expression régulière corresponde à la sous-chaîne ou au mot entier? ajoutez des balises \ w selon le cas. De plus, bien que cela soit strictement conforme à la façon dont vous avez indiqué l'exemple, cela ne fonctionne pas tout à fait lorsque abc apparaît une deuxième fois après efg. Si vous voulez gérer cela, ajoutez un si approprié dans le cas / abc / etc.

Frankc
la source
3

Malheureusement, vous ne pouvez pas. De la grepdocumentation:

grep recherche les FICHIERS d'entrée nommés (ou l'entrée standard si aucun fichier n'est nommé, ou si un seul trait d'union moins (-) est donné comme nom de fichier) pour les lignes contenant une correspondance avec le MOTIF donné.

Kaleb Pederson
la source
Qu'engrep -Pz
Navaro
3

Si vous êtes prêt à utiliser des contextes, cela peut être réalisé en tapant

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

Cela affichera tout entre "abc" et "efg", tant qu'ils sont à moins de 500 lignes les uns des autres.

agouge
la source
3

Si vous avez besoin que les deux mots soient proches l'un de l'autre, par exemple pas plus de 3 lignes, vous pouvez le faire:

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

Même exemple mais filtrant uniquement les fichiers * .txt:

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

Et vous pouvez également remplacer la grepcommande par la egrepcommande si vous souhaitez également rechercher des expressions régulières.

Mariano Ruiz
la source
3

Il y a quelques jours, j'ai publié une alternative à grep qui prend en charge cela directement, soit via une correspondance multiligne ou en utilisant des conditions - j'espère que cela sera utile pour certaines personnes qui recherchent ici. Voici à quoi ressembleraient les commandes de l'exemple:

Multiligne:

sift -lm 'abc.*efg' testfile

Conditions:

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

Vous pouvez également spécifier que 'efg' doit suivre 'abc' sur un certain nombre de lignes:

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

Vous pouvez trouver plus d'informations sur sift-tool.org .

svent
la source
Je ne pense pas que le premier exemple sift -lm 'abc.*efg' testfilefonctionne, car la correspondance est gourmande et engloutit toutes les lignes jusqu'à la dernière efgdu fichier.
Dr.Alex RE
2

Bien que l'option sed soit la plus simple et la plus facile, la doublure monocoque de LJ n'est malheureusement pas la plus portable. Ceux qui sont coincés avec une version du C Shell devront échapper à leur frange:

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

Malheureusement, cela ne fonctionne pas dans bash et al.

punaise
la source
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
ghostdog74
la source
1

vous pouvez utiliser grep au cas où vous ne souhaitez pas dans la séquence du motif.

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

exemple

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

grep -ltrouvera tous les fichiers qui correspondent au premier modèle, et xargs recherchera le deuxième modèle. J'espère que cela t'aides.

Balu Mohan
la source
1
Cela ignorerait l'ordre "pattern1" et "pattern2" apparaissant dans le fichier, cependant - OP spécifie spécifiquement que seuls les fichiers où "pattern2" apparaît APRÈS "pattern1" doivent être mis en correspondance.
Emil Lundberg
1

Avec chercheur d'argent :

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

similaire à la réponse du porteur de l'anneau, mais avec ag ​​à la place. Les avantages de vitesse du chercheur d'argent pourraient éventuellement briller ici.

Shwaydogg
la source
1
Ça n'a pas l'air de fonctionner. (echo abctest; echo efg)|ag 'abc.*(\n|.)*efg'ne correspond pas
phiresky
1

J'ai utilisé cela pour extraire une séquence fasta d'un fichier multi fasta en utilisant l'option -P pour grep:

grep -Pzo ">tig00000034[^>]+"  file.fasta > desired_sequence.fasta
  • P pour les recherches basées sur perl
  • z pour faire une fin de ligne sur 0 octet plutôt que le caractère de nouvelle ligne
  • o pour capturer simplement ce qui correspond puisque grep renvoie la ligne entière (ce qui dans ce cas puisque vous avez fait -z est le fichier entier).

Le noyau de l'expression rationnelle est le [^>]qui se traduit par "pas plus grand que le symbole"

Jon Boyle
la source
0

Comme alternative à la réponse de Balu Mohan, il est possible d'imposer l'ordre des modèles en utilisant uniquement grep, headet 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

Celui-ci n'est cependant pas très joli. Format plus lisible:

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

Cela affichera les noms de tous les fichiers où "pattern2"apparaît après "pattern1", ou où les deux apparaissent sur la même ligne :

$ 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

Explication

  • tail -n +i- imprimer toutes les lignes après le ie, inclus
  • grep -n - ajouter des lignes correspondantes avec leurs numéros de ligne
  • head -n1 - imprimer uniquement la première ligne
  • cut -d : -f 1- imprimer la première colonne coupée en utilisant :comme délimiteur
  • 2>/dev/null- tailsortie d'erreur de silence qui se produit si l' $()expression retourne vide
  • grep -q- taire grepet retourner immédiatement si une correspondance est trouvée, car nous ne sommes intéressés que par le code de sortie
Emil Lundberg
la source
Quelqu'un peut-il expliquer cela &>? Je l'utilise aussi, mais je ne l'ai jamais vu documenté nulle part. BTW, pourquoi devons-nous faire taire grep de cette façon, en fait? grep -qne fera pas l'affaire aussi?
syntaxerror
1
&>indique à bash de rediriger la sortie standard et l'erreur standard, voir REDIRECTION dans le manuel bash. Vous avez tout à fait raison en ce que nous pourrions tout aussi bien faire grep -q ...au lieu de grep ... &>/dev/null, bonne prise!
Emil Lundberg
Je le pensais. Enlèvera la douleur de beaucoup de frappe supplémentaire maladroite. Merci pour l'explication - j'ai donc dû sauter un peu dans le manuel. (Vous avez recherché quelque chose de connexe à distance il y a quelque temps.) --- Vous pourriez même envisager de le changer dans votre réponse. :)
syntaxerror
0

Cela devrait aussi marcher?!

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

$ARGVcontient le nom du fichier en cours lors de la lecture des file_list /srecherches de modificateurs sur la nouvelle ligne.

PS12
la source
0

Le modèle de fichier *.shest important pour empêcher l'inspection des répertoires. Bien sûr, un test pourrait également empêcher cela.

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

le

grep -n -m1 abc $f 

recherche au maximum 1 correspondance et renvoie (-n) le numéro de lin. Si une correspondance a été trouvée (test -n ...) trouver la dernière correspondance de efg (trouver tout et prendre la dernière avec queue -n 1).

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

sinon continuer.

Puisque le résultat est quelque chose comme 18:foofile.sh String alf="abc";nous devons couper de ":" jusqu'à la fin de la ligne.

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

Devrait retourner un résultat positif si la dernière correspondance de la 2e expression est passée la première correspondance de la première.

Ensuite, nous rapportons le nom de fichier echo $f.

Utilisateur inconnu
la source
0

Pourquoi pas quelque chose de simple comme:

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

renvoie 0 ou un entier positif.

egrep -o (affiche uniquement les correspondances, astuce: plusieurs correspondances sur la même ligne produisent une sortie multiligne comme si elles se trouvaient sur des lignes différentes)

  • grep -A1 abc (imprimer abc et la ligne après)

  • grep efg | wc -l (Nombre 0-n de lignes efg trouvées après abc sur la même ligne ou sur les lignes suivantes, le résultat peut être utilisé dans un "si")

  • grep peut être changé en egrep etc. si une correspondance de modèle est nécessaire

kevins
la source
0

Si vous avez une estimation de la distance entre les 2 chaînes 'abc' et 'efg' que vous recherchez, vous pouvez utiliser:

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

De cette façon, le premier grep retournera la ligne avec les lignes 'abc' plus # num1 après, et # num2 lignes après, et le second grep passera en revue toutes ces lignes pour obtenir le 'efg'. Vous saurez alors dans quels fichiers ils apparaissent ensemble.

Benjamin Berend
la source
0

Avec ugrep sorti il ​​y a quelques mois:

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

Cet outil est hautement optimisé pour la vitesse. Il est également compatible GNU / BSD / PCRE-grep.

Notez que nous devons utiliser une répétition paresseuse +?, sauf si vous voulez faire correspondre toutes les lignes avec efgjusqu'à la dernière efgdu fichier.

Dr. Alex RE
la source
-3

Cela devrait fonctionner:

cat FILE | egrep 'abc|efg'

S'il y a plus d'une correspondance, vous pouvez filtrer en utilisant grep -v

Gourou
la source
2
Bien que cet extrait de code soit le bienvenu et puisse fournir de l'aide, il serait grandement amélioré s'il incluait une explication sur la manière et la raison pour laquelle cela résout le problème. N'oubliez pas que vous répondrez à la question des lecteurs à l'avenir, pas seulement à la personne qui pose la question maintenant! Veuillez modifier votre réponse pour ajouter des explications et donner une indication des limitations et hypothèses applicables.
Toby Speight
1
Cela ne recherche pas réellement sur plusieurs lignes , comme indiqué dans la question.
n.st