¿Cómo busco líneas que contengan una de dos palabras, pero no ambas?

25

Estoy tratando de usar greppara mostrar solo las líneas que contienen cualquiera de las dos palabras, si solo una de ellas aparece en la línea, pero no si están en la misma línea.

Hasta ahora lo he intentado grep pattern1 | grep pattern2 | ...pero no obtuve el resultado que esperaba.

Trasmos
fuente
(1) Hablas de "palabras" y "patrones". Cual es Palabras comunes como "rápido", "marrón" y "zorro", o expresiones regulares como [a-z][a-z0-9]\(,7\}\(\.[a-z0-9]\{,3\}\)+? (2) ¿Qué sucede si una de las palabras / patrones aparece más de una vez en una línea (y la otra no aparece)? ¿Es eso equivalente a la palabra que aparece una vez, o cuenta como ocurrencias múltiples?
G-Man dice 'reinstalar a Mónica' el

Respuestas:

59

Una herramienta que no grepsea ​​el camino a seguir.

Usando perl, por ejemplo, el comando sería:

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

perl -neejecuta el comando dado sobre cada línea de stdin, que en este caso imprime la línea si coincide /pattern1/ xor /pattern2/o, en otras palabras, coincide con un patrón pero no con el otro (exclusivo o).

Esto funciona para el patrón en cualquier orden, y debería tener un mejor rendimiento que las invocaciones múltiples de grep, y también es menos tipeado.

O, incluso más corto, con awk:

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

o para versiones de awk que no tienen xor:

awk '/pattern1/+/pattern2/==1`
Chris
fuente
44
Niza: ¿el Awk está xordisponible solo en GNU Awk?
Steeldriver
99
@steeldriver Creo que es solo GNU, sí. O al menos falta en versiones anteriores. Puede reemplazarlo con /pattern1/+/pattern2/==1ir xorfalta.
Chris
44
@JimL. Podría poner límites de palabras ( \b) en los patrones mismos, es decir \bword\b.
wjandrea
44
@vikingsteve Si específicamente quieres usar grep, hay muchas otras respuestas aquí. Pero para las personas que solo quieren hacer el trabajo, es bueno saber que hay otras herramientas que pueden hacer todo lo que hace grep, pero cada vez más fácilmente.
Chris
3
@vikingsteve Supongo que la demanda de una solución grep es una especie de problema XY
Hagen von Eitzen
30

Con GNU grep, puede pasar ambas palabras grepy luego eliminar las líneas que contienen ambos patrones.

$ 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
Haxiel
fuente
16

Probar con egrep

egrep  'pattern1|pattern2' file | grep -v -e 'pattern1.*pattern2' -e 'pattern2.*pattern1'
msp9011
fuente
3
también se puede escribir comogrep -e foo -e bar | grep -v -e 'foo.*bar' -e 'bar.*foo'
glenn jackman
8
Además, nota de la página man grep: Direct invocation as either egrep or fgrep is deprecated- prefierogrep -E
glenn jackman
Eso no está en mi sistema operativo @glennjackman
Grump
1
@Grump realmente? ¿Qué sistema operativo es ese? Incluso POSIX menciona que grep debería tener -fy -eopciones, aunque las más antiguas egrepy fgrepcontinuarán siendo compatibles durante un tiempo.
terdon
1
@terdon, POSIX no especifica la ruta de las utilidades POSIX. Una vez más, de ahí, el estándar grep(que soporta -F, -E, -e, -fcomo requiere POSIX) está en /usr/xpg4/bin. Las utilidades en /binson anticuadas.
Stéphane Chazelas
12

Con grepimplementaciones que admiten expresiones regulares tipo perl (like pcregrepo GNU o ast-open grep -P), puede hacerlo en una grepinvocación con:

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

Es decir, encontrar las líneas que coinciden pat1pero no pat2, o pat2no pat1.

(?=...)y (?!...)son respectivamente operadores de anticipación y de anticipación negativos. Entonces, técnicamente, lo anterior busca el comienzo del sujeto ( ^) siempre que sea seguido .*pat1y no seguido .*pat2, o lo mismo con pat1e pat2invertido.

Eso es subóptimo para las líneas que contienen ambos patrones, ya que luego se buscarían dos veces. En su lugar, podría usar operadores perl más avanzados como:

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

(?(1)yespattern|nopattern)coincide con yespatternsi el grupo de captura 1st (vacío ()arriba) coincide, y de lo nopatterncontrario. Si esa ()partidos, eso significa que pat1no coinciden, por lo que buscamos pat2(aspecto positivo por delante), y buscamos no pat2 de otro modo (por delante aspecto negativo).

Con sed, podrías escribirlo:

sed -ne '/pat1/{/pat2/!p;d;}' -e '/pat2/p'
Stéphane Chazelas
fuente
Su primera solución falla grep: the -P option only supports a single pattern, al menos en todos los sistemas a los que tengo acceso. Sin embargo, +1 para tu segunda solución.
Chris
1
@ Chris, tienes razón. Eso parece ser una limitación específica de GNU grep. pcregrepy grep ast-open no tienen ese problema. He reemplazado el múltiplo -econ el operador de alternancia RE, por lo que ahora debería funcionar con GNU greptambién.
Stéphane Chazelas
Sí, funciona bien ahora.
Chris
3

En términos booleanos, está buscando A xor B, que se puede escribir como

(A y no B)

o

(B y no A)

Dado que su pregunta no menciona que le preocupa el orden de la salida siempre que se muestren las líneas coincidentes, la expansión booleana de A xor B es bastante simple en 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
Jim L.
fuente
1
Esto funciona, pero codificará el orden del archivo.
Gavilán
@Sparhawk Cierto, aunque "revolver" es una palabra dura. ;) enumera todas las coincidencias 'a' primero, en orden, luego todas las coincidencias 'b' a continuación, en orden. El OP no expresó ningún interés en mantener el orden, solo mostró las líneas. FAWK, el siguiente paso podría ser sort | uniq.
Jim L.
Llamada justa; Estoy de acuerdo en que mi idioma era inexacto. Quise decir que el orden original sería cambiado.
Gavilán
1
@Sparhawk ... Y edité en su observación para una divulgación completa.
Jim L.
-2

Para el siguiente ejemplo:

# Patterns:
#    apple
#    pear

# Example line
line="a_apple_apple_pear_a"

Esto se puede hacer simplemente con grep -E, uniqy wc.

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

Si grepse compila con expresiones regulares de Perl, puede coincidir en la última aparición en lugar de necesitar canalizar a uniq:

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

Salida del 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

Una frase:

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

Si no desea codificar el patrón, ensamblarlo con un conjunto variable de elementos puede automatizarse con una función.

Esto también se puede hacer de forma nativa en Bash como una función sin canalizaciones o procesos adicionales, pero sería más complicado y probablemente esté fuera del alcance de su pregunta.

Zhro
fuente
(1) Me preguntaba cuándo alguien respondería usando expresiones regulares de Perl. Si te enfocaste en esa parte de tu publicación y explicaste cómo funcionaba, esta podría ser una buena respuesta. (2) Pero me temo que el resto no es tan bueno. La pregunta dice "mostrar solo líneas que contengan cualquiera de las dos palabras" (énfasis agregado). Si se supone que la salida son líneas , entonces es lógico pensar que la entrada también debe ser varias líneas.   Pero su enfoque solo funciona cuando mira una sola línea. ... (Continúa)
G-Man dice 'Restablecer a Mónica'
(Cont.) ... Por ejemplo, si la entrada contiene las líneas Big apple\ny pear-shaped\n, entonces la salida debe contener ambas líneas. Su solución obtendría una cuenta de 2; la versión larga informaría "Ambas palabras coinciden" (que es una respuesta a la pregunta incorrecta) y la versión corta no diría nada en absoluto. (3) Una sugerencia: usar -oaquí es una muy mala idea, ya que oculta las líneas que contienen las coincidencias, por lo que no puede ver cuándo ambas palabras aparecen en la misma línea. ... (Continúa)
G-Man dice 'Restablecer a Mónica'
(Cont.) ... (4) Línea inferior: su uso de uniq/ sort -uy la elegante expresión regular de Perl para que coincida solo con la última aparición en cada línea realmente no se suman a una respuesta útil a esta pregunta. Pero, incluso si lo hicieran, sería una mala respuesta porque no explicas cómo contribuyen a responder la pregunta. (Ver la respuesta de Stéphane Chazelas para un ejemplo de una buena explicación.)
G-Man dice 'Restablecer a Monica'
El OP dice que querían "mostrar solo líneas que contengan cualquiera de las dos palabras", lo que significa que cada línea debe evaluarse por sí sola. No veo por qué sientes que esto no responde la pregunta. Proporcione una entrada de ejemplo que considere que fallará.
Zhro
Oh, ¿ eso es lo que quisiste decir? “Lea la entrada de una línea a la vez y ejecute estos dos o tres comandos para cada línea . "? (1) Es dolorosamente claro que eso es lo que quisiste decir. (2) Es dolorosamente ineficiente. Cuatro respuestas antes de la suya mostraron cómo manejar todo el archivo en unos pocos comandos (uno, dos o cuatro), y ¿desea ejecutar 3 ×  n comandos para n líneas de entrada? Incluso si funciona, gana un voto negativo por una ejecución innecesariamente costosa. (3) A riesgo de dividir los pelos, todavía no hace el trabajo de mostrar las líneas apropiadas.
G-Man dice 'reinstalar a Monica'