¿Puede grep generar solo agrupaciones específicas que coincidan?

293

Digamos que tengo un archivo:

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

Solo quiero saber qué palabras aparecen después de "foobar", para poder usar esta expresión regular:

"foobar \(\w\+\)"

El paréntesis indica que tengo un interés especial en la palabra justo después de foobar. Pero cuando hago un grep "foobar \(\w\+\)" test.txt, obtengo las líneas completas que coinciden con la expresión regular completa, en lugar de solo "la palabra después de foobar":

foobar bash 1
foobar happy

Preferiría mucho que la salida de ese comando se viera así:

bash
happy

¿Hay alguna manera de decirle a grep que solo muestre los elementos que coinciden con la agrupación (o una agrupación específica) en una expresión regular?

Cory Klein
fuente
44
para aquellos que no necesitan grep:perl -lne 'print $1 if /foobar (\w+)/' < test.txt
bóveda

Respuestas:

327

GNU grep tiene la -Popción de expresiones regulares de estilo perl, y la -oopción de imprimir solo lo que coincida con el patrón. Estos se pueden combinar usando aserciones de mirar alrededor (descritas en Patrones extendidos en la página de manual de perlre ) para eliminar parte del patrón grep de lo que se determina que coincide con el propósito de -o.

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

Esta \Kes la forma abreviada (y una forma más eficiente) (?<=pattern)que utiliza como una afirmación de retrospectiva de ancho cero antes del texto que desea generar. (?=pattern)se puede usar como una aserción anticipada de ancho cero después del texto que desea generar.

Por ejemplo, si desea hacer coincidir la palabra entre fooy bar, puede usar:

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

o (por simetría)

$ grep -oP '(?<=foo )\w+(?= bar)' test.txt
camh
fuente
3
¿Cómo lo haces si tu expresión regular tiene más de una agrupación? (como lo
indica
44
@barracel: No creo que puedas. Tiempo parased(1)
camh
1
@camh Acabo de probar que grep -oP 'foobar \K\w+' test.txtno genera nada con los OP test.txt. La versión grep es 2.5.1. Qué podría estar mal ? O_O
SOUser
@XichenLi: No puedo decir. Acabo de construir v2.5.1 de grep (es bastante antiguo, de 2006) y funcionó para mí.
camh
@SOUser: Experimenté lo mismo: no genera nada para archivar. Envié la solicitud de edición para incluir '>' antes del nombre del archivo para enviar la salida, ya que esto funcionó para mí.
rjchicago
39

Grep estándar no puede hacer esto, pero las versiones recientes de GNU grep sí . Puedes recurrir a sed, awk o perl. Aquí hay algunos ejemplos que hacen lo que desea en su entrada de muestra; se comportan de manera ligeramente diferente en los casos de esquina.

Reemplace foobar word other stuffpor word, imprima solo si se realiza un reemplazo.

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

Si la primera palabra es foobar, imprima la segunda palabra.

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

Tira foobarsi es la primera palabra, y salta la línea de lo contrario; luego elimine todo después del primer espacio en blanco e imprima.

perl -lne 's/^foobar\s+// or next; s/\s.*//; print'
Gilles
fuente
¡Increíble! Pensé que podría hacer esto con sed, pero no lo he usado antes y esperaba poder usar mi familiar grep. Pero la sintaxis para estos comandos en realidad parece muy familiar ahora que estoy familiarizado con la búsqueda y reemplazo + expresiones regulares de estilo vim. Gracias una tonelada.
Cory Klein
1
No es cierto, Gilles. Vea mi respuesta para una solución grep de GNU.
camh
1
@camh: Ah, no sabía que GNU grep ahora tenía soporte completo para PCRE. He corregido mi respuesta, gracias.
Gilles
1
Esta respuesta es especialmente útil para Linux embebido ya que Busybox grepno tiene soporte PCRE.
Craig McQueen
Obviamente, hay varias formas de lograr la misma tarea presentada, sin embargo, si el OP solicita el uso de grep, ¿por qué responde algo más? Además, su primer párrafo es incorrecto: sí, grep puede hacerlo.
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
fuente
1
+1 para el ejemplo sed, parece una mejor herramienta para el trabajo que grep. Un comentario, ^y $son extraños ya que .*es un partido codicioso. Sin embargo, incluirlos podría ayudar a aclarar la intención de la expresión regular.
Tony
18

Bueno, si sabes que foobar es siempre la primera palabra o la línea, entonces puedes usar cortar. Al igual que:

grep "foobar" test.file | cut -d" " -f2
Dave
fuente
El -ocambio en grep está ampliamente implementado (más que las extensiones grep de Gnu), por lo grep -o "foobar" test.file | cut -d" " -f2que aumentará la efectividad de esta solución, que es más portátil que el uso de afirmaciones retrospectivas.
dubiousjim
Creo que necesitarías grep -o "foobar .*"o grep -o "foobar \w+".
G-Man
9

Si PCRE no es compatible, puede lograr el mismo resultado con dos invocaciones de grep. Por ejemplo, para agarrar la palabra después de foobar, haga esto:

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

Esto se puede expandir a una palabra arbitraria después de foobar como esta (con ERE para facilitar la lectura):

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

Salida:

1

Tenga en cuenta que el índice iestá basado en cero.

Thor
fuente
6

pcregreptiene una -oopción más inteligente que le permite elegir qué grupos de captura desea obtener. Entonces, usando su archivo de ejemplo,

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

El uso grepno es compatible con plataformas cruzadas, ya que -P/ --perl-regexpsolo está disponible en GNUgrep , no en BSDgrep .

Aquí está la solución usando ripgrep:

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

Según man rg:

-r/ --replace REPLACEMENT_TEXTReemplazar cada partido con el texto dado.

Los índices de grupo de captura (p. Ej. $5) Y los nombres (p. Ej. $foo) Se admiten en la cadena de reemplazo.

Relacionado: GH-462 .

kenorb
fuente
2

La respuesta de @jgshawkey me pareció muy útil. grepno es una herramienta tan buena para esto, pero sed lo es, aunque aquí tenemos un ejemplo que usa grep para tomar una línea relevante.

La sintaxis de expresiones regulares de sed es idiosincrásica si no está acostumbrado.

Aquí hay otro ejemplo: este analiza la salida de xinput para obtener un número entero ID

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

y quiero 19

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

Tenga en cuenta la sintaxis de la clase:

[[:digit:]]

y la necesidad de escapar de lo siguiente +

Supongo que solo una línea coincide.

Tim Richardson
fuente
Esto es exactamente lo que estaba tratando de hacer. ¡Gracias!
James
Versión ligeramente más simple sin el extra grep, asumiendo que 'TouchPad' está a la izquierda de 'id':echo "SynPS/2 Synaptics TouchPad id=19 [slave pointer (2)]" | sed -nE "s/.*TouchPad.+id=([0-9]+).*/\1/p"
Amit Naidu