Grep peut-il ne générer que les groupes spécifiés qui correspondent?

293

Dis que j'ai un fichier:

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

Je veux seulement savoir quels mots apparaissent après "foobar", je peux donc utiliser cette regex:

"foobar \(\w\+\)"

Les parenthèses indiquent que j’ai un intérêt particulier pour le mot juste après foobar. Mais quand je fais un grep "foobar \(\w\+\)" test.txt, je reçois les lignes entières qui correspondent à la regex entière, plutôt que juste "le mot après foobar":

foobar bash 1
foobar happy

Je préférerais de beaucoup que le résultat de cette commande ressemble à ceci:

bash
happy

Existe-t-il un moyen d'indiquer à grep de ne générer que les éléments correspondant au groupe (ou à un groupe spécifique) dans une expression régulière?

Cory Klein
la source
4
pour ceux qui n'ont pas besoin de grep:perl -lne 'print $1 if /foobar (\w+)/' < test.txt
vault

Réponses:

328

GNU grep a l’ -Poption pour les expressions rationnelles de style Perl et l’ -ooption d’imprimer uniquement ce qui correspond au modèle. Ceux-ci peuvent être combinés à l'aide d'assertions de recherche (décrites dans la section relative aux modèles étendus de la page de manuel perlre ) pour supprimer une partie du modèle grep de ce qui a été déterminé comme ayant correspondu -o.

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

La \Kest la forme abrégée (et la plus efficace) (?<=pattern)que vous utilisez comme assertion de suivi de largeur nulle avant le texte que vous souhaitez afficher. (?=pattern)peut être utilisé comme une affirmation d'anticipation de largeur zéro après le texte que vous souhaitez générer.

Par exemple, si vous voulez faire correspondre le mot entre fooet bar, vous pouvez utiliser:

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

ou (pour symétrie)

$ grep -oP '(?<=foo )\w+(?= bar)' test.txt
camh
la source
3
Comment faites-vous si votre regex a plus qu'un groupement? (comme le titre l'indique?)
barracel
4
@barracel: Je ne crois pas que vous puissiez le faire. Il est temps poursed(1)
camh
1
@camh Je viens de tester que grep -oP 'foobar \K\w+' test.txtrien ne sort avec les OP test.txt. La version de grep est 2.5.1. Quel pourrait être le problème? O_O
SOUser le
@XichenLi: Je ne peux pas dire. Je viens de construire la version 2.5.1 de grep (c'est assez vieux - à partir de 2006) et cela a fonctionné pour moi.
camh
@SOUser: J'ai connu la même chose - rien ne sort du fichier. J'ai soumis la demande de modification afin d'inclure '>' avant le nom du fichier pour envoyer la sortie, car cela fonctionnait pour moi.
Rjchicago
39

Grep standard ne peut pas faire cela, mais les versions récentes de GNU grep le peuvent . Vous pouvez vous tourner vers sed, awk ou perl. Voici quelques exemples qui font ce que vous voulez dans votre exemple d’entrée. ils se comportent légèrement différemment dans les cas de coin.

Remplacer foobar word other stuffpar word, n'imprimez que si un remplacement est effectué.

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

Si le premier mot est foobar, imprimez le deuxième mot.

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

Effacez-le foobarsi c'est le premier mot et sautez la ligne sinon; puis tout dépouiller après le premier espace et imprimer.

perl -lne 's/^foobar\s+// or next; s/\s.*//; print'
Gilles
la source
Impressionnant! Je pensais pouvoir le faire avec sed, mais je ne l'avais jamais utilisé auparavant et espérais pouvoir utiliser mon familier grep. Mais la syntaxe de ces commandes semble très familière maintenant que je suis familiarisé avec les fonctions de recherche et remplacement de vim, ainsi que les expressions rationnelles. Merci beaucoup.
Cory Klein
1
Ce n'est pas vrai, Gilles. Voir ma réponse pour une solution GNU grep.
camh
1
@camh: Ah, je ne savais pas que GNU grep avait maintenant une prise en charge complète de PCRE. J'ai corrigé ma réponse, merci.
Gilles
1
Cette réponse est particulièrement utile pour Linux embarqué car Busybox grepne prend pas en charge PCRE.
Craig McQueen
Évidemment, il y a plusieurs façons d'accomplir la même tâche, cependant, si le PO demande l'utilisation de grep, pourquoi répondez-vous à autre chose? En outre, votre premier paragraphe est incorrect: oui, grep peut le faire.
fcm
33
    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
la source
1
+1 pour l'exemple sed, semble être un meilleur outil pour le travail que grep. Un commentaire, le ^et $sont étrangers car .*est un match gourmand. Cependant, les inclure pourrait aider à clarifier l'intention de la regex.
Tony
18

Eh bien, si vous savez que foobar est toujours le premier mot ou la première ligne, vous pouvez utiliser couper. Ainsi:

grep "foobar" test.file | cut -d" " -f2
Dave
la source
Le -ocommutateur sur grep est largement implémenté (plus que les extensions Gnu grep), donc cela grep -o "foobar" test.file | cut -d" " -f2augmentera l'efficacité de cette solution, qui est plus portable que d'utiliser des assertions lookbehind.
dubiousjim
Je crois que vous auriez besoin de grep -o "foobar .*"ou grep -o "foobar \w+".
G-Man
9

Si PCRE n'est pas pris en charge, vous pouvez obtenir le même résultat avec deux invocations de grep. Par exemple, pour saisir le mot après foobar, procédez comme suit :

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

Ceci peut être étendu à un mot arbitraire après foobar comme ceci (avec ERE pour la lisibilité):

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

Sortie:

1

Notez que l'index iest basé sur zéro.

Thor
la source
6

pcregrepa une -ooption plus intelligente qui vous permet de choisir les groupes de capture que vous voulez générer. Donc, en utilisant votre exemple de fichier,

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

L'utilisation grepn'est pas compatible entre plates-formes, car -P/ --perl-regexpn'est disponible que sous GNUgrep , pas sous BSDgrep .

Voici la solution utilisant ripgrep:

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

Selon man rg:

-r/ --replace REPLACEMENT_TEXTRemplace chaque correspondance par le texte donné.

Les index de groupe de capture (par exemple $5) et les noms (par exemple $foo) sont pris en charge dans la chaîne de remplacement.

Connexes: GH-462 .

Kenorb
la source
2

J'ai trouvé la réponse de @jgshawkey très utile. grepsed n'est pas un bon outil pour cela, mais sed l'est, bien que nous ayons ici un exemple qui utilise grep pour saisir une ligne pertinente.

La syntaxe regex de sed est idiosyncratique si vous n’êtes pas habitué.

Voici un autre exemple: celui-ci analyse la sortie de xinput pour obtenir un entier ID

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

et je veux 19

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

Notez la syntaxe de la classe:

[[:digit:]]

et la nécessité d'échapper à ce qui suit +

Je suppose qu'une seule ligne correspond.

Tim Richardson
la source
C'est exactement ce que j'essayais de faire. Merci!
James
Version légèrement plus simple sans extra grep, en supposant que 'TouchPad' est à gauche de 'id':echo "SynPS/2 Synaptics TouchPad id=19 [slave pointer (2)]" | sed -nE "s/.*TouchPad.+id=([0-9]+).*/\1/p"
Amit Naidu