O grep pode gerar apenas grupos especificados correspondentes?

293

Digamos que eu tenho um arquivo:

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

Eu só quero saber quais palavras aparecem depois de "foobar", para que eu possa usar este regex:

"foobar \(\w\+\)"

Os parênteses indicam que tenho um interesse especial pela palavra logo após foobar. Mas quando eu faço a grep "foobar \(\w\+\)" test.txt, recebo as linhas inteiras que correspondem a toda a expressão regular, em vez de apenas "a palavra após foobar":

foobar bash 1
foobar happy

Eu preferiria muito que a saída desse comando fosse assim:

bash
happy

Existe uma maneira de dizer ao grep para emitir apenas os itens que correspondem ao agrupamento (ou a um agrupamento específico) em uma expressão regular?

Cory Klein
fonte
4
para quem não precisa de grep:perl -lne 'print $1 if /foobar (\w+)/' < test.txt
vault

Respostas:

327

O GNU grep tem a -Popção de expressões regulares no estilo perl e a -oopção de imprimir apenas o que corresponde ao padrão. Eles podem ser combinados usando asserções de pesquisa (descritas em Padrões estendidos na página de manual do perlre ) para remover parte do padrão grep do que é determinado ter correspondido para os fins de -o.

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

O \Ké o formato abreviado (e mais eficiente) (?<=pattern)que você usa como uma declaração de look-behind de largura zero antes do texto que deseja produzir. (?=pattern)pode ser usado como uma afirmação antecipada de largura zero após o texto que você deseja gerar.

Por exemplo, se você deseja combinar a palavra entre fooe bar, você pode usar:

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

ou (por simetria)

$ grep -oP '(?<=foo )\w+(?= bar)' test.txt
camh
fonte
3
Como você faz isso se o seu regex tem mais do que um agrupamento? (como o título implícito?)
barracel
4
@ barracel: Eu não acredito que você pode. Hora desed(1)
camh 22/03/2013
1
@camh Acabei de testar que grep -oP 'foobar \K\w+' test.txtnada produz com os OP's test.txt. A versão grep é 2.5.1. O que pode estar errado ? O_O
SOUser
@XichenLi: Eu não posso dizer. Acabei de criar a v2.5.1 do grep (é bem antiga - desde 2006) e funcionou para mim.
Camh
@ SOUser: Eu experimentei o mesmo - não produz nada para arquivar. Enviei a solicitação de edição para incluir '>' antes do nome do arquivo para enviar a saída, pois isso funcionou para mim.
Rjchicago
39

O grep padrão não pode fazer isso, mas as versões recentes do GNU grep podem . Você pode mudar para sed, awk ou perl. Aqui estão alguns exemplos que fazem o que você deseja na sua entrada de amostra; eles se comportam de maneira ligeiramente diferente nos cantos.

Substitua foobar word other stuffpor word, imprima apenas se uma substituição for concluída.

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

Se a primeira palavra for foobar, imprima a segunda palavra.

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

Retire foobarse for a primeira palavra e pule a linha caso contrário; depois retire tudo após o primeiro espaço em branco e imprima.

perl -lne 's/^foobar\s+// or next; s/\s.*//; print'
Gilles
fonte
Impressionante! Eu pensei que poderia fazer isso com o sed, mas não o usei antes e esperava poder usar meu familiar grep. Mas a sintaxe para esses comandos realmente parece muito familiar agora que estou familiarizado com a pesquisa e substituição de expressões no estilo vim. Muito obrigado.
Cory Klein
1
Não é verdade, Gilles. Veja minha resposta para uma solução GNU grep.
Camh
1
@camh: Ah, eu não sabia que o GNU grep agora tinha suporte total ao PCRE. Corrigi minha resposta, obrigado.
Gilles
1
Esta resposta é especialmente útil para Linux embarcado, pois o Busybox grepnão possui suporte para PCRE.
Craig McQueen
Obviamente, existem várias maneiras de realizar a mesma tarefa apresentada; no entanto, se o OP solicitar o uso de grep, por que você responde alguma outra coisa? Além disso, seu primeiro parágrafo está incorreto: sim, o grep pode fazer isso.
fcm 11/03
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
fonte
1
+1 no exemplo sed, parece ser uma ferramenta melhor para o trabalho do que o grep. Um comentário, ^e $são estranhos, pois .*é uma combinação gananciosa. No entanto, incluí-los pode ajudar a esclarecer a intenção da regex.
Tony
18

Bem, se você souber que foobar é sempre a primeira palavra ou a linha, use cortada. Igual a:

grep "foobar" test.file | cut -d" " -f2
Dave
fonte
A -oativação do grep é amplamente implementada (mais do que as extensões grep do Gnu), o grep -o "foobar" test.file | cut -d" " -f2que aumentará a eficácia dessa solução, que é mais portátil do que usar asserções ocultas.
precisa
Eu acredito que você precisa grep -o "foobar .*"ou grep -o "foobar \w+".
G-Man
9

Se o PCRE não for suportado, você poderá obter o mesmo resultado com duas invocações do grep. Por exemplo, para pegar a palavra após foobar, faça o seguinte:

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

Isso pode ser expandido para uma palavra arbitrária após foobar como este (com EREs para facilitar a leitura):

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

Resultado:

1

Observe que o índice ié baseado em zero.

Thor
fonte
6

pcregreppossui uma -oopção mais inteligente que permite escolher quais grupos de captura você deseja exibir. Então, usando seu arquivo de exemplo,

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

O uso grepnão é compatível com várias plataformas, pois -P/ --perl-regexpestá disponível apenas no GNUgrep , não no BSDgrep .

Aqui está a solução usando ripgrep:

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

Conforme man rg:

-r/ --replace REPLACEMENT_TEXTSubstitua todas as correspondências pelo texto fornecido.

Os índices do grupo de captura (por exemplo, $5) e os nomes (por exemplo $foo) são suportados na sequência de substituição.

Palavras-chave : GH-462 .

kenorb
fonte
2

Achei a resposta de @jgshawkey muito útil. grepnão é uma ferramenta tão boa para isso, mas sed é, embora aqui tenhamos um exemplo que usa grep para pegar uma linha relevante.

A sintaxe da regex do sed é idiossincrática se você não estiver acostumado.

Aqui está outro exemplo: este analisa a saída do xinput para obter um número inteiro de ID

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

e eu quero 19

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

Observe a sintaxe da classe:

[[:digit:]]

e a necessidade de escapar do seguinte +

Presumo que apenas uma linha corresponda.

Tim Richardson
fonte
Isto é exatamente o que eu estava tentando fazer. Obrigado!
James
Versão ligeiramente mais simples sem o extra grep, assumindo que 'TouchPad' está à esquerda de 'id':echo "SynPS/2 Synaptics TouchPad id=19 [slave pointer (2)]" | sed -nE "s/.*TouchPad.+id=([0-9]+).*/\1/p"
Amit Naidu