Как я grep для строк, содержащих одно из двух слов, но не оба?


25

Я пытаюсь использовать, grepчтобы показать только строки, содержащие одно из двух слов, если только одно из них появляется в строке, но не если они находятся в одной строке.

Пока я пытался, grep pattern1 | grep pattern2 | ...но не получил ожидаемого результата.


(1) Вы говорите о «словах» и «шаблонах». Что он? Обычные слова, такие как «быстрый», «коричневый» и «лиса», или регулярные выражения, как [a-z][a-z0-9]\(,7\}\(\.[a-z0-9]\{,3\}\)+? (2) Что, если одно из слов / шаблонов появляется более одного раза в строке (а другое не появляется)? Это эквивалентно тому, что слово появляется один раз, или оно считается как несколько вхождений?
G-Man говорит: «Восстановите Монику»

Ответы:


59

Инструмент, отличный grepот пути.

Например, используя perl, команда будет:

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

perl -neвыполняет команду, заданную для каждой строки стандартного ввода, которая в этом случае печатает строку, если она соответствует /pattern1/ xor /pattern2/, или другими словами, соответствует одному шаблону, но не другому (исключающему или).

Это работает для шаблона в любом порядке и должно иметь лучшую производительность, чем множественные вызовы grep, а также меньше печатать.

Или, еще короче, с awk:

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

или для версий awk, которые не имеют xor:

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

4
Хорошо - Awk xorдоступен только в GNU Awk?
steeldriver

9
@steeldriver Я думаю, что это только GNU, да. Или, по крайней мере, он отсутствует на старых версиях. Вы можете заменить его на /pattern1/+/pattern2/==1ir xorотсутствует.
Крис

4
@JimL. Вы можете поместить границы слова ( \b) в самих моделях, то есть \bword\b.
wjandrea

4
@vikingsteve Если вы специально хотите использовать grep, здесь есть много других ответов. Но для людей, которые просто хотят выполнить работу, полезно знать, что есть другие инструменты, которые могут делать все, что делает grep, но все более легко.
Крис

3
@vikingsteve Я бы сильно предположил, что спрос на решение grep является своего рода проблемой XY
Хаген фон Айцен

30

С помощью GNU grepвы можете передать оба слова, grepа затем удалить строки, содержащие оба шаблона.

$ 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

Попробуй с egrep

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

3
также можно записать какgrep -e foo -e bar | grep -v -e 'foo.*bar' -e 'bar.*foo'
Гленн Джекман

8
Также обратите внимание на страницу руководства grep: Direct invocation as either egrep or fgrep is deprecated- предпочитаюgrep -E
Гленн Джекман

Этого нет в моей ОС @glennjackman
Grump

1
@ Правда? Что это за ОС? Даже в POSIX упоминается, что у grep должны быть опции -fи -eопции, хотя старые egrepи fgrepбудут поддерживаться некоторое время.
Тердон

1
@terdon, POSIX не указывает путь к утилитам POSIX. Опять же , есть, стандарт grep(который поддерживает -F, -E, -e, , -fкак POSIX требует) в /usr/xpg4/bin. Утилиты в /binустарели.
Стефан Шазелас

12

С grepреализациями, которые поддерживают Perl-подобные регулярные выражения (например, pcregrepили GNU, или ast-open grep -P), вы можете сделать это за один grepвызов:

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

То есть найдите линии, которые соответствуют, pat1но не соответствуют pat2, или pat2нет pat1.

(?=...)и (?!...)соответственно смотрят в будущее и смотрят в будущее операторы. Технически, вышесказанное ищет начало субъекта ( ^), если за ним следует, .*pat1а не следует .*pat2, или то же самое с pat1и в pat2обратном порядке.

Это неоптимально для строк, которые содержат оба шаблона, так как их потом будут искать дважды. Вместо этого вы можете использовать более продвинутые операторы Perl, такие как:

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

(?(1)yespattern|nopattern)совпадает с совпадающей yespatternгруппой 1st st (пустой ()сверху), и в nopatternпротивном случае. Если это ()соответствует, это означает, что pat1не соответствует, поэтому мы ищем pat2(позитивный взгляд вперед), и мы ищем не pat2 иначе (негативный взгляд вперед).

С помощью sedвы можете написать это:

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

Ваше первое решение не работает grep: the -P option only supports a single pattern, по крайней мере, в каждой системе, к которой у меня есть доступ. +1 за ваше второе решение, хотя.
Крис

1
@ Крис, ты прав. Это, похоже, ограничение, специфичное для GNU grep. pcregrepи у ast-open grep такой проблемы нет. Я заменил -eмножитель оператором чередования RE, поэтому grepтеперь он должен работать и с GNU .
Стефан Шазелас

Да, сейчас работает нормально.
Крис

3

В логических терминах вы ищете A xor B, который можно записать как

(А, а не В)

или

(Б а не А)

Учитывая, что в вашем вопросе не упоминается, что вы обеспокоены порядком вывода, пока отображаются соответствующие строки, логическое расширение A xor B чертовски просто в 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
Это работает, но это зашифрует порядок файла.
Sparhawk

@Sparhawk Верно, хотя "схватка" - грубое слово. ;) сначала перечисляются все совпадения 'a' по порядку, а затем по порядку следуют все совпадения 'b'. ОП не выразил никакой заинтересованности в поддержании порядка, просто показать линии. FAWK, следующим шагом может быть sort | uniq.
Джим Л.

Честный звонок; Я согласен, что мой язык был неточным. Я имел в виду, что первоначальный порядок будет изменен.
Sparhawk

1
@ Sparhawk ... И я отредактировал в твоих наблюдениях полное раскрытие.
Джим Л.

-2

Для следующего примера:

# Patterns:
#    apple
#    pear

# Example line
line="a_apple_apple_pear_a"

Это может быть сделано исключительно с grep -E,uniq и wc.

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

Если grepскомпилировано с регулярными выражениями Perl, вы можете сопоставить последнее вхождение, вместо того, чтобы передаватьuniq :

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

Выведите результат:

# 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

Однострочник:

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

Если вы не хотите жестко кодировать шаблон, его сборка с переменным набором элементов может быть автоматизирована с помощью функции.

Это также может быть сделано изначально в Bash как функция без конвейеров или дополнительных процессов, но будет более сложным и, вероятно, выходит за рамки вашего вопроса.


(1) Мне было интересно, когда кто-нибудь собирается дать ответ, используя регулярные выражения Perl. Если вы сосредоточились на этой части своего поста и объяснили, как это работает, это может быть хорошим ответом. (2) Но я боюсь, что остальное не так хорошо. Вопрос гласит: «показывать только строки, содержащие одно из двух слов» (выделение добавлено). Если предполагается, что выходные данные являются строками , то очевидно, что входные данные также должны состоять из нескольких строк.   Но ваш подход работает только при рассмотрении только одной строки. … (Продолжение)
G-Man сказал «Восстановить Монику»

(Продолжение)… Например, если вход содержит строки Big apple\nи pear-shaped\n, то выход должен содержать обе эти строки. Ваше решение получит счет 2; длинная версия будет сообщать «оба слова совпадают» (что является ответом на неправильный вопрос), а короткая версия вообще ничего не говорит. (3) Предложение: использование -oздесь - очень плохая идея, потому что оно скрывает строки, содержащие совпадения, поэтому вы не можете видеть, когда оба слова появляются на одной строке. … (Продолжение)
G-Man сказал «Восстановить Монику»

(Продолжение)… (4) Итог: использование вами uniq/ sort -uи необычного регулярного выражения Perl для сопоставления только с последним вхождением в каждой строке на самом деле не дает полезного ответа на этот вопрос. Но даже если бы они это сделали, это все равно было бы плохим ответом, потому что вы не объясняете, как они способствуют ответу на вопрос. (См . Ответ Стефана Шазеласа для примера хорошего объяснения.)
G-Man говорит: «Восстановите Монику»

ОП говорит, что они хотели «показать только строки, содержащие любое из двух слов», что означает, что каждая строка должна оцениваться самостоятельно. Я не понимаю, почему вы чувствуете, что это не отвечает на вопрос. Пожалуйста, предоставьте пример ввода, который, по вашему мнению, потерпит неудачу.
Жро

Ой, что то , что вы имели в виду? «Прочитайте ввод строки за раз и выполните эти две или три команды для каждой строки . «? (1) Мучительно неясно, что вы имели в виду. (2) Это мучительно неэффективно. Четыре ответа перед вами показали, как обрабатывать весь файл за несколько команд (одна, две или четыре), и вы хотите выполнить 3 ×  n команд для n строк ввода? Даже если это работает, он получает отрицательный голос за излишне дорогое исполнение. (3) С риском расщепления волос, он по-прежнему не показывает правильные линии.
G-Man говорит «Восстановить Монику»
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.