Kann grep nur bestimmte Gruppierungen ausgeben, die übereinstimmen?

291

Angenommen, ich habe eine Datei:

# file: 'test.txt'
foobar bash 1
bash
foobar happy
foobar

Ich möchte nur wissen, welche Wörter nach "foobar" erscheinen, damit ich diesen regulären Ausdruck verwenden kann:

"foobar \(\w\+\)"

Die Klammern zeigen an, dass ich ein besonderes Interesse an dem Wort direkt nach foobar habe. Aber wenn ich a mache grep "foobar \(\w\+\)" test.txt, bekomme ich die ganzen Zeilen, die dem gesamten regulären Ausdruck entsprechen, und nicht nur "das Wort nach foobar":

foobar bash 1
foobar happy

Ich würde es sehr bevorzugen, wenn die Ausgabe dieses Befehls so aussähe:

bash
happy

Gibt es eine Möglichkeit, grep anzuweisen, nur die Elemente auszugeben, die der Gruppierung (oder einer bestimmten Gruppierung) in einem regulären Ausdruck entsprechen?

Cory Klein
quelle
4
für diejenigen, die grep nicht brauchen:perl -lne 'print $1 if /foobar (\w+)/' < test.txt
Gewölbe

Antworten:

326

GNU grep bietet die -POption für reguläre Ausdrücke im Perl-Stil und die -oOption, nur das zu drucken, was dem Muster entspricht. Diese können mit Hilfe von Look-Around-Behauptungen (beschrieben unter Erweiterte Muster in der Perlre-Manpage ) kombiniert werden, um einen Teil des Grep- Musters von dem zu entfernen, für den eine Übereinstimmung festgestellt wurde -o.

$ grep -oP 'foobar \K\w+' test.txt
bash
happy
$

Dies \Kist die Kurzform (und effizientere Form), (?<=pattern)die Sie als Look-Behind-Behauptung ohne Breite vor dem auszugebenden Text verwenden. (?=pattern)kann als Look-Ahead-Behauptung mit der Breite Null nach dem auszugebenden Text verwendet werden.

Wenn Sie beispielsweise das Wort zwischen foound barzuordnen möchten, können Sie Folgendes verwenden:

$ grep -oP 'foo \K\w+(?= bar)' test.txt

oder (aus Symmetriegründen)

$ grep -oP '(?<=foo )\w+(?= bar)' test.txt
camh
quelle
3
Wie machst du das, wenn dein Regex mehr als eine Gruppierung hat? (wie der Titel schon
sagt
4
@barracel: Ich glaube nicht, dass du es kannst. Zeit fürsed(1)
camh
1
@camh Ich habe gerade getestet, dass grep -oP 'foobar \K\w+' test.txtmit den OP's nichts ausgegeben wird test.txt. Die Grep-Version ist 2.5.1. Was könnte falsch sein? O_O
SOUser
@ XichenLi: Ich kann nicht sagen. Ich habe gerade v2.5.1 von grep (es ist ziemlich alt - von 2006) erstellt und es hat bei mir funktioniert.
camh
@SOUser: Ich habe das gleiche erlebt - gibt nichts zur Datei aus. Ich habe die Editieranforderung gesendet, '>' vor dem Dateinamen einzufügen, um die Ausgabe zu senden, da dies für mich funktioniert hat.
RJCHICAGO
39

Standard grep kann dies nicht, aber die neuesten Versionen von GNU grep können dies . Sie können sich an sed, awk oder perl wenden. Hier sind einige Beispiele, die das tun, was Sie von Ihrer Beispieleingabe erwarten. Sie verhalten sich in Eckfällen etwas anders.

Ersetzen foobar word other stuffdurch word, nur drucken, wenn ein Ersatz erfolgt ist.

sed -n -e 's/^foobar \([[:alnum:]]\+\).*/\1/p'

Wenn das erste Wort ist foobar, drucken Sie das zweite Wort.

awk '$1 == "foobar" {print $2}'

Streifen Sie ab, foobarwenn es das erste Wort ist, und überspringen Sie die Zeile ansonsten. Dann alles nach dem ersten Leerzeichen entfernen und ausdrucken.

perl -lne 's/^foobar\s+// or next; s/\s.*//; print'
Gilles
quelle
Genial! Ich dachte, ich könnte das mit sed machen, aber ich habe es noch nie benutzt und hoffte, ich könnte mein Vertrautes benutzen grep. Aber die Syntax für diese Befehle kommt mir bekannt vor, da ich mit Suchen & Ersetzen + Regexen im Vim-Stil vertraut bin. Danke vielmals.
Cory Klein
1
Nicht wahr, Gilles. Siehe meine Antwort für eine GNU-grep-Lösung.
20.
1
@camh: Ah, ich wusste nicht, dass GNU grep jetzt volle PCRE-Unterstützung hat. Ich habe meine Antwort korrigiert, danke.
Gilles
1
Diese Antwort ist besonders nützlich für Embedded Linux, da Busybox grepkeine PCRE-Unterstützung bietet.
Craig McQueen
Offensichtlich gibt es mehrere Möglichkeiten, die gleiche Aufgabe zu erledigen. Wenn das OP Sie jedoch nach der Verwendung von grep fragt, warum antworten Sie dann auf etwas anderes? Auch dein erster Absatz ist falsch: Ja, grep kann das.
fcm
32
    sed -n "s/^.*foobar\s*\(\S*\).*$/\1/p"

-n     suppress printing
s      substitute
^.*    anything before foobar
foobar initial search match
\s*    any white space character (space)
\(     start capture group
\S*    capture any non-white space character (word)
\)     end capture group
.*$    anything after the capture group
\1     substitute everything with the 1st capture group
p      print it
jgshawkey
quelle
1
+1 für das sed-Beispiel scheint ein besseres Werkzeug für den Job zu sein als grep. Ein Kommentar, der ^und $sind irrelevant, da .*es sich um ein gieriges Match handelt. Das Einbeziehen dieser Elemente kann jedoch hilfreich sein, um die Absicht des regulären Ausdrucks zu klären.
Tony
18

Wenn Sie wissen, dass foobar immer das erste Wort oder die erste Zeile ist, können Sie cut verwenden. Wie so:

grep "foobar" test.file | cut -d" " -f2
Dave
quelle
Die -oAktivierung von grep ist weit verbreitet (mehr als die Gnu-grep-Erweiterungen). Dadurch grep -o "foobar" test.file | cut -d" " -f2wird die Effektivität dieser Lösung erhöht, die portabler ist als die Verwendung von Lookbehind-Assertions.
dubiousjim
Ich glaube, dass Sie brauchen würden grep -o "foobar .*"oder grep -o "foobar \w+".
G-Man
9

Wenn PCRE nicht unterstützt wird, können Sie mit zwei Aufrufen von grep dasselbe Ergebnis erzielen. Um beispielsweise das Wort nach foobar zu erfassen, gehen Sie wie folgt vor :

<test.txt grep -o 'foobar  *[^ ]*' | grep -o '[^ ]*$'

Dies kann nach foobar wie folgt zu einem beliebigen Wort erweitert werden (mit EREs zur besseren Lesbarkeit):

i=1
<test.txt egrep -o 'foobar +([^ ]+ +){'$i'}[^ ]+' | grep -o '[^ ]*$'

Ausgabe:

1

Beachten Sie, dass der Index auf iNull basiert.

Thor
quelle
6

pcregrepMit einer intelligenteren -oOption können Sie auswählen, welche Erfassungsgruppen ausgegeben werden sollen. Also, mit Ihrer Beispieldatei,

$ pcregrep -o1 "foobar (\w+)" test.txt
bash
happy
G-Man
quelle
4

Die Verwendung grepist nicht plattformübergreifend, da -P/ --perl-regexpnur unter GNUgrep und nicht unter BSDgrep verfügbar ist .

Hier ist die Lösung mit ripgrep:

$ rg -o "foobar (\w+)" -r '$1' <test.txt
bash
happy

Wie pro man rg:

-r/ --replace REPLACEMENT_TEXTErsetzen Sie jede Übereinstimmung durch den angegebenen Text.

Erfassungsgruppenindizes (z. B. $5) und Namen (z. B. $foo) werden in der Ersetzungszeichenfolge unterstützt.

Verwandt: GH-462 .

Kenorb
quelle
2

Ich fand die Antwort von @jgshawkey sehr hilfreich. grepist kein so gutes Werkzeug dafür, aber sed ist es, obwohl wir hier ein Beispiel haben, das grep verwendet, um eine relevante Zeile zu erfassen.

Die Regex-Syntax von sed ist eigenwillig, wenn Sie nicht daran gewöhnt sind.

Hier ist ein weiteres Beispiel: Dieses analysiert die Ausgabe von xinput, um eine ID-Ganzzahl zu erhalten

⎜   ↳ SynPS/2 Synaptics TouchPad                id=19   [slave  pointer  (2)]

und ich will 19

export TouchPadID=$(xinput | grep 'TouchPad' | sed  -n "s/^.*id=\([[:digit:]]\+\).*$/\1/p")

Beachten Sie die Klassensyntax:

[[:digit:]]

und die Notwendigkeit, dem Folgenden zu entkommen +

Ich gehe davon aus, dass nur eine Zeile passt.

Tim Richardson
quelle
Genau das habe ich versucht. Vielen Dank!
James
Etwas einfachere Version ohne das Extra grep, vorausgesetzt 'TouchPad' steht links von 'id':echo "SynPS/2 Synaptics TouchPad id=19 [slave pointer (2)]" | sed -nE "s/.*TouchPad.+id=([0-9]+).*/\1/p"
Amit Naidu