Como faço para grep para linhas contendo uma das duas palavras, mas não as duas?


25

Estou tentando usar greppara mostrar apenas linhas contendo uma das duas palavras, se apenas uma delas aparecer na linha, mas não se elas estiverem na mesma linha.

Até agora, tentei, grep pattern1 | grep pattern2 | ...mas não obtive o resultado esperado.


(1) Você fala sobre "palavras" e "padrões". Qual e? Palavras comuns como "rápido", "marrom" e "raposa" ou expressões regulares como [a-z][a-z0-9]\(,7\}\(\.[a-z0-9]\{,3\}\)+? (2) E se uma das palavras / padrões aparecer mais de uma vez em uma linha (e a outra não aparecer)? Isso é equivalente à palavra que aparece uma vez ou conta como várias ocorrências?
G-Man diz 'Reinstate Monica'

Respostas:


59

Uma ferramenta que não grepé o caminho a percorrer.

Usando perl, por exemplo, o comando seria:

perl -ne 'print if /pattern1/ xor /pattern2/'

perl -neexecuta o comando fornecido sobre cada linha de stdin, que nesse caso imprime a linha se corresponder /pattern1/ xor /pattern2/ou, em outras palavras, corresponde a um padrão, mas não ao outro (exclusivo ou).

Isso funciona para o padrão em qualquer ordem e deve ter um desempenho melhor do que várias invocações grepe também é menos digitado.

Ou, ainda mais curto, com awk:

awk 'xor(/pattern1/,/pattern2/)'

ou para versões do awk que não possuem xor:

awk '/pattern1/+/pattern2/==1`

4
Nice - o Awk está xordisponível apenas no GNU Awk?
steeldriver 30/01

9
@steeldriver Eu acho que é apenas o GNU, sim. Ou pelo menos está faltando em versões mais antigas. Você pode substituí-lo por /pattern1/+/pattern2/==1ir xorestá ausente.
Chris

4
@JimL. Você pode colocar limites de palavras ( \b) nos próprios padrões, ie \bword\b.
wjandrea 30/01

4
@vikingsteve Se você deseja especificamente usar o grep, há muitas outras respostas aqui. Mas para as pessoas que querem apenas fazer o trabalho, é bom saber que existem outras ferramentas que podem fazer tudo o que o grep faz, mas de maneira cada vez mais fácil.
Chris

3
@vikingsteve Eu suponho fortemente que a demanda por uma solução grep é um tipo de problema XY
Hagen von Eitzen

30

Com o GNU grep, você pode passar as duas palavras grepe remover as linhas que contêm os dois padrões.

$ cat testfile.txt
abc
def
abc def
abc 123 def
1234
5678
1234 def abc
def abc

$ grep -w -e 'abc' -e 'def' testfile.txt | grep -v -e 'abc.*def' -e 'def.*abc'
abc
def

16

Tente com egrep

egrep  'pattern1|pattern2' file | grep -v -e 'pattern1.*pattern2' -e 'pattern2.*pattern1'

3
também pode ser escrito comogrep -e foo -e bar | grep -v -e 'foo.*bar' -e 'bar.*foo'
glenn jackman 30/01

8
Além disso, observe a página de manual do grep: Direct invocation as either egrep or fgrep is deprecated- prefiragrep -E
glenn jackman

Isso não está no meu sistema operacional @glennjackman
Grump

1
@Grump realmente? Que sistema operacional é esse? Até o POSIX menciona que o grep deve ter -fe -eopções, embora seja antigo egrepe fgrepcontinuará sendo suportado por um tempo.
terdon

1
@terdon, POSIX não especifica o caminho dos utilitários POSIX. Mais uma vez, lá, o padrão grep(ou suportes -F, -E, -e, -fcomo exige POSIX) é em /usr/xpg4/bin. Os utilitários /binsão antiquados.
Stéphane Chazelas

12

Com grepimplementações que suportam expressões regulares do tipo perl (como pcregrepou GNU ou ast-open grep -P), você pode fazer isso em uma grepchamada com:

grep -P '^(?=.*pat1)(?!.*pat2)|^(?=.*pat2)(?!.*pat1)'

Ou seja, encontrar as linhas que correspondem, pat1mas não pat2, ou pat2mas não pat1.

(?=...)e, (?!...)respectivamente, são operadores de antecipação e negativo. Portanto, tecnicamente, o item acima procura o início do assunto ( ^), desde que seja seguido .*pat1e não seguido por .*pat2, ou o mesmo com pat1e pat2invertido.

Isso é subótimo para linhas que contêm os dois padrões, pois seriam procurados duas vezes. Você poderia usar operadores perl mais avançados, como:

grep -P '^(?=.*pat1|())(?(1)(?=.*pat2)|(?!.*pat2))'

(?(1)yespattern|nopattern)corresponde contra yespatternse o grupo de captura 1st (vazio ()acima) corresponder, ou nopatternnão. Se esse ()partidas, isso significa que pat1não se encontraram, então olhamos para pat2(olhar positivo à frente), e nós olhamos para não pat2 o contrário (em frente olhar negativo).

Com sed, você pode escrever:

sed -ne '/pat1/{/pat2/!p;d;}' -e '/pat2/p'

Sua primeira solução falha grep: the -P option only supports a single pattern, pelo menos em todos os sistemas aos quais tenho acesso. +1 para sua segunda solução, no entanto.
Chris

1
@ Chris, você está certo. Essa parece ser uma limitação específica ao GNU grep. pcregrepe o ast-open grep não tem esse problema. Substituí o múltiplo -epelo operador RE alternativo, portanto, ele deve funcionar com o GNU greptambém agora.
Stéphane Chazelas 31/01

Sim, funciona bem agora.
Chris

3

Em termos booleanos, você está procurando A xor B, que pode ser escrito como

(A e não B)

ou

(B e não A)

Dado que sua pergunta não menciona que você está preocupado com a ordem da saída, desde que as linhas correspondentes sejam mostradas, a expansão booleana de A xor B é bastante simples no grep:

$ cat << EOF > foo
> a b
> a
> b
> c a
> c b
> b a
> b c
> EOF
$ grep -w 'a' foo | grep -vw 'b'; grep -w 'b' foo | grep -vw 'a';
a
c a
b
c b
b c

1
Isso funciona, mas irá embaralhar a ordem do arquivo.
Sparhawk

@ Sparhawk True, embora "embaralhar" seja uma palavra dura. ;) lista todas as correspondências 'a' primeiro, em ordem, depois todas as correspondências 'b' a seguir, em ordem. O OP não manifestou interesse em manter a ordem, apenas mostre as linhas. FAWK, o próximo passo poderia ser sort | uniq.
Jim L.

Chamada justa; Concordo que meu idioma era impreciso. Eu pretendia sugerir que a ordem original seria alterada.
Sparhawk

1
@ Sparhawk ... E eu editei em sua observação para divulgação completa.
Jim L.

-2

Para o seguinte exemplo:

# Patterns:
#    apple
#    pear

# Example line
line="a_apple_apple_pear_a"

Isso pode ser feito puramente com grep -E, uniqe wc.

# Grep for regex pattern, sort as unique, and count the number of lines
result=$(grep -oE 'apple|pear' <<< $line | sort -u | wc -l)

Se grepfor compilado com expressões regulares do Perl, você poderá corresponder na última ocorrência, em vez de precisar canalizar para uniq:

# Grep for regex pattern and count the number of lines
result=$(grep -oP '(apple(?!.*apple)|pear(?!.*pear))' <<< $line | wc -l)

Saída do resultado:

# Only one of the words exists if the result is < 2
((result > 0)) &&
   if (($result < 2)); then
      echo Only one word matched
   else
      echo Both words matched
   fi

Uma linha:

(($(grep -oP '(apple(?!.*apple)|pear(?!.*pear))' <<< $line | wc -l) == 1)) && echo Only one word matched

Se você não deseja codificar o padrão, montá-lo com um conjunto variável de elementos pode ser automatizado com uma função.

Isso também pode ser feito de forma nativa no Bash como uma função sem canais ou processos adicionais, mas estaria mais envolvido e provavelmente está fora do escopo da sua pergunta.


(1) Fiquei me perguntando quando alguém iria dar uma resposta usando expressões regulares do Perl. Se você se concentrou nessa parte da sua postagem e explicou como ela funcionava, essa poderia ser uma boa resposta. (2) Mas receio que o resto não seja tão bom. A pergunta diz: "mostre apenas linhas que contenham uma das duas palavras" (grifo nosso). Se a saída deve ser linhas , é lógico que a entrada também deve ser várias linhas.   Mas sua abordagem funciona apenas quando se olha apenas uma única linha. … (Continua)
G-Man diz 'Reinstate Monica'

(Continua)… Por exemplo, se a entrada contiver as linhas Big apple\ne pear-shaped\n, a saída deverá conter as duas linhas. Sua solução receberá uma contagem de 2; a versão longa informaria "Ambas as palavras correspondiam" (que é uma resposta à pergunta errada) e a versão curta não dizia nada. (3) Uma sugestão: usar -oaqui é uma péssima idéia, porque oculta as linhas que contêm as correspondências, portanto você não pode ver quando as duas palavras aparecem na mesma linha. … (Continua)
G-Man diz 'Reinstate Monica'

(Continua) ... (4) Conclusão: seu uso de uniq/ sort -ue a expressão regular sofisticada de Perl para corresponder apenas à última ocorrência em cada linha não são realmente uma resposta útil para esta pergunta. Mas, mesmo se o fizessem, ainda seria uma resposta ruim, porque você não explica como eles contribuem para responder à pergunta. (Veja a resposta de Stéphane Chazelas para um exemplo de uma boa explicação.)
G-Man diz 'Reinstate Monica'

O OP diz que eles queriam "mostrar apenas linhas contendo uma das duas palavras", o que significa que cada linha deve ser avaliada por si própria. Não vejo por que você acha que isso não responde à pergunta. Forneça um exemplo de entrada que você acha que falharia.
Zhro 02/02

Oh, é isso que você quis dizer? “Leia a entrada uma linha de cada vez e execute estes dois ou três comandos para cada linha . "? (1) É dolorosamente claro que foi isso que você quis dizer. (2) É dolorosamente ineficiente. Quatro respostas anteriores à sua mostraram como lidar com o arquivo inteiro em alguns comandos (um, dois ou quatro) e você deseja executar comandos 3x ×  n para n linhas de entrada? Mesmo que funcione, ele recebe um voto negativo por execução desnecessariamente cara. (3) Correndo o risco de partir os cabelos, ele ainda não faz o trabalho de mostrar as linhas apropriadas.
G-Man Diz 'Reinstate Monica'
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.