Як знайти візерунки в декількох рядках за допомогою grep?


208

Я хочу знайти файли з "abc" AND "efg" у тому порядку, і ці два рядки знаходяться в різних рядках у цьому файлі. Напр .: файл із вмістом:

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

Слід відповідати.


Відповіді:


225

Греп недостатній для цієї операції.

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 () помилка -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]

4
Файли не знаходять, він повертає відповідну частину з одного файлу
shiggity

11
@Lj. будь ласка, можете пояснити цю команду? Мені знайоме sed, але якщо ніколи раніше не бачив такого виразу.
Ентоні

1
@Anthony, це задокументовано на сторінці людини sed, під адресою. Важливо усвідомити, що / abc / & / efg / - це адреса.
Кальмар

49
Я підозрюю, що ця відповідь була б корисною, якби вона мала трохи більше пояснень, і в такому випадку я б ще раз проголосувала за неї. Я знаю трохи sed, але недостатньо, щоб використати цю відповідь для створення значущого коду виходу після півгодини обертання. Порада: "RTFM" рідко отримує голоси на StackOverflow, як показує ваш попередній коментар.
Майкл Шепер

25
Швидке пояснення на прикладі: sed '1,5d': видалити рядки між 1 і 5. sed '1,5! D': видалити рядки не між 1 і 5 (тобто зберегти рядки між), то замість числа ви можете пошук рядка з / шаблоном /. Дивіться також простіший нижче: sed -n '/ abc /, / efg / p' p призначений для друку, а прапор -n не відображає всіх рядків
phil_w

86

Ось рішення, натхнене цією відповіддю :

  • якщо 'abc' та 'efg' можуть бути в одному рядку:

    grep -zl 'abc.*efg' <your list of files>
  • якщо 'abc' та 'efg' повинні бути в різних рядках:

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

Парами:

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

  • -l друкувати ім'я кожного вхідного файлу, з якого зазвичай було б надруковано вихід.

  • (?s)активуйте PCRE_DOTALL, що означає, що "." знаходить будь-який символ або новий рядок.


@syntaxerror Ні, я думаю, що це лише малі регістри l. AFAIK немає -1варіанту числа .
Sparhawk

Здається, ти маєш рацію, можливо, я зробив помилку на друку під час тестування. У будь-якому випадку вибачте, що проклали помилковий слід.
syntaxerror

6
Це чудово. У мене просто одне питання щодо цього. Якщо -zпараметр вказує grep для обробки нових рядків, zero byte charactersто тоді для чого нам потрібен (?s)регекс? Якщо це вже не новий рядок, чи не повинен він .мати можливість прямого зіставлення?
Durga Swaroop

1
-z (він же --null-data) і (? s) - саме те, що вам потрібно, щоб відповідати багаторядковій лінії зі стандартним грепом. Люди на MacOS, будь ласка, залишайте коментарі щодо наявності опцій -z або --null-data у ваших системах!
Zeke Fast

4
-z точно не доступний на MacOS
Ділан Ніколсон

33

sed має бути достатньо, як плакат LJ, зазначений вище,

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

sed -n '/abc/,/efg/p' file

16

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

На прикладі питання щодо ОП, я думаю, що наступні варіанти чудово працюють, і другий найкращий варіант відповідає тому, як я розумію питання:

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

Я скопіював текст як / tmp / test1 і видалив 'g' та зберег у вигляді / tmp / test2. Ось висновок, який показує, що перший показує відповідні рядки, а другий показує лише ім'я файлу (типово -o означає показ відповідності, а типовий -l - показ лише імені файлу). Зауважте, що "z" необхідний для багаторядкових, а "(. | \ N)" означає, щоб відповідати "нічого, крім нового рядка", або "нового рядка" - тобто нічого:

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.
киб

7

awk one-liner:

awk '/abc/,/efg/' [file-with-content]

4
Це буде щасливо друкувати від abcкінця до кінця файлу, якщо у файлі відсутній шаблон закінчення або відсутній останній кінцевий шаблон. Ви можете це виправити, але це значно ускладнить сценарій.
трійка

Як виключити /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

Знайдена друга відповідь була корисною для вилучення цілого багаторядкового блоку з сірниками на пару рядків - .*?для отримання мінімальної відповідності довелося використовувати не жадібну відповідність ( ).
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. Якщо ви хочете вирішити це, додайте, якщо це доречно, у / abc / case тощо.


3

На жаль, ви не можете. З grepдокументів:

grep шукає названі вхідні ФАЙЛИ (або стандартне введення, якщо жодні файли не названі, або якщо один імфічний мінус (-) задано як ім'я файлу) для рядків, що містять відповідність даному ПАТЕРНУ.


про щоgrep -Pz
Наваро

3

Якщо ви готові використовувати контексти, цього можна досягти, набравши текст

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

Це покаже все між "abc" та "efg", якщо вони знаходяться в межах 500 рядків один від одного.


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, яка підтримує це безпосередньо, або шляхом узгодження кількох рядків, або за допомогою умов - сподіваємось, це корисно для деяких людей, які шукають тут. Ось як виглядатимуть команди для прикладу:

Багаторядковий:

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 .


Я не думаю, що перший приклад sift -lm 'abc.*efg' testfileспрацьовує, тому що збіг жадібний і збиває всі рядки до останнього efgу файлі.
доктор Алекс RE

2

Хоча варіант sed є найпростішим і найпростішим, одножильний LJ, на жаль, не самий портативний. Тим, хто застряг у версії C Shell, потрібно буде уникнути чуб:

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

Це, на жаль, не працює в bash та ін.


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

1

ви можете використовувати греп, якщо ви не зацікавлені в послідовності шаблону.

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'

подібний до відповіді носія дзвінка, але замість нього. Переваги швидкості срібного пошуку тут, можливо, світяться.


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 -qтеж не зробить трюк?
синтаксис-помилка

1
&>вказує bash переспрямовувати як стандартний вихід, так і стандартну помилку, див. ПОВЕРНЕННЯ в посібнику з bash. Ви дуже праві в тому, що ми могли б так само добре зробити grep -q ...замість того grep ... &>/dev/null, щоб добре ловити!
Еміль Лундберг

Так і думав. Заберуть біль від багато незручного набору тексту. Дякую за пояснення - тому я, мабуть, трохи пропустив у посібнику. (Подивився щось віддалене, пов’язане з цим деякий час тому.) --- Ви можете навіть розглянути можливість змінити це у своїй відповіді. :)
syntaxerror

0

Це теж має працювати ?!

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

$ARGVмістить ім'я поточного файлу під час читання з file_list /sпошуку модифікаторів у новому рядку.


0

Файл *.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) кількість ліній. Якщо збіг було знайдено (тест -n ...), знайдіть останню відповідність efg (знайдіть усіх і візьміть останню із хвостом -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 тощо, якщо потрібне узгодження шаблону


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у файлі.


-3

Це має працювати:

cat FILE | egrep 'abc|efg'

Якщо є більше одного матчу, ви можете відфільтрувати за допомогою grep -v


2
У той час як цей фрагмент коду вітається, і може забезпечити деяку допомогу, було б значно покращено , якщо вона була придбана пояснення про те , як і те, чому це вирішує проблему. Пам’ятайте, що ви відповідаєте на запитання читачів у майбутньому, а не лише про людину, яка зараз запитує! Будь ласка, відредагуйте свою відповідь, щоб додати пояснення та вказати, які обмеження та припущення застосовуються.
Toby Speight

1
Насправді це не пошук у кількох рядках , як зазначено в запитанні.
n.st
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.