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.