Comment puis-je rechercher des lignes contenant l'un des deux mots, mais pas les deux?

25

J'essaie d'utiliser greppour afficher uniquement les lignes contenant l'un des deux mots, si un seul d'entre eux apparaît dans la ligne, mais pas s'ils sont sur la même ligne.

Jusqu'à présent, j'ai essayé, grep pattern1 | grep pattern2 | ...mais je n'ai pas obtenu le résultat escompté.

Trasmos
la source
(1) Vous parlez de «mots» et de «schémas». Lequel est-ce? Des mots ordinaires comme «rapide», «brun» et «renard», ou des expressions régulières comme [a-z][a-z0-9]\(,7\}\(\.[a-z0-9]\{,3\}\)+? (2) Que se passe-t-il si l'un des mots / motifs apparaît plus d'une fois sur une ligne (et que l'autre n'apparaît pas)? Est-ce l'équivalent du mot apparaissant une fois, ou cela compte-t-il comme plusieurs occurrences?
G-Man dit «Réinstalle Monica» le

Réponses:

59

Un outil autre que greple chemin à parcourir.

En utilisant perl, par exemple, la commande serait:

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

perl -neexécute la commande donnée sur chaque ligne de stdin, qui dans ce cas imprime la ligne si elle correspond /pattern1/ xor /pattern2/, ou en d'autres termes correspond à un modèle mais pas à l'autre (exclusif ou).

Cela fonctionne pour le modèle dans l'un ou l'autre ordre, et devrait avoir de meilleures performances que les appels multiples de grep, et est également moins typé.

Ou, encore plus court, avec awk:

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

ou pour les versions d'awk qui n'ont pas xor:

awk '/pattern1/+/pattern2/==1`
Chris
la source
4
Bien - l'Awk est-il xordisponible uniquement dans GNU Awk?
Steeldriver
9
@steeldriver Je pense que c'est uniquement GNU, oui. Ou du moins, il manque sur les anciennes versions. Vous pouvez le remplacer par /pattern1/+/pattern2/==1ir xormanquant.
Chris
4
@JimL. Vous pouvez mettre des limites de mots ( \b) dans les motifs eux-mêmes, c'est-à-dire \bword\b.
wjandrea
4
@vikingsteve Si vous souhaitez spécifiquement utiliser grep, il existe de nombreuses autres réponses ici. Mais pour les gens qui veulent juste faire le travail, il est bon de savoir qu'il existe d'autres outils qui peuvent faire tout ce que fait grep, mais de plus en plus facilement.
Chris
3
@vikingsteve Je suppose fortement que la demande d'une solution grep est une sorte de problème XY
Hagen von Eitzen
30

Avec GNU grep, vous pouvez passer les deux mots à grep, puis supprimer les lignes contenant les deux motifs.

$ 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
la source
16

Essayez avec egrep

egrep  'pattern1|pattern2' file | grep -v -e 'pattern1.*pattern2' -e 'pattern2.*pattern1'
msp9011
la source
3
peut aussi s'écriregrep -e foo -e bar | grep -v -e 'foo.*bar' -e 'bar.*foo'
glenn jackman
8
Notez également sur la page de manuel de grep: Direct invocation as either egrep or fgrep is deprecated- prefergrep -E
glenn jackman
Ce n'est pas dans mon OS @glennjackman
Grump
1
@Grump vraiment? Quel OS est-ce? Même POSIX mentionne que grep devrait avoir -fet des -eoptions bien que les plus anciennes egrepet fgrepcontinueront d'être prises en charge pendant un certain temps.
terdon
1
@terdon, POSIX ne spécifie pas le chemin des utilitaires POSIX. Encore une fois, là, la norme grep(qui supporte -F, -E, -e, -fcomme POSIX exige) est /usr/xpg4/bin. Les utilitaires dans /binsont obsolètes.
Stéphane Chazelas
12

Avec des grepimplémentations qui prennent en charge les expressions régulières de type perl (comme pcregrepou GNU ou ast-opengrep -P ), vous pouvez le faire en une seule grepinvocation avec:

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

C'est-à-dire trouver les lignes qui correspondent pat1mais paspat2 ou pat2pas pat1.

(?=...)et (?!...)sont respectivement des opérateurs d'anticipation et d'anticipation négative. Donc, techniquement, ce qui précède cherche le début du sujet ( ^) à condition qu'il soit suivi par.*pat1 et non suivi .*pat2, ou le même avec pat1et pat2inversé.

Ce n'est pas optimal pour les lignes qui contiennent les deux modèles car ils seraient ensuite recherchés deux fois. Vous pouvez utiliser à la place des opérateurs Perl plus avancés comme:

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

(?(1)yespattern|nopattern)correspond à yespatternsi le groupe de capture 1st (vide ()ci-dessus) correspond, et nopatternsinon. Si cela ()correspond, cela signifie pat1ne correspond pas, alors nous recherchonspat2 (regard positif devant), et nous ne cherchons pas pat2 autrement (regard négatif devant).

Avec sed, vous pourriez l'écrire:

sed -ne '/pat1/{/pat2/!p;d;}' -e '/pat2/p'
Stéphane Chazelas
la source
Votre première solution échoue avec grep: the -P option only supports a single pattern, au moins sur tous les systèmes auxquels j'ai accès. +1 pour votre deuxième solution, cependant.
Chris
1
@Chris, vous avez raison. Cela semble être une limitation spécifique à GNU grep. pcregrepet grep ast-open n'ont pas ce problème. J'ai remplacé le multiple -epar l'opérateur RE d'alternance, donc cela devrait aussi fonctionner avec GNU grep.
Stéphane Chazelas
Oui, ça fonctionne bien maintenant.
Chris
3

En termes booléens, vous recherchez A xor B, qui peut s'écrire

(A et non B)

ou

(B et non A)

Étant donné que votre question ne mentionne pas que vous vous souciez de l'ordre de sortie tant que les lignes correspondantes sont affichées, l'expansion booléenne de A xor B est assez 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.
la source
1
Cela fonctionne, mais il va brouiller l'ordre du fichier.
Sparhawk
@Sparhawk Vrai, bien que "brouiller" soit un mot dur. ;) il répertorie toutes les correspondances «a» en premier, dans l'ordre, puis toutes les correspondances «b» dans l'ordre. Le PO n'a manifesté aucun intérêt à maintenir l'ordre, il suffit de montrer les lignes. FAWK, la prochaine étape pourrait être sort | uniq.
Jim L.
Appel juste; J'accepte que ma langue était inexacte. Je voulais dire que la commande d'origine serait modifiée.
Sparhawk
1
@Sparhawk ... Et j'ai modifié votre observation pour une divulgation complète.
Jim L.
-2

Pour l'exemple suivant:

# Patterns:
#    apple
#    pear

# Example line
line="a_apple_apple_pear_a"

Cela peut être fait uniquement avec grep -E, uniqet 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 grep est compilé avec des expressions régulières Perl, vous pouvez faire correspondre la dernière occurrence au lieu de devoir diriger vers uniq:

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

Sortez le résultat:

# 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

Un one-liner:

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

Si vous ne voulez pas coder en dur le modèle, son assemblage avec un ensemble variable d'éléments peut être automatisé avec une fonction.

Cela peut également être fait nativement dans Bash en tant que fonction sans canaux ni processus supplémentaires, mais serait plus impliqué et est probablement hors de portée de votre question.

Zhro
la source
(1) Je me demandais quand quelqu'un allait donner une réponse en utilisant des expressions régulières Perl. Si vous vous concentrez sur cette partie de votre message et expliquez comment cela fonctionne, cela pourrait être une bonne réponse. (2) Mais je crains que le reste ne soit pas si bon. La question dit «ne montrer que les lignes contenant l'un des deux mots» (non souligné dans l'original). Si la sortie est censée être des lignes , il va de soi que l'entrée doit également être composée de plusieurs lignes.   Mais votre approche ne fonctionne que lorsque vous ne regardez qu'une seule ligne. … (Suite)
G-Man dit «Réinstalle Monica» le
(Suite)… Par exemple, si l'entrée contient les lignes Big apple\net pear-shaped\n, alors la sortie doit contenir ces deux lignes. Votre solution obtiendrait un nombre de 2; la version longue rapporterait «Les deux mots correspondent» (ce qui est une réponse à la mauvaise question) et la version courte ne dirait rien du tout. (3) Une suggestion: utiliser -oici est une très mauvaise idée, car elle masque les lignes qui contiennent les correspondances, donc vous ne pouvez pas voir quand les deux mots apparaissent sur la même ligne. … (Suite)
G-Man dit «Réinstalle Monica» le
(Suite)… (4) Conclusion: votre utilisation de uniq/ sort -uet de l'expression régulière de Perl pour correspondre uniquement à la dernière occurrence de chaque ligne ne correspond pas vraiment à une réponse utile à cette question. Mais, même s'ils le faisaient, ce serait toujours une mauvaise réponse parce que vous n'expliquez pas comment ils contribuent à répondre à la question. (Voir la réponse de Stéphane Chazelas pour un exemple d'une bonne explication.)
G-Man dit 'Reinstate Monica' le
Le PO dit vouloir "ne montrer que les lignes contenant l'un des deux mots", ce qui signifie que chaque ligne doit être évaluée seule. Je ne vois pas pourquoi vous pensez que cela ne répond pas à la question. Veuillez fournir un exemple d'entrée qui, selon vous, échouerait.
Zhro
Oh, c'est ce que tu voulais dire? «Lisez l'entrée ligne par ligne et exécutez ces deux ou trois commandes pour chaque ligne . "? (1) Il est douloureusement difficile de comprendre que c'est ce que vous vouliez dire. (2) C'est douloureusement inefficace. Quatre réponses avant la vôtre ont montré comment gérer l' intégralité du fichier en quelques commandes (une, deux ou quatre), et vous souhaitez exécuter 3 ×  n commandes pour n lignes d'entrée? Même si cela fonctionne, il obtient un vote négatif pour une exécution inutilement coûteuse. (3) Au risque de fendre les poils, il ne fait toujours pas le travail de montrer les lignes appropriées.
G-Man dit «Réinstalle Monica» le