Wie greife ich nach Zeilen, die eines von zwei Wörtern enthalten, aber nicht beide?

25

Ich versuche, grepnur Zeilen anzuzeigen, die eines der beiden Wörter enthalten, wenn nur eines in der Zeile erscheint, aber nicht, wenn sie sich in derselben Zeile befinden.

Bisher habe ich es versucht grep pattern1 | grep pattern2 | ..., aber nicht das erwartete Ergebnis erzielt.

Trasmos
quelle
(1) Sie sprechen von „Wörtern“ und „Mustern“. Welches ist es? Gewöhnliche Wörter wie "schnell", "braun" und "Fuchs" oder reguläre Ausdrücke wie [a-z][a-z0-9]\(,7\}\(\.[a-z0-9]\{,3\}\)+? (2) Was ist, wenn eines der Wörter / Muster mehr als einmal in einer Zeile erscheint (und das andere nicht)? Entspricht das dem Wort, das einmal vorkommt, oder zählt es als Mehrfachvorkommen?
G-Man sagt, dass Monica

Antworten:

59

Ein anderes Werkzeug als grepder richtige Weg.

Unter Verwendung von Perl wäre der Befehl beispielsweise:

perl -ne 'print if /pattern1/ xor /pattern2/'

perl -neFührt den Befehl aus, der über jede Zeile von stdin ausgegeben wird. In diesem Fall wird die Zeile gedruckt, wenn sie übereinstimmt /pattern1/ xor /pattern2/, oder mit anderen Worten , wenn sie mit einem Muster übereinstimmt, jedoch nicht mit dem anderen (exklusiv oder).

Dies funktioniert für das Muster in jeder Reihenfolge und sollte eine bessere Leistung aufweisen als mehrere Aufrufe von grepund ist auch weniger tippend.

Oder noch kürzer mit awk:

awk 'xor(/pattern1/,/pattern2/)'

oder für Versionen von awk, die nicht haben xor:

awk '/pattern1/+/pattern2/==1`
Chris
quelle
4
Schön - gibt es das Awk xornur in GNU Awk?
Steeldriver
9
@steeldriver Ich denke, es ist nur GNU, ja. Oder zumindest fehlt es bei älteren Versionen. Sie können es durch /pattern1/+/pattern2/==1ir ersetzen xorfehlt.
Chris
4
@JimL. Sie könnten Wortgrenzen ( \b) in die Muster selbst einfügen, dh \bword\b.
wjandrea
4
@vikingsteve Wenn Sie speziell grep verwenden möchten, gibt es hier viele andere Antworten. Aber für Leute, die einfach nur ihre Arbeit erledigen wollen, ist es gut zu wissen, dass es andere Tools gibt, die alles können, was grep macht, aber immer einfacher.
Chris
3
@vikingsteve Ich würde stark annehmen, dass die Nachfrage nach einer grep-Lösung eine Art XY-Problem ist
Hagen von Eitzen
30

Mit GNU grepkönnen Sie beide Wörter übergeben grepund dann die Zeilen entfernen, die beide Muster enthalten.

$ cat testfile.txt
abc
def
abc def
abc 123 def
1234
5678
1234 def abc
def abc

$ grep -w -e 'abc' -e 'def' testfile.txt | grep -v -e 'abc.*def' -e 'def.*abc'
abc
def
Haxiel
quelle
16

Versuche es mit egrep

egrep  'pattern1|pattern2' file | grep -v -e 'pattern1.*pattern2' -e 'pattern2.*pattern1'
msp9011
quelle
3
kann auch geschrieben werden alsgrep -e foo -e bar | grep -v -e 'foo.*bar' -e 'bar.*foo'
Glenn Jackman
8
Beachten Sie auch auf der Manpage von grep: Direct invocation as either egrep or fgrep is deprecated- prefergrep -E
glenn jackman
Das steht nicht in meinem Betriebssystem @glennjackman
Grump
1
@Grump wirklich? Welches Betriebssystem ist das? Sogar POSIX erwähnt, dass grep -fund -eOptionen haben sollte , obwohl die älteren egrepund fgrepnoch eine Weile unterstützt werden.
terdon
1
@terdon, POSIX gibt den Pfad der POSIX-Dienstprogramme nicht an. Auch hier gibt der Standard grep(das Stützen -F, -E, -e, -fwie POSIX erfordert) ist in /usr/xpg4/bin. Die Versorgungsunternehmen in /binsind veraltet.
Stéphane Chazelas
12

Mit grepImplementierungen, die perlähnliche reguläre Ausdrücke (wie pcregrepoder GNU oder ast-open grep -P) unterstützen, können Sie dies in einem grepAufruf tun :

grep -P '^(?=.*pat1)(?!.*pat2)|^(?=.*pat2)(?!.*pat1)'

Das heißt die Zeilen finden, die pat1aber nicht passen pat2, oder pat2doch nicht pat1.

(?=...)und (?!...)sind jeweils vorausschauende und negative vorausschauende Betreiber. Technisch betrachtet, sucht das Obige nach dem Anfang des Themas ( ^), vorausgesetzt, es wird gefolgt .*pat1und nicht gefolgt .*pat2, oder dasselbe mit pat1und pat2umgekehrt.

Das ist suboptimal für Linien, die beide Muster enthalten, da sie dann zweimal gesucht würden. Sie könnten stattdessen fortgeschrittenere Perl-Operatoren verwenden, wie:

grep -P '^(?=.*pat1|())(?(1)(?=.*pat2)|(?!.*pat2))'

(?(1)yespattern|nopattern)Übereinstimmungen mit, yespatternwenn die 1st- Erfassungsgruppe ( ()oben leer ) übereinstimmt, nopatternandernfalls. Wenn dies ()zutrifft, bedeutet pat1dies , dass es nicht zutrifft, und wir suchen nach pat2(positiver Blick nach vorn) und wir suchen nach nichts anderem pat2 (negativer Blick nach vorn).

Mit sedkönnte man es schreiben:

sed -ne '/pat1/{/pat2/!p;d;}' -e '/pat2/p'
Stéphane Chazelas
quelle
Ihre erste Lösung schlägt fehl grep: the -P option only supports a single pattern, zumindest auf jedem System, auf das ich Zugriff habe. +1 für Ihre zweite Lösung.
Chris
1
@ Chris, du hast recht. Das scheint eine GNU-spezifische Einschränkung zu sein grep. pcregrepund ast-open grep haben dieses problem nicht. Ich habe das Multiple -edurch den alternierenden RE-Operator ersetzt, daher sollte es grepjetzt auch mit GNU funktionieren .
Stéphane Chazelas
Ja, es funktioniert jetzt gut.
Chris
3

In Booleschen Begriffen suchen Sie nach A xor B, das als geschrieben werden kann

(A und nicht B)

oder

(B und nicht A)

Da in Ihrer Frage nicht erwähnt wird, dass Sie sich mit der Reihenfolge der Ausgabe befassen, solange die übereinstimmenden Zeilen angezeigt werden, ist die Boolesche Erweiterung von A xor B in grep ziemlich einfach:

$ cat << EOF > foo
> a b
> a
> b
> c a
> c b
> b a
> b c
> EOF
$ grep -w 'a' foo | grep -vw 'b'; grep -w 'b' foo | grep -vw 'a';
a
c a
b
c b
b c
Jim L.
quelle
1
Dies funktioniert, aber es wird die Reihenfolge der Datei verschlüsseln.
Sparhawk
@Sparhawk Stimmt, obwohl "Scramble" ein hartes Wort ist. ;) listet zuerst alle 'a'-Übereinstimmungen nacheinander auf, dann alle' b'-Übereinstimmungen nacheinander. Das OP hat kein Interesse an der Aufrechterhaltung der Reihenfolge bekundet, nur die Linien zeigen. FAWK, der nächste Schritt könnte sein sort | uniq.
Jim L.
Fairer Ruf; Ich stimme zu, dass meine Sprache ungenau war. Ich wollte damit andeuten, dass die ursprüngliche Reihenfolge geändert würde.
Sparhawk
1
@Sparhawk ... Und ich habe in Ihrer Beobachtung zur vollständigen Offenlegung bearbeitet.
Jim L.
-2

Für das folgende Beispiel:

# Patterns:
#    apple
#    pear

# Example line
line="a_apple_apple_pear_a"

Dies kann rein mit getan werden grep -E, uniqund wc.

# Grep for regex pattern, sort as unique, and count the number of lines
result=$(grep -oE 'apple|pear' <<< $line | sort -u | wc -l)

Wenn grepes mit regulären Perl-Ausdrücken kompiliert wurde, können Sie beim letzten Vorkommen eine Übereinstimmung finden, anstatt eine Pipe an uniqfolgende Stellen ausführen zu müssen :

# Grep for regex pattern and count the number of lines
result=$(grep -oP '(apple(?!.*apple)|pear(?!.*pear))' <<< $line | wc -l)

Ergebnis ausgeben:

# Only one of the words exists if the result is < 2
((result > 0)) &&
   if (($result < 2)); then
      echo Only one word matched
   else
      echo Both words matched
   fi

Ein Einzeiler:

(($(grep -oP '(apple(?!.*apple)|pear(?!.*pear))' <<< $line | wc -l) == 1)) && echo Only one word matched

Wenn Sie das Muster nicht hart codieren möchten, können Sie es mit einer Funktion automatisieren, indem Sie es mit einem variablen Satz von Elementen zusammensetzen.

Dies kann auch nativ in Bash als Funktion ohne Pipes oder zusätzliche Prozesse erfolgen, ist jedoch aufwändiger und liegt wahrscheinlich außerhalb des Rahmens Ihrer Frage.

Zhro
quelle
(1) Ich habe mich gefragt, wann jemand eine Antwort mit regulären Perl-Ausdrücken geben würde. Wenn Sie sich auf diesen Teil Ihres Beitrags konzentriert und erklärt haben, wie es funktioniert, könnte dies eine gute Antwort sein. (2) Aber ich fürchte, der Rest ist nicht so gut. Die Frage lautet „Nur Zeilen anzeigen, die eines der beiden Wörter enthalten“ (Hervorhebung hinzugefügt). Wenn die Ausgabe Zeilen sein soll , ist es naheliegend, dass die Eingabe auch mehrere Zeilen sein muss.   Ihr Ansatz funktioniert jedoch nur, wenn Sie nur eine einzelne Zeile betrachten. … (Fortsetzung)
G-Man sagt, dass Monica
(Fortsetzung) ... Wenn die Eingabe beispielsweise die Zeilen Big apple\nund enthält pear-shaped\n, sollte die Ausgabe beide Zeilen enthalten. Ihre Lösung würde mit 2 bewertet; Die Langfassung würde "Beide Wörter stimmen überein" (was eine Antwort auf die falsche Frage ist) und die Kurzfassung würde überhaupt nichts aussagen. (3) Ein Vorschlag: Die Verwendung von -ohier ist eine wirklich schlechte Idee, da hier die Zeilen ausgeblendet werden, die die Übereinstimmungen enthalten, sodass Sie nicht sehen können, wann beide Wörter in derselben Zeile erscheinen. … (Fortsetzung)
G-Man sagt, dass Monica
(Fortsetzung)… (4) Fazit: Ihre Verwendung von uniq/ sort -uund der ausgefallene reguläre Perl-Ausdruck, der nur dem letzten Vorkommen in jeder Zeile entspricht, ergeben keine wirklich nützliche Antwort auf diese Frage. Aber selbst wenn dies der Fall wäre, wäre es immer noch eine schlechte Antwort, da Sie nicht erklären, wie sie zur Beantwortung der Frage beitragen. ( Ein Beispiel für eine gute Erklärung finden Sie in der Antwort von Stéphane Chazelas .)
G-Man sagt, dass Monica
Das OP sagt, dass sie "nur Zeilen anzeigen wollten, die eines der beiden Wörter enthalten", was bedeutet, dass jede Zeile für sich ausgewertet werden muss. Ich verstehe nicht, warum Sie das Gefühl haben, dass dies die Frage nicht beantwortet. Bitte geben Sie ein Beispiel an, bei dem Sie der Meinung sind, dass es fehlschlagen würde.
Zhro
Oh, ist das , was Sie gemeint? “Lesen Sie die Eingabe zeilenweise und führen Sie diese zwei oder drei Befehle für jede Zeile aus . ”? (1) Es ist schmerzlich unklar, dass Sie das gemeint haben. (2) Es ist schmerzlich ineffizient. Vier Antworten vor Ihnen zeigten, wie Sie mit wenigen Befehlen (eins, zwei oder vier) mit der gesamten Datei umgehen , und Sie möchten 3 × ausführen  n Befehle für n Eingabezeilen ausführen ? Selbst wenn es funktioniert, verdient es eine Ablehnung für unnötig teure Ausführung. (3) Auf die Gefahr Haarspalterei, es macht immer noch nicht die Aufgabe , zeigt die entsprechenden Zeilen.
G-Man sagt, dass Monica