Как найти шаблоны по нескольким строкам, используя grep?
Я хочу найти файлы с "abc" и "efg" в этом порядке, и эти две строки находятся в разных строках в этом файле. Например: файл с содержанием:
blah blah..
blah blah..
blah abc blah
blah blah..
blah blah..
blah blah..
blah efg blah blah
blah blah..
blah blah..
Должно совпадать.
30 ответов
Grep недостаточно для этой операции.
pcregrep, который можно найти в большинстве современных систем Linux, можно использовать как
pcregrep -M 'abc.*(\n|.)*efg' test.txt
Также есть более новый pcre2grep. Оба предоставлены проектом PCRE.
pcre2grep доступен для Mac OS X через порты Mac как часть порта pcre2
:
% sudo port install pcre2
и через Homebrew как:
% brew install pcre
или для pcre2
% brew install pcre2
Вот решение, вдохновленное этим ответом:
если '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, что означает, что '.' находит любой символ или перевод строки.
Я не уверен, возможно ли это с помощью grep, но sed делает это очень просто:
sed -e '/abc/,/efg/!d' [file-with-content]
sed должно быть достаточно, как указано выше в постере ЖЖ,
вместо!d вы можете просто использовать p для печати:
sed -n '/abc/,/efg/p' file
Я сильно полагался на 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.
Это можно легко сделать, сначала используя tr
заменить символы новой строки каким-либо другим символом:
tr '\n' '\a' | grep 'abc.*def' | tr '\a' '\n'
Здесь я использую символ будильника, \a
(ASCII 7) вместо новой строки. Это почти никогда не встречается в вашем тексте, и grep
может сопоставить его с .
или сопоставить его конкретно с \a
,
Если вы хотите использовать контексты, это можно сделать, набрав
grep -A 500 abc test.txt | grep -B 500 efg
Это отобразит все между "abc" и "efg", если они находятся в пределах 500 строк друг от друга.
Вы можете сделать это очень легко, если вы можете использовать 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
Я не знаю, как бы я это сделал с 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 и т. Д.
Возможно с ripgrep:
$ rg --multiline 'abc(\n|.)+?efg' test.txt
3:blah abc blah
4:blah abc blah
5:blah blah..
6:blah blah..
7:blah blah..
8:blah efg blah blah
Или другие заклинания.
Если хочешь
.
считать новой строкой:
$ rg --multiline '(?s)abc.+?efg' test.txt
3:blah abc blah
4:blah abc blah
5:blah blah..
6:blah blah..
7:blah blah..
8:blah efg blah blah
Или эквивалентно наличию
(?s)
было бы
rg --multiline --multiline-dotall
И чтобы ответить на исходный вопрос, где они должны быть на отдельных строках:
$ rg --multiline 'abc.*[\n](\n|.)*efg' test.txt
И если вы хотите, чтобы это было «не жадным», чтобы вы не просто получили первый abc с последним efg (разделите их на пары):
$ rg --multiline 'abc.*[\n](\n|.)*?efg' test.txt
https://til.hashrocket.com/posts/9zneks2cbv-multiline-matches-with-ripgrep-rg
Если вам нужно, чтобы оба слова были близко друг к другу, например, не более 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
Команда, если вы хотите, также найти с помощью регулярных выражений.
Несколько дней назад я выпустил альтернативу grep, которая поддерживает это напрямую, либо с помощью многострочного сопоставления, либо с использованием условий - надеюсь, это будет полезно для некоторых людей, которые ищут здесь. Вот как будут выглядеть команды для примера:
Multiline: sift -lm 'abc.*efg' testfile
условия: sift -l 'abc' testfile --followed-by 'efg'
Вы также можете указать, что 'efg' должен следовать за 'abc' в определенном количестве строк:sift -l 'abc' testfile --followed-within 5:'efg'
Вы можете найти больше информации на http://sift-tool.org/.
К сожалению, вы не можете. От grep
документы:
grep ищет в именованных входных ФАЙЛАХ (или в стандартном вводе, если файлы не названы, или если в качестве имени файла указан один дефис-минус (-)) строки, содержащие соответствие данному ШАБЛОНУ.
Хотя опция sed самая простая и легкая, однострочная версия LJ, к сожалению, не самая портативная. Те, кто застрял с версией C Shell, должны избежать челки:
sed -e '/abc/,/efg/\!d' [file]
Это, к сожалению, не работает в Bash et al.
ag 'abc.*(\n|.)*efg'
похож на ответ на предъявителя кольца, но вместо этого используется ag. Скоростные преимущества серебряного искателя могли бы здесь проявиться.
Я использовал это, чтобы извлечь последовательность fasta из файла multi-fasta, используя опцию -P для grep:
grep -Pzo ">tig00000034[^>]+" file.fasta > desired_sequence.fasta
-P для поисков на основе Perl -z для окончания строки в 0 байт, а не для новой строки char -o, чтобы просто захватить совпадения, поскольку grep возвращает всю строку (что в данном случае, так как вы сделали -z, это весь файл). Основой регулярного выражения является [^>]
что означает "не больше, чем символ"
Вы можете использовать grep, если вы не заинтересованы в последовательности паттерна.
grep -l "pattern1" filepattern*.* | xargs grep "pattern2"
пример
grep -l "vector" *.cpp | xargs grep "map"
grep -l
найдет все файлы, которые соответствуют первому шаблону, а xargs будет grep для второго шаблона. Надеюсь это поможет.
#!/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
Если у вас есть некоторая оценка расстояния между двумя строками "abc" и "efg", которые вы ищете, вы можете использовать:
grep -r. -e 'abc' -A num1 -B num2 | grep 'efg'
Таким образом, первый grep вернет строку с "abc" плюс #num1 строки после него и #num2 строки после него, а второй grep просеет все эти строки, чтобы получить "efg". Тогда вы узнаете, в каких файлах они появляются вместе.
С ugrep, выпущенным несколько месяцев назад:
ugrep 'abc(\n|.)+?efg'
Этот инструмент оптимизирован по скорости. Он также совместим с GNU/BSD/PCRE-grep.
Обратите внимание, что мы должны использовать ленивое повторение +?
, если вы не хотите сопоставить все строки с efg
вместе до последнего efg
в файле.
У вас есть как минимум пара вариантов -
- ДОТАЛЛ метод
- используйте (?), чтобы ОБЪЕДИНИТЬ. символ для включения \ n
- вы также можете использовать опережающий просмотр (?=\n) - не будет захвачен в совпадении
пример-текст:
true
match me
false
match me one
false
match me two
true
match me three
third line!!
{BLANK_LINE}
команда:
grep -Pozi '(?s)true.+?\n(?=\n)' example-text
-p для регулярных выражений Perl -o, чтобы соответствовать только шаблону, а не всей строке -z, чтобы разрешить разрывы строк -i делает регистронезависимым
выход:
Примечания:
- +? makes modifier non-greedy so matches shortest string instead of largest (prevents from returning one match containing entire text)
- вы можете использовать ручной метод oldschool OG, используя \ n
команда:
grep -Pozi 'true(.|\n)+?\n(?=\n)'
выход:
true
match me
true
match me three
third line!!
Файл паттерна *.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
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
,
В качестве альтернативы ответу Балу Мохана, можно применять порядок шаблонов, используя только 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
th включительноgrep -n
- предварительно сопоставлять строки с номерами строкhead -n1
- печатать только первый рядcut -d : -f 1
- распечатать первый вырезанный столбец, используя:
как разделитель2>/dev/null
- тишинаtail
вывод ошибки, если$()
выражение возвращается пустымgrep -q
- тишинаgrep
и немедленно возвращаемся, если совпадение найдено, так как нас интересует только код выхода
Почему бы не что-нибудь простое вроде:
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 в той же или следующих строках, результат можно использовать в 'if")grep можно изменить на egrep и т. д., если требуется сопоставление с образцом
Использование любого awk и чтение в память только по одной строке за раз:
$ awk 'f && /efg/{print FILENAME; exit} /abc/{f=1}' file
file
Очевидно, вы можете изменить его, чтобы печатать все, что захотите, я просто предполагаю, что вы хотите напечатать имя файла.
Если вам нужен статус успешного/неуспешного выхода, как если бы вы получилиgrep
затем измените его на:
awk 'f && /efg/{f++; exit} /abc/{f=1} END{ if (f==2) { print FILENAME; exit 0 } else exit 1 }' file
или если вы хотите обрабатывать несколько входных файлов и ваш awk поддерживаетnextfile
:
awk 'FNR==1{f=0} f && /efg/{print FILENAME; nextfile} /abc/{f=1}' file1 file2 ...
и т. д...
Это тоже должно работать?!
perl -lpne 'print $ARGV if /abc.*?efg/s' file_list
$ARGV
содержит имя текущего файла при чтении из file_list
/s
Модификатор ищет по новой строке.
Для рекурсивного поиска по всем файлам (по нескольким строкам в каждом файле) с присутствием ОБЕИХ строк (т.е. строка1 и строка2 в разных строках и обе находятся в одном файле):
grep -r -l 'string1' * > tmp; while read p; do grep -l 'string2' $p; done < tmp; rm tmp
Для рекурсивного поиска по всем файлам (по нескольким строкам в каждом файле) при наличии ЛЮБОЙ строки (т.е. строка1 и строка2 в разных строках и либо в одном файле):
grep -r -l 'string1\|string2' *
Я считаю, что следующее должно работать и имеет преимущество использования только расширенных регулярных выражений без необходимости установки дополнительного инструмента, такого какpcregrep
если у вас его еще нет или у вас нет-P
доступна опция grep (например, macOS):
egrep -irzo “.*abc(.*\s.*){1,}.*efg.*" path_to_filenames
Предостережение: это имеет некоторые небольшие недостатки:
- он найдет самый большой набор строк от первой до последней в каждом файле, если только...
- есть несколько повторений
abc
[вещи]efg
шаблон в каждом файле.
Это должно работать:
cat FILE | egrep 'abc|efg'
Если найдено более одного совпадения, вы можете отфильтровать с помощью grep -v