Jak grep dla linii zawierających jedno z dwóch słów, ale nie oba?

25

Próbuję użyć, grepaby wyświetlić tylko wiersze zawierające jedno z dwóch słów, jeśli tylko jedno z nich pojawia się w wierszu, ale nie, jeśli znajdują się w tym samym wierszu.

Do tej pory próbowałem, grep pattern1 | grep pattern2 | ...ale nie uzyskałem oczekiwanego rezultatu.

Trasmos
źródło
(1) Mówisz o „słowach” i „wzorach”. Który to jest? Zwyczajne słowa, takie jak „szybki”, „brązowy” i „lis”, lub wyrażenia regularne, takie jak [a-z][a-z0-9]\(,7\}\(\.[a-z0-9]\{,3\}\)+? (2) Co się stanie, jeśli jedno ze słów / wzorów pojawi się więcej niż jeden raz w linii (a drugie nie pojawi się)? Czy jest to równoważne słowu występującemu raz, czy też liczy się jako wielokrotne wystąpienie?
G-Man mówi „Reinstate Monica”

Odpowiedzi:

59

Narzędzie inne niż grepjest do zrobienia.

Na przykład, używając perla, komenda wyglądałaby następująco:

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

perl -neuruchamia polecenie podane dla każdej linii standardowej, która w tym przypadku wypisuje linię, jeśli pasuje /pattern1/ xor /pattern2/, lub innymi słowy dopasowuje jeden wzór, ale nie drugi (wyłączny lub).

Działa to dla wzorca w dowolnej kolejności i powinno mieć lepszą wydajność niż wiele wywołań grep, a także mniej pisania.

Lub, nawet krócej, z awk:

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

lub dla wersji awk, które nie mają xor:

awk '/pattern1/+/pattern2/==1`
Chris
źródło
4
Fajnie - czy Awk jest xordostępny tylko w GNU Awk?
steeldriver
9
@steeldriver Myślę, że to tylko GNU, tak. A przynajmniej brakuje go w starszych wersjach. Możesz zastąpić go brakującym /pattern1/+/pattern2/==1ir xor.
Chris
4
@JimL. Można wstawić granice słów ( \b) w samych wzorcach, tj \bword\b.
wjandrea
4
@vikingsteve Jeśli szczególnie chcesz użyć grep, znajdziesz tutaj wiele innych odpowiedzi. Ale dla ludzi, którzy chcą po prostu wykonać zadanie, dobrze wiedzieć, że istnieją inne narzędzia, które mogą zrobić wszystko, co robi grep, ale coraz łatwiej.
Chris
3
@vikingsteve Zdecydowanie przypuszczam, że popyt na rozwiązanie grep jest rodzajem problemu XY
Hagen von Eitzen
30

Dzięki GNU grepmożesz przekazać oba słowa do, grepa następnie usunąć linie zawierające oba wzorce.

$ 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
źródło
16

Spróbuj z egrep

egrep  'pattern1|pattern2' file | grep -v -e 'pattern1.*pattern2' -e 'pattern2.*pattern1'
msp9011
źródło
3
można również zapisać jakogrep -e foo -e bar | grep -v -e 'foo.*bar' -e 'bar.*foo'
glenn jackman
8
Uwaga ze strony man grep: Direct invocation as either egrep or fgrep is deprecated- preferujgrep -E
glenn jackman
Tego nie ma w moim systemie operacyjnym @glennjackman
Grump
1
@ Grump naprawdę? Co to za system operacyjny? Nawet POSIX wspomina, że grep powinien mieć -fi -eopcje, chociaż są starsze egrepi fgrepbędą przez jakiś czas obsługiwane.
terdon
1
@terdon, POSIX nie określa ścieżki narzędzi POSIX. Ponownie istnieje norma grep(to podpory -F, -E, -e, -fjak tego wymaga POSIX) jest /usr/xpg4/bin. Narzędzia w /binsą przestarzałe.
Stéphane Chazelas
12

Dzięki grepimplementacjom, które obsługują wyrażenia regularne podobne do perla (jak pcregreplub GNU lub ast-open grep -P), możesz to zrobić za pomocą jednego grepwywołania za pomocą:

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

To jest znaleźć linie, które pasują, pat1ale nie pat2, pat2ale nie pat1.

(?=...)i (?!...)są odpowiednio operatorami perspektywicznymi i negatywnymi. Tak więc technicznie powyższe wygląda na początek tematu ( ^) pod warunkiem, że następuje po nim .*pat1i nie następuje po nim .*pat2, lub to samo z pat1i pat2odwrócone.

Jest to nieoptymalne dla linii zawierających oba wzorce, ponieważ byłyby wówczas dwukrotnie wyszukiwane. Zamiast tego możesz użyć bardziej zaawansowanych operatorów perla, takich jak:

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

(?(1)yespattern|nopattern)pasuje do, yespatternjeśli grupa przechwytywania 1st (pusta ()powyżej) jest zgodna, i nopatterninaczej. Jeśli to ()mecze, że środki pat1nie są zgodne, więc szukamy pat2(pozytywnego spojrzenia w przyszłość) i patrzymy na nie pat2 inaczej (negatywny patrzeć w przyszłość).

Za pomocą sedmożesz to napisać:

sed -ne '/pat1/{/pat2/!p;d;}' -e '/pat2/p'
Stéphane Chazelas
źródło
Twoje pierwsze rozwiązanie zawiedzie grep: the -P option only supports a single pattern, przynajmniej na każdym systemie, do którego mam dostęp. +1 za drugie rozwiązanie.
Chris
1
@Chris, masz rację. Wydaje się, że jest to ograniczenie specyficzne dla GNU grep. pcregrepa ast-open grep nie ma tego problemu. Zamieniłem wielokrotność -ena alternatywny operator RE, więc powinien on działać również z GNU grep.
Stéphane Chazelas
Tak, teraz działa dobrze.
Chris
3

Mówiąc logicznie, szukasz A xor B, który można zapisać jako

(A i nie B)

lub

(B i nie A)

Biorąc pod uwagę, że twoje pytanie nie wspomina, że ​​zajmujesz się kolejnością danych wyjściowych, o ile wyświetlane są pasujące linie, rozszerzenie boolowskie A xor B jest dość proste w 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.
źródło
1
Działa to, ale szyfruje kolejność plików.
Sparhawk
@Sparhawk Prawda, chociaż „wspinaczka” jest trudnym słowem. ;) wyświetla najpierw wszystkie dopasowania „a”, w kolejności, a następnie wszystkie dopasowania „b”, w kolejności. OP nie wyraził zainteresowania utrzymaniem porządku, wystarczy pokazać wiersze. FAWK, następnym krokiem może być sort | uniq.
Jim L.
Uczciwe połączenie; Zgadzam się, że mój język był niedokładny. Miałem na myśli, że oryginalne zamówienie zostanie zmienione.
Sparhawk
1
@Sparhawk ... I zredagowałem w twoich obserwacjach dla pełnego ujawnienia.
Jim L.
-2

Dla następującego przykładu:

# Patterns:
#    apple
#    pear

# Example line
line="a_apple_apple_pear_a"

Można to zrobić wyłącznie z grep -E, uniq, i wc.

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

Jeśli grepjest skompilowany z wyrażeniami regularnymi Perla, możesz dopasować do ostatniego wystąpienia, zamiast konieczności potoku do uniq:

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

Wynik:

# 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

Jednowarstwowy:

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

Jeśli nie chcesz na stałe kodować wzorca, składanie go ze zmiennym zestawem elementów można zautomatyzować za pomocą funkcji.

Można to również zrobić natywnie w Bash jako funkcję bez rur lub dodatkowych procesów, ale byłby bardziej zaangażowany i prawdopodobnie nie wchodzi w zakres twojego pytania.

Zhro
źródło
(1) Zastanawiałem się, kiedy ktoś udzieli odpowiedzi za pomocą wyrażeń regularnych Perla. Jeśli skupiłeś się na tej części swojego postu i wyjaśniłeś, jak to działa, może to być dobra odpowiedź. (2) Ale obawiam się, że reszta nie jest tak dobra. Pytanie brzmi: „pokaż tylko wiersze zawierające jedno z dwóch słów” (podkreślenie dodane). Jeśli dane wyjściowe mają być liniami , oznacza to, że dane wejściowe muszą składać się z wielu linii.   Ale twoje podejście działa tylko wtedy, gdy patrzysz tylko na jedną linię. … (Ciąg dalszy)
G-Man mówi „Przywróć Monikę”
(Ciąg dalszy) ... Na przykład, jeśli dane wejściowe zawiera wiersze Big apple\ni pear-shaped\n, następnie wyjście powinno zawierać zarówno tych linii. Twoje rozwiązanie uzyskałoby liczbę 2; w długiej wersji będzie napisane „Dopasowane oba słowa” (co jest odpowiedzią na złe pytanie), a w krótkiej wersji nic nie powie. (3) Sugestia: użycie -otutaj jest naprawdę złym pomysłem, ponieważ ukrywa wiersze zawierające dopasowania, więc nie możesz zobaczyć, kiedy oba słowa pojawiają się w tym samym wierszu. … (Ciąg dalszy)
G-Man mówi „Przywróć Monikę”
(Ciąg dalszy)… (4) Konkluzja: twoje użycie uniq/ sort -ui fantazyjnego wyrażenia regularnego Perla, by dopasować tylko ostatnie wystąpienie w każdym wierszu, tak naprawdę nie stanowi użytecznej odpowiedzi na to pytanie. Ale nawet gdyby tak było, nadal byłaby to zła odpowiedź, ponieważ nie wyjaśnisz, w jaki sposób przyczyniają się do udzielenia odpowiedzi na pytanie. (Zobacz odpowiedź Stéphane'a Chazelasa na przykład dobrego wyjaśnienia.)
G-Man mówi „Przywróć Monikę”
OP twierdzi, że chcieli „pokazać tylko wiersze zawierające jedno z dwóch słów”, co oznacza, że ​​każdy wiersz musi zostać oceniony osobno. Nie rozumiem, dlaczego uważasz, że to nie odpowiada na pytanie. Podaj przykładowe dane wejściowe, które Twoim zdaniem mogą zawieść.
Zhro
Och, czy o to ci chodziło? „Odczytaj wejście po linii i wykonaj te dwa lub trzy polecenia dla każdej linii . ”? (1) To boleśnie niejasne, że o to ci chodziło. (2) Jest to boleśnie nieefektywne. Cztery odpowiedzi przed twoimi pokazały, jak obsłużyć cały plik w kilku poleceniach (jednym, dwóch lub czterech), a chcesz uruchomić polecenia 3 ×  n dla n wierszy wprowadzania? Nawet jeśli działa, zyskuje głos w dół za niepotrzebnie kosztowne wykonanie. (3) Ryzykując rozdwajanie włosów, nadal nie wykonuje zadania pokazania odpowiednich linii.
G-Man mówi „Przywróć Monikę”