¿Cómo usar grep para buscar una línea con una de dos palabras pero no ambas?

11

Quiero buscar líneas con 'word1' XOR 'word2' en un archivo de texto. Por lo tanto, debería generar líneas con word1, word2 pero no las líneas con ambas palabras. Quería usar el XOR pero no sé cómo escribir eso en la línea de comando de Linux.

Lo intenté:

grep 'word1\|word2' text.txt
grep word1 word2 text.txt
grep word1 text.txt | grep word2
grep 'word1\^word2' text.txt

y muchos más, pero no pudo obtener el éxito.

Lukali
fuente

Respuestas:

6

grep 'word1\|word2' text.txtbusca líneas que contengan word1o word2. Esto incluye líneas que contienen ambos.

grep word1 text.txt | grep word2busca líneas que contengan word1y word2. Las dos palabras pueden superponerse (por ejemplo, foobarcontiene fooy ob). Otra forma de buscar líneas que contengan ambas palabras, pero solo de forma no superpuesta, es buscarlas en cualquier orden:grep 'word1.*word2\|word2.*word1' text.txt

grep word1 text.txt | grep -v word2busca líneas que contienen word1pero no word2. La -vopción le dice a grep que mantenga líneas no coincidentes y que elimine las líneas coincidentes, en lugar de lo contrario. Esto le da la mitad de los resultados que deseaba. Al agregar la búsqueda simétrica, obtienes todas las líneas que contienen exactamente una de las palabras.

grep word1 text.txt | grep -v word2
grep word2 text.txt | grep -v word1

Alternativamente, puede comenzar desde las líneas que contienen cualquiera de las palabras y eliminar las líneas que contienen ambas palabras. Dados los bloques de construcción anteriores, esto es fácil si las palabras no se superponen.

grep 'word1\|word2' text.txt | grep -v 'word1.*word2\|word2.*word1'
Gilles 'SO- deja de ser malvado'
fuente
Gracias, esto es exactamente lo que estaba buscando. Las otras respuestas también son muy interesantes, así que las miraré. Gracias a todos por contribuir.
Lukali
17

Con GNU awk:

$ printf '%s\n' {foo,bar}{bar,foo} neither | gawk 'xor(/foo/,/bar/)'
foofoo
barbar

O portablemente:

awk '((/foo/) + (/bar/)) % 2'

Con un grepsoporte para -P(PCRE):

grep -P '^((?=.*foo)(?!.*bar)|(?=.*bar)(?!.*foo))'

Con sed:

sed '
  /foo/{
    /bar/d
    b
  }
  /bar/!d'

Si desea considerar solo palabras completas (que no hay fooni baren foobarni barbarpor ejemplo), deberá decidir cómo se delimitan esas palabras. Si es por cualquier carácter que no sean letras, dígitos y guiones bajos, como lo hace la -wopción de muchas grepimplementaciones, entonces los cambiaría a:

gawk 'xor(/\<foo\>/,/\<bar\>/)'
awk '((/(^|[^[:alnum:]_)foo([^[:alnum:]_]|$)/) + \
      (/(^|[^[:alnum:]_)bar([^[:alnum:]_]|$)/)) % 2'
grep -P '^((?=.*\bfoo\b)(?!.*\bbar\b)|(?=.*\bbar\b)(?!.*\bfoo\b))'

Para sedeso se vuelve un poco complicado a menos que tenga una sedimplementación como GNU sed que admita \</ \>como límites de palabras como lo awkhace GNU .

Stéphane Chazelas
fuente
66
Stephane, ¡por favor escribe un libro sobre scripting de shell!
pfnuesel
Lo siento, solo comencé la línea de comandos hace unas semanas. ¿Cómo lo forzaría a buscar solo palabras? Intenté -Pw y -wP pero esto me dio un resultado incorrecto. También intenté usar '' entre * word1 / * word2 y alrededor de word1 / word2.
Lukali
@Lukali, ver edición.
Stéphane Chazelas
2

Una solución bash:

#!/bin/bash 
while (( $# )); do
    a=0 ; [[ $1 =~ foo ]] && a=1 
    b=0 ; [[ $1 =~ bar ]] && b=1
    (( a ^ b )) && echo "$1"
    shift
done

Para probarlo:

$ ./script {foo,bar}\ {foo,bar} neither
foo foo
bar bar
NotAnUnixNazi
fuente