Wie finde ich mit grep Muster über mehrere Zeilen hinweg?

208

Ich möchte Dateien finden, die "abc" UND "efg" in dieser Reihenfolge haben, und diese beiden Zeichenfolgen befinden sich in dieser Datei in unterschiedlichen Zeilen. ZB: eine Datei mit Inhalt:

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

Sollte abgestimmt sein.

Saobi
quelle
4
Mögliches Duplikat von Wie kann ich in einer Datei nach einem mehrzeiligen Muster suchen?
Ciro Santilli 法轮功 冠状 病 六四 事件 15

Antworten:

225

Grep reicht für diesen Vorgang nicht aus.

pcregrep, das in den meisten modernen Linux-Systemen zu finden ist, kann als verwendet werden

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

wo -M, --multiline erlauben Muster mehr als eine Zeile passen

Es gibt auch einen neueren pcre2grep . Beide werden vom PCRE-Projekt bereitgestellt .

pcre2grep ist für Mac OS X über Mac-Ports als Teil des Ports verfügbar pcre2:

% sudo port install pcre2 

und über Homebrew als:

% brew install pcre

oder für pcre2

% brew install pcre2

pcre2grep ist auch unter Linux verfügbar (Ubuntu 18.04+)

$ sudo apt install pcre2-utils # PCRE2
$ sudo apt install pcregrep    # Older PCRE
Ringträger
quelle
11
@StevenLu -M, --multiline- Ermöglicht, dass Muster mit mehr als einer Zeile übereinstimmen.
Ringträger
7
Beachten Sie, dass. * (\ N |.) * (\ N |.) * Entspricht und letzteres kürzer ist. Außerdem tritt auf meinem System "pcre_exec () error -8" auf, wenn ich die längere Version ausführe. Versuchen Sie stattdessen 'abc (\ n |.) * Efg'!
Daveagp
6
Sie müssen den Ausdruck in diesem Fall Beispiel nicht gierig machen:'abc.*(\n|.)*?efg'
Ringträger
4
und Sie können das erste weglassen .*-> 'abc(\n|.)*?efg'um die Regex kürzer zu machen (und um pedantisch zu sein)
Michi
6
pcregrepmacht die Sache einfacher, wird aber grepauch funktionieren. Siehe beispielsweise stackoverflow.com/a/7167115/123695
Michael Mior
113

Ich bin mir nicht sicher, ob es mit grep möglich ist, aber sed macht es sehr einfach:

sed -e '/abc/,/efg/!d' [file-with-content]
LJ.
quelle
4
Dies findet keine Dateien, es gibt den passenden Teil aus einer einzelnen Datei zurück
Shiggity
11
@Lj. Können Sie diesen Befehl erklären? Ich bin vertraut mit sed, aber wenn ich noch nie einen solchen Ausdruck gesehen habe.
Anthony
1
@Anthony, es ist in der Manpage von sed unter Adresse dokumentiert. Es ist wichtig zu wissen, dass / abc / & / efg / eine Adresse ist.
Tintenfisch
49
Ich vermute, diese Antwort wäre hilfreich gewesen, wenn sie etwas mehr Erklärung gehabt hätte, und in diesem Fall hätte ich sie noch einmal hochgestimmt. Ich kenne ein bisschen Sed, aber nicht genug, um diese Antwort zu verwenden, um nach einer halben Stunde Fummeln einen aussagekräftigen Exit-Code zu erstellen. Tipp: 'RTFM' erhält selten Up-Votes bei StackOverflow, wie Ihr vorheriger Kommentar zeigt.
Michael Scheper
25
Schnelle Erklärung am Beispiel: sed '1,5d': Zeilen zwischen 1 und 5 löschen. Sed '1,5! D': Zeilen löschen, die nicht zwischen 1 und 5 liegen (dh die Zeilen zwischen), dann können Sie anstelle einer Zahl Suche nach einer Zeile mit / pattern /. Siehe auch die einfachere unten: sed -n '/ abc /, / efg / p' p ist für den Druck und das Flag -n zeigt nicht alle Zeilen an
phil_w
86

Hier ist eine Lösung, die von dieser Antwort inspiriert ist :

  • wenn 'abc' und 'efg' in derselben Zeile stehen können:

    grep -zl 'abc.*efg' <your list of files>
  • wenn 'abc' und 'efg' in unterschiedlichen Zeilen stehen müssen:

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

Parameter:

  • -zBehandeln Sie die Eingabe als eine Reihe von Zeilen, die jeweils durch ein Null-Byte anstelle einer neuen Zeile abgeschlossen werden. dh grep behandelt die Eingabe als eine große Zeile.

  • -l Druckname jeder Eingabedatei, aus der normalerweise die Ausgabe gedruckt worden wäre.

  • (?s)aktiviere PCRE_DOTALL, was bedeutet, dass '.' findet ein Zeichen oder eine neue Zeile.

atti
quelle
@syntaxerror Nein, ich denke, es ist nur ein Kleinbuchstabe l. AFAIK gibt es keine -1Nummernoption.
Sparhawk
Scheint, als hättest du doch recht, vielleicht hatte ich beim Testen einen Tippfehler gemacht. Auf jeden Fall entschuldigen Sie die falsche Spur.
Syntaxfehler
6
Das ist ausgezeichnet. Ich habe nur eine Frage dazu. Wenn in den -zOptionen grep angegeben ist, um Zeilenumbrüche so zu behandeln, zero byte characterswarum benötigen wir dann das (?s)in der Regex? Wenn es sich bereits um ein Nicht-Zeilenumbruchzeichen handelt, sollte es dann nicht .direkt zugeordnet werden können?
Durga Swaroop
1
-z (auch bekannt als --null-data) und (? s) sind genau das, was Sie benötigen, um mehrzeilig mit einem Standard-Grep abzugleichen. Leute unter MacOS, bitte hinterlassen Sie Kommentare zur Verfügbarkeit von -z- oder --null-Datenoptionen auf Ihren Systemen!
Zeke Fast
4
-z definitiv nicht verfügbar unter MacOS
Dylan Nicholson
33

sed sollte als oben angegebenes Poster LJ ausreichen,

anstelle von! d können Sie einfach p zum Drucken verwenden:

sed -n '/abc/,/efg/p' file
Kara
quelle
15

Ich habe mich stark auf pcregrep verlassen, aber mit neuerem grep müssen Sie pcregrep für viele seiner Funktionen nicht installieren. Einfach benutzen grep -P.

Im Beispiel der OP-Frage denke ich, dass die folgenden Optionen gut funktionieren, wobei die zweitbeste zu meinem Verständnis der Frage passt:

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

Ich habe den Text als / tmp / test1 kopiert und das 'g' gelöscht und als / tmp / test2 gespeichert. Hier ist die Ausgabe, die zeigt, dass die erste die übereinstimmende Zeichenfolge und die zweite nur den Dateinamen anzeigt (typisch -o zeigt Übereinstimmung an und typisch -l zeigt nur Dateinamen an). Beachten Sie, dass das 'z' für mehrzeilig erforderlich ist und das '(. | \ N)' bedeutet, dass entweder 'irgendetwas anderes als Zeilenumbruch' oder 'Zeilenumbruch' übereinstimmt - dh alles:

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

Um festzustellen, ob Ihre Version neu genug ist, führen Sie sie aus man grepund prüfen Sie, ob oben etwas Ähnliches angezeigt wird:

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

Das ist aus GNU grep 2.10.

Salbei
quelle
14

Dies kann einfach durchgeführt werden, indem zuerst trdie Zeilenumbrüche durch ein anderes Zeichen ersetzt werden:

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

Hier verwende ich das Alarmzeichen \a(ASCII 7) anstelle einer neuen Zeile. Dies wird in Ihrem Text fast nie gefunden und grepkann mit einem .oder speziell mit einem übereinstimmen \a.

Gavin S. Yancey
quelle
1
Dies war mein Ansatz, aber ich benutzte \0und brauchte grep -aund passte auf \x00... Sie haben mir geholfen, zu vereinfachen! echo $log | tr '\n' '\0' | grep -aoE "Error: .*?\x00Installing .*? has failed\!" | tr '\0' '\n'ist jetztecho $log | tr '\n' '\a' | grep -oE "Error: .*?\aInstalling .*? has failed\!" | tr '\a' '\n'
Charlie Gorichanaz
1
Verwenden Sie grep -o.
Kyb
7

awk Einzeiler:

awk '/abc/,/efg/' [file-with-content]
Swynndla
quelle
4
Dies wird gerne von abcbis zum Ende der Datei gedruckt, wenn das Endmuster nicht in der Datei vorhanden ist oder das letzte Endmuster fehlt. Sie können das beheben, aber es wird das Skript ziemlich kompliziert.
Tripleee
Wie /efg/von der Ausgabe ausschließen?
Kyb
6

Sie können dies sehr einfach tun, wenn Sie Perl verwenden können.

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

Sie können dies auch mit einem einzelnen regulären Ausdruck tun. Dazu muss jedoch der gesamte Inhalt der Datei in einer einzigen Zeichenfolge zusammengefasst werden, was bei großen Dateien möglicherweise zu viel Speicherplatz beansprucht. Der Vollständigkeit halber ist hier diese Methode:

perl -e '@lines = <>; $content = join("", @lines); print "Found in $ARGV\n" if ($content =~ /abc.*efg/s);' yourfilename.txt
Sundar - Monica wieder einsetzen
quelle
Die zweite Antwort war nützlich, um einen ganzen mehrzeiligen Block mit Übereinstimmungen in ein paar Zeilen zu extrahieren - musste nicht gieriges Matching ( .*?) verwenden, um eine minimale Übereinstimmung zu erzielen.
RichVel
5

Ich weiß nicht, wie ich das mit grep machen würde, aber ich würde so etwas mit awk machen:

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

Sie müssen jedoch vorsichtig sein, wie Sie dies tun. Möchten Sie, dass die Regex mit der Teilzeichenfolge oder dem gesamten Wort übereinstimmt? Fügen Sie gegebenenfalls \ w Tags hinzu. Auch wenn dies genau dem entspricht, wie Sie das Beispiel angegeben haben, funktioniert es nicht ganz, wenn abc ein zweites Mal nach efg erscheint. Wenn Sie damit umgehen möchten, fügen Sie im Fall / abc / usw. ein Gegebenenfalls hinzu.

frankc
quelle
3

Das kannst du leider nicht. Aus den grepDokumenten:

grep durchsucht die benannten Eingabedateien (oder die Standardeingabe, wenn keine Dateien benannt sind oder wenn ein einzelnes Bindestrich-Minus (-) als Dateiname angegeben wird) nach Zeilen, die eine Übereinstimmung mit dem angegebenen MUSTER enthalten.

Kaleb Pederson
quelle
was ist mitgrep -Pz
Navaro
3

Wenn Sie bereit sind, Kontexte zu verwenden, kann dies durch Eingabe erreicht werden

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

Dies zeigt alles zwischen "abc" und "efg" an, solange sie innerhalb von 500 Zeilen voneinander liegen.

agouge
quelle
3

Wenn Sie möchten, dass beide Wörter nahe beieinander liegen, z. B. nicht mehr als 3 Zeilen, können Sie dies tun:

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

Gleiches Beispiel, aber nur * .txt-Dateien filtern:

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

Außerdem können Sie den grepBefehl durch einen egrepBefehl ersetzen, wenn Sie auch reguläre Ausdrücke suchen möchten.

Mariano Ruiz
quelle
3

Ich habe vor einigen Tagen eine grep-Alternative veröffentlicht, die dies direkt unterstützt, entweder durch mehrzeiliges Matching oder unter Verwendung von Bedingungen - hoffentlich ist es für einige Leute nützlich, die hier suchen. So würden die Befehle für das Beispiel aussehen:

Mehrzeilig:

sift -lm 'abc.*efg' testfile

Bedingungen:

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

Sie können auch angeben, dass 'efg' innerhalb einer bestimmten Anzahl von Zeilen auf 'abc' folgen muss:

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

Weitere Informationen finden Sie auf sift-tool.org .

svent
quelle
Ich denke nicht, dass das erste Beispiel sift -lm 'abc.*efg' testfilefunktioniert, da das Match gierig ist und alle Zeilen bis zum letzten efgin der Datei verschlingt .
Dr. Alex RE
2

Während die sed-Option die einfachste und einfachste ist, ist der Einzeiler von LJ leider nicht die tragbarste. Diejenigen, die mit einer Version der C-Shell feststecken, müssen ihrem Pony entkommen:

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

Dies funktioniert bei bash et al. Leider nicht.

Fehler
quelle
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
quelle
1

Sie können grep verwenden, falls Sie nicht an der Reihenfolge des Musters interessiert sind.

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

Beispiel

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

grep -lfindet alle Dateien, die mit dem ersten Muster übereinstimmen, und xargs sucht nach dem zweiten Muster. Hoffe das hilft.

Balu Mohan
quelle
1
Dies würde die Reihenfolge ignorieren, in der "Muster1" und "Muster2" in der Datei erscheinen. OP gibt jedoch ausdrücklich an, dass nur Dateien abgeglichen werden sollen, in denen "Muster2" NACH "Muster1" erscheint.
Emil Lundberg
1

Mit Silbersucher :

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

ähnlich der Antwort des Ringträgers, aber stattdessen mit ag. Geschwindigkeitsvorteile von Silver Searcher könnten hier möglicherweise glänzen.

Shwaydogg
quelle
1
Dies scheint nicht zu funktionieren. (echo abctest; echo efg)|ag 'abc.*(\n|.)*efg'stimmt nicht überein
Phiresky
1

Ich habe dies verwendet, um eine Fasta-Sequenz aus einer Multi-Fasta-Datei mit der Option -P für grep zu extrahieren:

grep -Pzo ">tig00000034[^>]+"  file.fasta > desired_sequence.fasta
  • P für perlbasierte Suchen
  • z, damit eine Zeile mit 0 Byte und nicht mit Zeilenumbruch endet
  • o um nur zu erfassen, was übereinstimmt, da grep die gesamte Zeile zurückgibt (was in diesem Fall, seit Sie -z getan haben, die gesamte Datei ist).

Der Kern des regulären Ausdrucks ist der, [^>]der "nicht größer als das Symbol" bedeutet.

Jon Boyle
quelle
0

Als Alternative zu Balu Mohan Antwort ist es möglich , die Reihenfolge der Muster nur mit zu erzwingen grep, headund 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

Dieser ist allerdings nicht sehr hübsch. Lesbarer formatiert:

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

Dadurch werden die Namen aller Dateien drucken , wo "pattern2"nach erscheint "pattern1", oder wo beide erscheinen auf der gleichen Linie :

$ 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

Erläuterung

  • tail -n +i- Alle Zeilen nach dem ith einschließlich drucken
  • grep -n - Stellen Sie übereinstimmende Zeilen mit ihren Zeilennummern voran
  • head -n1 - Nur die erste Zeile drucken
  • cut -d : -f 1- Drucken Sie die erste Schnittspalte :als Trennzeichen
  • 2>/dev/null- Stummschaltungsfehlerausgabe tail, die auftritt, wenn der $()Ausdruck leer zurückgegeben wird
  • grep -q- Schweigen grepund sofort zurückkehren, wenn eine Übereinstimmung gefunden wird, da wir nur am Exit-Code interessiert sind
Emil Lundberg
quelle
Kann mir bitte jemand das erklären &>? Ich benutze es auch, aber ich habe es nirgendwo dokumentiert gesehen. Übrigens, warum müssen wir grep eigentlich so zum Schweigen bringen? grep -qwird der Trick nicht auch tun?
Syntaxfehler
1
&>weist bash an, sowohl die Standardausgabe als auch den Standardfehler umzuleiten, siehe REDIRECTION im bash-Handbuch. Sie haben sehr Recht damit, dass wir es genauso gut tun könnten, grep -q ...anstatt grep ... &>/dev/nullguten Fang zu machen!
Emil Lundberg
Dachte mir. Entfernt den Schmerz vieler unangenehmer zusätzlicher Eingaben. Danke für die Erklärung - also muss ich ein bisschen im Handbuch übersprungen haben. (Ich habe vor einiger Zeit etwas aus der Ferne
nachgeschlagen
0

Das sollte auch funktionieren?!

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

$ARGVEnthält den Namen der aktuellen Datei beim Lesen von file_list /sModifikatorsuchen über Zeilenumbrüche.

PS12
quelle
0

Das Dateimuster *.shist wichtig, um zu verhindern, dass Verzeichnisse überprüft werden. Natürlich könnte auch ein Test dies verhindern.

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

Das

grep -n -m1 abc $f 

sucht maximal 1 Matching und gibt (-n) die Leinennummer zurück. Wenn eine Übereinstimmung gefunden wurde (test -n ...), finde die letzte Übereinstimmung von efg (finde alle und nimm die letzte mit Schwanz -n 1).

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

sonst weiter.

Da das Ergebnis so etwas wie ist 18:foofile.sh String alf="abc";, müssen wir bis zum Zeilenende von ":" wegschneiden.

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

Sollte ein positives Ergebnis liefern, wenn die letzte Übereinstimmung des 2. Ausdrucks nach der ersten Übereinstimmung des ersten Ausdrucks liegt.

Dann melden wir den Dateinamen echo $f.

Benutzer unbekannt
quelle
0

Warum nicht etwas Einfaches wie:

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

gibt 0 oder eine positive ganze Zahl zurück.

egrep -o (Zeigt nur Übereinstimmungen an, Trick: Mehrere Übereinstimmungen in derselben Zeile erzeugen eine mehrzeilige Ausgabe, als ob sie sich in verschiedenen Zeilen befinden.)

  • grep -A1 abc (drucke abc und die Zeile danach)

  • grep efg | wc -l (0-n Anzahl der nach abc in denselben oder folgenden Zeilen gefundenen efg-Zeilen, Ergebnis kann in einem 'if "verwendet werden)

  • grep kann in egrep usw. geändert werden, wenn ein Mustervergleich erforderlich ist

Kevins
quelle
0

Wenn Sie eine Schätzung über den Abstand zwischen den beiden gesuchten Zeichenfolgen 'abc' und 'efg' haben, können Sie Folgendes verwenden:

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

Auf diese Weise gibt der erste grep die Zeile mit den Zeilen 'abc' plus # num1 danach und den Zeilen # num2 danach zurück, und der zweite grep durchsucht alle Zeilen, um das 'efg' zu erhalten. Dann wissen Sie, bei welchen Dateien sie zusammen erscheinen.

Benjamin Berend
quelle
0

Mit ugrep vor ein paar Monaten veröffentlicht:

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

Dieses Tool ist stark auf Geschwindigkeit optimiert. Es ist auch GNU / BSD / PCRE-grep-kompatibel.

Beachten Sie, dass wir eine verzögerte Wiederholung verwenden sollten +?, es sei denn, Sie möchten alle Zeilen efgbis zum letzten efgin der Datei miteinander abgleichen.

Dr. Alex RE
quelle
-3

Das sollte funktionieren:

cat FILE | egrep 'abc|efg'

Wenn es mehr als eine Übereinstimmung gibt, können Sie mit grep -v herausfiltern

Guru
quelle
2
Während dieser Code - Schnipsel zu begrüßen ist, und etwas Hilfe bieten kann, würde es erheblich verbessert , wenn es eine Erklärung enthalten von , wie und warum Dies löst das Problem. Denken Sie daran, dass Sie in Zukunft die Frage für die Leser beantworten, nicht nur für die Person, die jetzt fragt! Bitte bearbeiten Sie Ihre Antwort, um eine Erklärung hinzuzufügen, und geben Sie an, welche Einschränkungen und Annahmen gelten.
Toby Speight
1
Das sucht nicht wirklich über mehrere Zeilen hinweg , wie in der Frage angegeben.
9.