Как найти шаблоны по нескольким строкам, используя grep?

208

Я хочу найти файлы с «abc» И «efg» в этом порядке, и эти две строки находятся в разных строках в этом файле. Например: файл с содержанием:

blah blah..
blah blah..
blah abc blah
blah blah..
blah blah..
blah blah..
blah efg blah blah
blah blah..
blah blah..

Должно совпадать.

Saobi
источник
4
Возможный дубликат Как я могу найти многострочный шаблон в файле?
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

Ответы:

225

Grep недостаточно для этой операции.

pcregrep, который встречается в большинстве современных систем Linux, может использоваться как

pcregrep -M  'abc.*(\n|.)*efg' test.txt

где -M, --multiline позволяют модели , чтобы соответствовать более чем одной линии

Также есть более новый pcre2grep . Оба предоставлены проектом PCRE .

pcre2grep доступен для Mac OS X через порты Mac как часть порта pcre2:

% sudo port install pcre2 

и через Homebrew как:

% brew install pcre

или для pcre2

% brew install pcre2

pcre2grep также доступен в Linux (Ubuntu 18.04+)

$ sudo apt install pcre2-utils # PCRE2
$ sudo apt install pcregrep    # Older PCRE
носитель кольца
источник
11
@StevenLu -M, --multiline- разрешить шаблонам соответствовать более чем одной строке.
носитель кольца
7
Обратите внимание, что. * (\ N |.) * Эквивалентно (\ n |.) *, А последний короче. Более того, в моей системе «pcre_exec () error -8» возникает при запуске более длинной версии. Поэтому попробуйте вместо этого 'abc (\ n |.) * Efg'!
daveagp
6
Вы должны сделать выражение не жадным в этом случае, например:'abc.*(\n|.)*?efg'
кольцо на предъявителя
4
и вы можете опустить первый .*-> 'abc(\n|.)*?efg'чтобы сделать регулярное выражение короче (и быть педантичным)
Michi
6
pcregrepделает вещи проще, но grepтоже будет работать. Например, см stackoverflow.com/a/7167115/123695
Michael MIOR
113

Я не уверен, возможно ли это с помощью grep, но sed делает это очень просто:

sed -e '/abc/,/efg/!d' [file-with-content]
LJ.
источник
4
Это не находит файлы, оно возвращает соответствующую часть из одного файла
shiggity
11
@Lj. пожалуйста, вы можете объяснить эту команду? Я знаком с sed, но если никогда не видел такого выражения раньше.
Энтони
1
@Anthony, это задокументировано в справочной странице sed под адресом. Важно понимать, что / abc / & / efg / - это адрес.
Squidly
49
Я подозреваю, что этот ответ был бы полезен, если бы в нем было немного больше объяснений, и в этом случае я бы проголосовал за него еще раз. Я немного знаком с sed, но не настолько, чтобы использовать этот ответ для получения значимого кода выхода после получаса. Совет: «RTFM» редко получает положительные голоса в StackOverflow, как показывает ваш предыдущий комментарий.
Майкл Шепер
25
Краткое объяснение на примере: sed '1,5d': удалить строки между 1 и 5. sed '1,5! D': удалить строки не между 1 и 5 (т.е. оставить строки между), а затем вместо числа можно поиск строки с / pattern /. См. Также более простой пример ниже: sed -n '/ abc /, / efg / p' p предназначен для печати, а флаг -n не отображает все строки
phil_w
87

Вот решение, вдохновленное этим ответом :

  • если 'abc' и 'efg' могут быть в одной строке:

    grep -zl 'abc.*efg' <your list of files>
  • если 'abc' и 'efg' должны быть в разных строках:

    grep -Pzl '(?s)abc.*\n.*efg' <your list of files>

Params:

  • -zОбрабатывайте ввод как набор строк, каждая из которых заканчивается нулевым байтом вместо новой строки. то есть grep обрабатывает ввод как одну большую строку.

  • -l напечатать имя каждого входного файла, из которого обычно выводился бы вывод.

  • (?s)активировать PCRE_DOTALL, что означает «.» находит любой символ или перевод строки.

ИПК
источник
@syntaxerror Нет, я думаю, что это только строчные буквы l. AFAIK нет -1варианта номера .
Sparhawk
Кажется, вы правы, возможно, я сделал опечатку при тестировании. В любом случае извините за прокладку ложного следа.
синтаксическая ошибка
6
Это отлично. У меня только один вопрос по этому поводу. Если в -zпараметрах указывается grep для обработки символов новой строки, zero byte charactersто зачем нам (?s)в регулярном выражении? Если это уже не символ новой строки, не должны ли .быть в состоянии сопоставить его напрямую?
Дурга Сваруп
1
-z (aka --null-data) и (? s) - это именно то, что вам нужно для соответствия многострочного стандартного grep. Люди на MacOS, пожалуйста, оставляйте комментарии о наличии опций -z или --null-data на ваших системах!
Зик Фаст
4
-z определенно не доступен на MacOS
Дилан Николсон
33

sed должно быть достаточно, как указано выше в постере ЖЖ,

вместо! d вы можете просто использовать p для печати:

sed -n '/abc/,/efg/p' file
Кара
источник
16

Я сильно полагался на pcregrep, но с более новым grep вам не нужно устанавливать pcregrep для многих его функций. Просто используйте grep -P.

В примере с вопросом OP, я думаю, что следующие варианты работают хорошо, со вторым лучшим соответствием, как я понимаю вопрос:

grep -Pzo "abc(.|\n)*efg" /tmp/tes*
grep -Pzl "abc(.|\n)*efg" /tmp/tes*

Я скопировал текст как / tmp / test1, удалил «g» и сохранил как / tmp / test2. Вот выходные данные, показывающие, что первый показывает совпадающую строку, а второй показывает только имя файла (типично -o - показать совпадение, а типичное -l - показать только имя файла). Обратите внимание, что «z» необходимо для многострочного, а «(. | \ N)» означает совпадение с «чем-либо, кроме newline» или «newline» - т.е. с чем угодно:

user@host:~$ grep -Pzo "abc(.|\n)*efg" /tmp/tes*
/tmp/test1:abc blah
blah blah..
blah blah..
blah blah..
blah efg
user@host:~$ grep -Pzl "abc(.|\n)*efg" /tmp/tes*
/tmp/test1

Чтобы определить, является ли ваша версия достаточно новой, запустите man grepи посмотрите, появляется ли что-то похожее на это сверху:

   -P, --perl-regexp
          Interpret  PATTERN  as a Perl regular expression (PCRE, see
          below).  This is highly experimental and grep -P may warn of
          unimplemented features.

Это из GNU grep 2.10.

шалфей
источник
14

Это можно легко сделать, используя сначала trзамену новой строки другим символом:

tr '\n' '\a' | grep -o 'abc.*def' | tr '\a' '\n'

Здесь я использую символ тревоги \a(ASCII 7) вместо новой строки. Это почти никогда не встречается в вашем тексте, и grepможет совпадать с ним .или специально соответствовать \a.

Гэвин С. Янси
источник
1
Это был мой подход, но я использовал \0и, следовательно, нуждался grep -aи соответствовал \x00… Вы помогли мне упростить! echo $log | tr '\n' '\0' | grep -aoE "Error: .*?\x00Installing .*? has failed\!" | tr '\0' '\n'сейчасecho $log | tr '\n' '\a' | grep -oE "Error: .*?\aInstalling .*? has failed\!" | tr '\a' '\n'
Чарли
1
Использование grep -o.
kyb
7

awk one-liner:

awk '/abc/,/efg/' [file-with-content]
Swynndla
источник
4
Это с радостью будет печатать от abcконца до конца файла, если конечный шаблон отсутствует в файле или отсутствует последний конечный шаблон. Вы можете это исправить, но это значительно усложнит сценарий.
tripleee
Как исключить /efg/из вывода?
Кюб
6

Вы можете сделать это очень легко, если вы можете использовать Perl.

perl -ne 'if (/abc/) { $abc = 1; next }; print "Found in $ARGV\n" if ($abc && /efg/); }' yourfilename.txt

Вы также можете сделать это с помощью одного регулярного выражения, но это включает в себя все содержимое файла в одну строку, что может в конечном итоге занять слишком много памяти большими файлами. Для полноты, вот этот метод:

perl -e '@lines = <>; $content = join("", @lines); print "Found in $ARGV\n" if ($content =~ /abc.*efg/s);' yourfilename.txt
sundar - Восстановить Монику
источник
Найденный второй ответ был полезен для извлечения всего многострочного блока с совпадениями на пару строк - пришлось использовать не жадное сопоставление ( .*?), чтобы получить минимальное совпадение.
RichVel
5

Я не знаю, как бы я это сделал с grep, но я бы сделал что-то подобное с awk:

awk '/abc/{ln1=NR} /efg/{ln2=NR} END{if(ln1 && ln2 && ln1 < ln2){print "found"}else{print "not found"}}' foo

Вы должны быть осторожны, как вы это делаете, хотя. Вы хотите, чтобы регулярное выражение соответствовало подстроке или всему слову? добавьте теги \ w по мере необходимости. Кроме того, хотя это строго соответствует тому, как вы изложили пример, оно не совсем работает, когда abc появляется во второй раз после efg. Если вы хотите справиться с этим, добавьте if в случае необходимости в / abc / case и т. Д.

frankc
источник
3

К сожалению, вы не можете. Из grepдокументов:

grep ищет в именованных входных ФАЙЛАХ (или в стандартном вводе, если файлы не названы, или если в качестве имени файла указан один дефис-минус (-)) строки, содержащие совпадение с заданным ШАБЛОНОМ.

Калеб Педерсон
источник
что оgrep -Pz
Navaro
3

Если вы хотите использовать контексты, этого можно достичь, набрав

grep -A 500 abc test.txt | grep -B 500 efg

Это отобразит все между «abc» и «efg», если они находятся в пределах 500 строк друг от друга.

agouge
источник
3

Если вам нужно, чтобы оба слова были близко друг к другу, например, не более 3 строк, вы можете сделать это:

find . -exec grep -Hn -C 3 "abc" {} \; | grep -C 3 "efg"

Тот же пример, но фильтрация только файлов * .txt:

find . -name *.txt -exec grep -Hn -C 3 "abc" {} \; | grep -C 3 "efg"

А также вы можете заменить grepкоманду на egrepкоманду, если вы также хотите найти с регулярными выражениями.

Мариано Руис
источник
3

Несколько дней назад я выпустил альтернативу grep, которая поддерживает это напрямую, либо с помощью многострочного сопоставления, либо с использованием условий - надеюсь, это будет полезно для некоторых людей, которые ищут здесь. Вот как будут выглядеть команды для примера:

Multiline:

sift -lm 'abc.*efg' testfile

условия:

sift -l 'abc' testfile --followed-by 'efg'

Вы также можете указать, что 'efg' должен следовать за 'abc' в определенном количестве строк:

sift -l 'abc' testfile --followed-within 5:'efg'

Вы можете найти больше информации на sift-tool.org .

Svent
источник
Я не думаю, что первый пример sift -lm 'abc.*efg' testfileработает, потому что совпадение является жадным и поглощает все строки до последней efgв файле.
Доктор Алекс RE
2

Хотя опция sed является самой простой и легкой, однострочная версия LJ, к сожалению, не самая портативная. Те, кто застрял с версией C Shell, должны избежать челки:

sed -e '/abc/,/efg/\!d' [file]

Это, к сожалению, не работает в Bash et al.

ошибка
источник
1
#!/bin/bash
shopt -s nullglob
for file in *
do
 r=$(awk '/abc/{f=1}/efg/{g=1;exit}END{print g&&f ?1:0}' file)
 if [ "$r" -eq 1 ];then
   echo "Found pattern in $file"
 else
   echo "not found"
 fi
done
ghostdog74
источник
1

Вы можете использовать grep, если вы не заинтересованы в последовательности паттерна.

grep -l "pattern1" filepattern*.* | xargs grep "pattern2"

пример

grep -l "vector" *.cpp | xargs grep "map"

grep -lнайдет все файлы, которые соответствуют первому шаблону, а xargs будет grep для второго шаблона. Надеюсь это поможет.

Балу Мохан
источник
1
Это игнорировало бы порядок «pattern1» и «pattern2», появляющиеся в файле, хотя - OP определенно указывает, что должны совпадать только те файлы, где «pattern2» появляется ПОСЛЕ «pattern1».
Эмиль Лундберг
1

С серебряным искателем :

ag 'abc.*(\n|.)*efg'

похож на ответ на предъявителя кольца, но вместо этого используется ag. Скоростные преимущества серебряного искателя могли бы здесь проявиться.

Shwaydogg
источник
1
Это не похоже на работу. (echo abctest; echo efg)|ag 'abc.*(\n|.)*efg'не совпадает
phiresky
1

Я использовал это для извлечения последовательности fasta из мультифаст-файла, используя опцию -P для grep:

grep -Pzo ">tig00000034[^>]+"  file.fasta > desired_sequence.fasta
  • P для поиска на основе Perl
  • z для создания конца строки в 0 байтов, а не символа новой строки
  • o просто захватить то, что совпало, так как grep возвращает всю строку (что в данном случае, поскольку вы сделали -z - это весь файл).

Ядро регулярного выражения - это то, [^>]что переводится как «не больше, чем символ»

Джон Бойл
источник
0

В качестве альтернативы ответа Бала Мохан, можно применять порядок моделей с использованием только grep, headи tail:

for f in FILEGLOB; do tail $f -n +$(grep -n "pattern1" $f | head -n1 | cut -d : -f 1) 2>/dev/null | grep "pattern2" &>/dev/null && echo $f; done

Этот не очень красивый, хотя. Форматируется более наглядно:

for f in FILEGLOB; do
    tail $f -n +$(grep -n "pattern1" $f | head -n1 | cut -d : -f 1) 2>/dev/null \
    | grep -q "pattern2" \
    && echo $f
done

Это напечатает имена всех файлов , где "pattern2"появляется после того, как "pattern1", или когда оба появляются на одной и той же линии :

$ echo "abc
def" > a.txt
$ echo "def
abc" > b.txt
$ echo "abcdef" > c.txt; echo "defabc" > d.txt
$ for f in *.txt; do tail $f -n +$(grep -n "abc" $f | head -n1 | cut -d : -f 1) 2>/dev/null | grep -q "def" && echo $f; done
a.txt
c.txt
d.txt

объяснение

  • tail -n +i- печатать все строки после iй включительно
  • grep -n - предварительно сопоставлять совпадающие строки с их номерами
  • head -n1 - печатать только первый ряд
  • cut -d : -f 1- напечатать первый вырезанный столбец, используя :в качестве разделителя
  • 2>/dev/null- tailвывод ошибки тишины, которая возникает, если $()выражение возвращает пустое значение
  • grep -q- замолчать grepи немедленно вернуться, если совпадение найдено, так как нас интересует только код выхода
Эмиль Лундберг
источник
Может кто-нибудь объяснить, пожалуйста &>? Я тоже этим пользуюсь, но нигде не видел, чтобы это было задокументировано. Кстати, почему мы должны так замалчивать grep? grep -qтоже не справится?
синтаксическая ошибка
1
&>говорит bash перенаправить как стандартный вывод, так и стандартную ошибку, см. REDIRECTION в руководстве по bash. Вы очень правы в том , что мы могли бы также сделать grep -q ...вместо grep ... &>/dev/null, хороший улов!
Эмиль Лундберг
Так и думал. Удалит боль от многих неуклюжих дополнительных печатать. Спасибо за объяснение - поэтому я, должно быть, немного пропустил руководство. (Посмотрел что-то отдаленно связанное с этим некоторое время назад.) --- Вы можете даже подумать об изменении этого в своем ответе. :)
syntaxerror
0

Это тоже должно работать ?!

perl -lpne 'print $ARGV if /abc.*?efg/s' file_list

$ARGVсодержит имя текущего файла при чтении из file_list /sпоисков модификатора через новую строку .

PS12
источник
0

Filepattern *.shважен для предотвращения проверки каталогов. Конечно, некоторые испытания могут предотвратить это тоже.

for f in *.sh
do
  a=$( grep -n -m1 abc $f )
  test -n "${a}" && z=$( grep -n efg $f | tail -n 1) || continue 
  (( ((${z/:*/}-${a/:*/})) > 0 )) && echo $f
done

The

grep -n -m1 abc $f 

ищет максимум 1 совпадение и возвращает (-n) номер белья. Если совпадение было найдено (test -n ...), найдите последнее совпадение с efg (найдите все и возьмите последнее с tail -n 1).

z=$( grep -n efg $f | tail -n 1)

еще продолжить.

Поскольку в результате получается что-то вроде этого, 18:foofile.sh String alf="abc";нам нужно отрезать от ":" до конца строки.

((${z/:*/}-${a/:*/}))

Должен возвращать положительный результат, если последнее совпадение 2-го выражения прошло после первого совпадения первого.

Затем мы сообщаем имя файла echo $f.

Пользователь неизвестен
источник
0

Почему бы не что-то простое, как:

egrep -o 'abc|efg' $file | grep -A1 abc | grep efg | wc -l

возвращает 0 или положительное целое число.

egrep -o (Показывает только совпадения, трюк: несколько совпадений в одной строке производят многострочный вывод, как если бы они были в разных строках)

  • grep -A1 abc (выведите abc и строку после него)

  • grep efg | wc -l (0-n число строк efg, найденных после abc в той же или следующих строках, результат можно использовать в «если»)

  • grep может быть изменен на egrep и т. д., если требуется сопоставление с образцом

Kevins
источник
0

Если у вас есть некоторая оценка расстояния между двумя строками «abc» и «efg», которые вы ищете, вы можете использовать:

grep -r . -e 'abc' -A num1 -B num2 | grep 'efg'

Таким образом, первый grep вернет строку с «abc» плюс # num1 строки после нее и # num2 строки после нее, а второй grep просеет все эти строки, чтобы получить «efg». Тогда вы будете знать, в каких файлах они появляются вместе.

Бенджамин Беренд
источник
0

С ugrep вышел несколько месяцев назад:

ugrep 'abc(\n|.)+?efg'

Этот инструмент сильно оптимизирован по скорости. Он также совместим с GNU / BSD / PCRE-grep.

Обратите внимание, что мы должны использовать ленивое повторение +?, если вы не хотите сопоставлять все строки efgвместе до последнего efgв файле.

Доктор Алекс RE
источник
-3

Это должно работать:

cat FILE | egrep 'abc|efg'

Если найдено более одного совпадения, вы можете отфильтровать с помощью grep -v

Гуру
источник
2
Хотя этот фрагмент кода приветствуется и может оказать некоторую помощь, он будет значительно улучшен, если в него будет включено объяснение того, как и почему это решает проблему. Помните, что вы отвечаете на вопрос для читателей в будущем, а не только для того, кто спрашивает сейчас! Пожалуйста, отредактируйте свой ответ, чтобы добавить объяснение и указать, какие ограничения и предположения применяются.
Тоби Спейт
1
Это на самом деле не поиск по нескольким строкам , как указано в вопросе.
n.st