Grep può produrre solo raggruppamenti specifici corrispondenti?


293

Di 'che ho un file:

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

Voglio solo sapere quali parole compaiono dopo "foobar", quindi posso usare questa regex:

"foobar \(\w\+\)"

Le parentesi indicano che ho un interesse speciale per la parola subito dopo il foobar. Ma quando faccio un grep "foobar \(\w\+\)" test.txt, ottengo le intere righe che corrispondono all'intera regex, piuttosto che solo "la parola dopo foobar":

foobar bash 1
foobar happy

Preferirei di gran lunga che l'output di quel comando fosse simile al seguente:

bash
happy

C'è un modo per dire a grep di produrre solo gli elementi che corrispondono al raggruppamento (o un raggruppamento specifico) in un'espressione regolare?


4
per coloro che non hanno bisogno di grep:perl -lne 'print $1 if /foobar (\w+)/' < test.txt
caveau

Risposte:


327

GNU grep ha l' -Popzione per le regex in stile perl e l' -oopzione per stampare solo ciò che corrisponde al modello. Questi possono essere combinati usando asserzioni look-around (descritte in Extended Patterns nella manpage perlre ) per rimuovere parte del pattern grep da ciò che è stato determinato corrispondere ai fini di -o.

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

Il \Kè la forma abbreviata (e la forma più efficiente) di (?<=pattern)cui si utilizza come uno zero-width look-dietro l'affermazione prima che il testo che si desidera in uscita. (?=pattern)può essere utilizzato come un'asserzione di previsione a larghezza zero dopo il testo che si desidera produrre.

Ad esempio, se si desidera abbinare la parola tra fooe bar, è possibile utilizzare:

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

o (per simmetria)

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

3
Come lo fai se il tuo regex ha più di un raggruppamento? (come
suggerisce

4
@barracel: non credo che tu possa. Tempo persed(1)
camh

1
@camh Ho appena testato che grep -oP 'foobar \K\w+' test.txtnon produce nulla con gli OP test.txt. La versione grep è 2.5.1. Cosa potrebbe esserci di sbagliato? O_O
SOUser

@XichenLi: non posso dire. Ho appena creato v2.5.1 di grep (è piuttosto vecchio - dal 2006) e ha funzionato per me.
Camh,

@SOUser: ho provato lo stesso - non viene emesso nulla da archiviare. Ho inviato la richiesta di modifica per includere '>' prima del nome del file per inviare l'output poiché ha funzionato per me.
rjchicago,

39

Grep standard non può farlo, ma le versioni recenti di GNU grep possono farlo . Puoi passare a sed, awk o perl. Ecco alcuni esempi che fanno quello che vuoi sul tuo input di esempio; si comportano in modo leggermente diverso in casi angolari.

Sostituisci foobar word other stuffcon word, stampa solo se viene effettuata una sostituzione.

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

Se la prima parola è foobar, stampa la seconda parola.

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

Rimuovi foobarse è la prima parola e salta la riga altrimenti; quindi rimuovere tutto dopo il primo spazio bianco e stampare.

perl -lne 's/^foobar\s+// or next; s/\s.*//; print'

Eccezionale! Pensavo di essere in grado di farlo con sed, ma non l'ho mai usato prima e speravo di poter usare il mio familiare grep. Ma la sintassi di questi comandi sembra davvero molto familiare ora che ho familiarità con la ricerca in stile vim e sostituisci + regex. Grazie mille.
Cory Klein,

1
Non è vero, Gilles. Vedi la mia risposta per una soluzione GNU grep.
Camh,

1
@camh: Ah, non sapevo che GNU grep ora avesse il pieno supporto per PCRE. Ho corretto la mia risposta, grazie.
Gilles,

1
Questa risposta è particolarmente utile per Linux incorporato poiché Busybox grepnon ha il supporto PCRE.
Craig McQueen,

Ovviamente ci sono diversi modi per eseguire lo stesso compito presentato, tuttavia, se l'OP chiede l'utilizzo di grep, perché rispondi a qualcos'altro? Inoltre, il tuo primo paragrafo non è corretto: sì, grep può farlo.
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

1
+1 per l'esempio sed, sembra uno strumento migliore per il lavoro rispetto a grep. Un commento, ^e $sono estranei poiché .*è una partita golosa. Tuttavia, includerli potrebbe aiutare a chiarire l'intento della regex.
Tony,

18

Bene, se sai che foobar è sempre la prima parola o la riga, puoi usare cut. Così:

grep "foobar" test.file | cut -d" " -f2

Il -opassaggio a grep è ampiamente implementato (più delle estensioni grep di Gnu), quindi fare grep -o "foobar" test.file | cut -d" " -f2ciò aumenterà l'efficacia di questa soluzione, che è più portabile rispetto all'uso delle affermazioni lookbehind.
dubiousjim,

Credo che avresti bisogno di grep -o "foobar .*"o grep -o "foobar \w+".
G-Man

9

Se PCRE non è supportato, puoi ottenere lo stesso risultato con due invocazioni di grep. Ad esempio per afferrare la parola dopo foobar fare questo:

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

Questo può essere espanso in una parola arbitraria dopo foobar come questo (con ERE per la leggibilità):

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

Produzione:

1

Si noti che l'indice iè a base zero.


6

pcregrepha -oun'opzione più intelligente che ti consente di scegliere quali gruppi di acquisizione vuoi ottenere. Quindi, usando il tuo file di esempio,

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

4

L'uso grepnon è compatibile con più piattaforme, poiché -P/ --perl-regexpè disponibile solo su GNUgrep , non su BSDgrep .

Ecco la soluzione usando ripgrep:

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

Secondo man rg:

-r/ --replace REPLACEMENT_TEXTSostituisci ogni corrispondenza con il testo indicato.

Gli indici dei gruppi di acquisizione (ad es. $5) E i nomi (ad es. $foo) Sono supportati nella stringa di sostituzione.

Correlati: GH-462 .


2

Ho trovato molto utile la risposta di @jgshawkey. grepnon è un ottimo strumento per questo, ma sed è, anche se qui abbiamo un esempio che utilizza grep per afferrare una linea pertinente.

La sintassi Regex di sed è idiosincratica se non ci si è abituati.

Ecco un altro esempio: questo analizza l'output di xinput per ottenere un numero intero ID

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

e voglio 19

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

Nota la sintassi della classe:

[[:digit:]]

e la necessità di sfuggire a quanto segue +

Presumo che corrispondano solo a una riga.


Questo è esattamente quello che stavo cercando di fare. Grazie!
James,

Versione leggermente più semplice senza extra grep, supponendo che 'TouchPad' sia alla sinistra di 'id':echo "SynPS/2 Synaptics TouchPad id=19 [slave pointer (2)]" | sed -nE "s/.*TouchPad.+id=([0-9]+).*/\1/p"
Amit Naidu,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.