Jak znaleźć wzory w wielu liniach za pomocą grep?


208

Chcę znaleźć pliki, które mają „abc” ORAZ „efg” w tej kolejności, a te dwa ciągi znajdują się w różnych wierszach tego pliku. Np .: plik z zawartością:

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

Powinny być dopasowane.


Odpowiedzi:


225

Grep nie jest wystarczający do tej operacji.

pcregrep, który znajduje się w większości współczesnych systemów Linux, może być używany jako

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

gdzie -M, --multiline pozwól wzorom dopasować więcej niż jedną linię

Istnieje również nowszy pcre2grep . Oba są dostarczane przez projekt PCRE .

pcre2grep jest dostępny dla Mac OS X poprzez porty Mac jako część portu pcre2:

% sudo port install pcre2 

i za pośrednictwem Homebrew jako:

% brew install pcre

lub dla pcre2

% brew install pcre2

pcre2grep jest również dostępny w systemie Linux (Ubuntu 18.04+)

$ sudo apt install pcre2-utils # PCRE2
$ sudo apt install pcregrep    # Older PCRE

11
@StevenLu -M, --multiline- Zezwalaj wzorom na dopasowanie więcej niż jednej linii.
okaziciela pierścienia

7
Zauważ, że. * (\ N |.) * Jest równoważne z (\ n |.) *, A ten drugi jest krótszy. Ponadto w moim systemie występuje błąd „pcre_exec () -8”, gdy uruchamiam dłuższą wersję. Więc spróbuj zamiast tego „abc (\ n |.) * Efg”!
daveagp

6
W tym przypadku musisz uczynić wyrażenie niepochodzące:'abc.*(\n|.)*?efg'
nośnik pierścienia

4
i możesz pominąć pierwsze .*-> 'abc(\n|.)*?efg'skrócenie wyrażenia regularnego (i być pedantycznym)
Michi

6
pcregrepułatwia rzeczy, ale grepteż działa. Na przykład patrz stackoverflow.com/a/7167115/123695
Michael Mior

113

Nie jestem pewien, czy jest to możliwe z grep, ale sed bardzo ułatwia:

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

4
Nie znajduje plików, zwraca pasującą część z jednego pliku
shiggity,

11
@Lj. czy możesz wyjaśnić to polecenie? Znam to sed, ale jeśli nigdy wcześniej nie widziałem takiego wyrażenia.
Anthony

1
@Anthony, Jest to udokumentowane na stronie podręcznika użytkownika sed, pod adresem. Ważne jest, aby zdać sobie sprawę, że / abc / & / efg / to adres.
Squidly

49
Podejrzewam, że ta odpowiedź byłaby pomocna, gdyby zawierała nieco więcej wyjaśnień, a w takim przypadku podniosłabym ją jeszcze raz. Znam trochę sed, ale nie na tyle, aby użyć tej odpowiedzi, aby stworzyć znaczący kod wyjścia po pół godzinie majstrowania. Wskazówka: „RTFM” rzadko otrzymuje więcej głosów na StackOverflow, jak pokazuje twój poprzedni komentarz.
Michael Scheper,

25
Szybkie wyjaśnienie na przykładzie: sed '1,5d': usuń linie między 1 a 5. sed '1,5! D': usuń linie nie między 1 a 5 (tzn. Zachowaj linie między), a następnie zamiast liczby możesz: wyszukaj linię za pomocą / pattern /. Zobacz także prostszy poniżej: sed -n '/ abc /, / efg / p' p jest do wydruku, a flaga -n nie wyświetla wszystkich linii
phil_w

86

Oto rozwiązanie inspirowane tą odpowiedzią :

  • jeśli „abc” i „efg” mogą znajdować się w tej samej linii:

    grep -zl 'abc.*efg' <your list of files>
  • jeśli „abc” i „efg” muszą znajdować się w różnych wierszach:

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

Params:

  • -zTraktuj dane wejściowe jako zestaw wierszy zakończonych zerowym bajtem zamiast nowego wiersza. tzn. grep traktuje dane wejściowe jako jedną dużą linię.

  • -l wypisz nazwę każdego pliku wejściowego, z którego normalnie wydrukowano by wyjście.

  • (?s)aktywuj PCRE_DOTALL, co oznacza, że ​​„.” znajduje dowolny znak lub znak nowej linii.


@syntaxerror Nie, myślę, że to tylko małe litery l. AFAIK nie ma -1opcji numeru .
Sparhawk

Wygląda na to, że masz rację, może popełniłem literówkę podczas testowania. W każdym razie przepraszam za złożenie fałszywego śladu.
syntaxerror

6
To jest wspaniałe. Mam tylko jedno pytanie dotyczące tego. Jeśli -zopcje określają grep, aby traktować znaki nowej linii, zero byte charactersto dlaczego potrzebujemy (?s)wyrażenia regularnego? Jeśli jest to znak inny niż nowy wiersz, nie powinien .być w stanie dopasować go bezpośrednio?
Durga Swaroop

1
-z (aka --null-data) i (? s) są dokładnie tym, czego potrzebujesz, aby dopasować wiele linii do standardowego grep. Ludzie na MacOSie, zostawcie komentarze na temat dostępności opcji -z lub --null-data w waszych systemach!
Zeke Fast,

4
-z zdecydowanie niedostępne na MacOS
Dylan Nicholson

33

sed powinno wystarczyć, jak napisano powyżej plakat LJ,

zamiast! d możesz po prostu użyć p, aby wydrukować:

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

16

W dużej mierze polegałem na pcregrep, ale w nowszym grep nie musisz instalować pcregrep dla wielu jego funkcji. Po prostu użyj grep -P.

W przykładzie pytania PO myślę, że następujące opcje działają dobrze, a drugi najlepiej pasuje do tego, jak rozumiem pytanie:

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

Skopiowałem tekst jako / tmp / test1 i usunąłem „g” i zapisałem jako / tmp / test2. Oto wynik pokazujący, że pierwszy pokazuje pasujący ciąg, a drugi pokazuje tylko nazwę pliku (typowe -o ma pokazywać dopasowanie, a typowe -l pokazuje tylko nazwę pliku). Zauważ, że „z” jest konieczne dla multilinii, a „(. | \ N)” oznacza dopasowanie „cokolwiek innego niż nowa linia” lub „nowa linia” - tj. Cokolwiek:

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

Aby ustalić, czy Twoja wersja jest wystarczająco nowa, uruchom man grepi sprawdź, czy coś podobnego do tego pojawia się u góry:

   -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.

To pochodzi z GNU grep 2.10.


14

Można to łatwo zrobić, najpierw trzastępując znaki nowej linii inną postacią:

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

Tutaj używam znaku alarmu \a(ASCII 7) zamiast nowego wiersza. Tego prawie nigdy nie można znaleźć w tekście i grepmożna go dopasować za pomocą .lub specjalnie do niego \a.


1
To było moje podejście, ale używałem, \0a więc potrzebowałem grep -ai dopasowywałem \x00… Pomogłeś mi uprościć! echo $log | tr '\n' '\0' | grep -aoE "Error: .*?\x00Installing .*? has failed\!" | tr '\0' '\n'jest terazecho $log | tr '\n' '\a' | grep -oE "Error: .*?\aInstalling .*? has failed\!" | tr '\a' '\n'
Charlie Gorichanaz

1
Zastosowanie grep -o.
Kyb

7

awk one-liner:

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

4
Zostanie to wydrukowane szczęśliwie od abckońca do końca pliku, jeśli wzorzec końcowy nie jest obecny w pliku lub brakuje ostatniego wzorca końcowego. Możesz to naprawić, ale to znacznie skomplikuje skrypt.
tripleee

Jak wykluczyć /efg/z produkcji?
Kyb

6

Możesz to zrobić bardzo łatwo, jeśli możesz użyć Perla.

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

Możesz to zrobić również za pomocą jednego wyrażenia regularnego, ale wymaga to przeniesienia całej zawartości pliku do jednego ciągu, co może zająć zbyt dużo pamięci w przypadku dużych plików. Dla kompletności, oto ta metoda:

perl -e '@lines = <>; $content = join("", @lines); print "Found in $ARGV\n" if ($content =~ /abc.*efg/s);' yourfilename.txt

Znaleziona druga odpowiedź była przydatna do wyodrębnienia całego bloku wielowierszowego z dopasowaniami w kilku liniach - musiałem użyć chciwego dopasowywania ( .*?), aby uzyskać minimalne dopasowanie.
RichVel,

5

Nie wiem, jak zrobiłbym to z grep, ale zrobiłbym coś takiego z awk:

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

Musisz jednak uważać, jak to robisz. Czy chcesz, aby wyrażenie regularne pasowało do podłańcucha czy całego słowa? dodaj odpowiednio tagi \ w. Ponadto, chociaż jest to ściśle zgodne z tym, co podałeś w przykładzie, nie działa całkiem, gdy abc pojawia się drugi raz po efg. Jeśli chcesz sobie z tym poradzić, dodaj jeśli to właściwe w / abc / case itp.


3

Niestety nie możesz. Z grepdokumentów:

grep przeszukuje nazwane PLIKI wejściowe (lub standardowe wejście, jeśli nie ma nazw plików lub jeśli jako nazwę pliku podano pojedynczy łącznik minus (-)) w poszukiwaniu linii zawierających dopasowanie do podanego WZORCA.


a co zgrep -Pz
Navaro

3

Jeśli chcesz używać kontekstów, możesz to osiągnąć, pisząc

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

Spowoduje to wyświetlenie wszystkiego między „abc” i „efg”, o ile znajdują się w odległości 500 linii od siebie.


3

Jeśli potrzebujesz, aby oba słowa były blisko siebie, na przykład nie więcej niż 3 linie, możesz to zrobić:

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

Ten sam przykład, ale filtrowanie tylko plików * .txt:

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

A także możesz zamienić greppolecenie na egreppolecenie, jeśli chcesz również znaleźć wyrażenia regularne.


3

Kilka dni temu wydałem alternatywę grep, która obsługuje to bezpośrednio, albo poprzez dopasowanie wieloliniowe, albo przy użyciu warunków - mam nadzieję, że przyda się niektórym osobom szukającym tutaj. Tak wyglądałyby polecenia dla przykładu:

Multiline:

sift -lm 'abc.*efg' testfile

Warunki:

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

Możesz również określić, że „efg” musi podążać za „abc” w określonej liczbie wierszy:

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

Możesz znaleźć więcej informacji na sift-tool.org .


Nie sądzę, żeby pierwszy przykład sift -lm 'abc.*efg' testfilezadziałał, ponieważ dopasowanie jest zachłanne i pochłania wszystkie linie aż do ostatniego efgw pliku.
Dr Alex RE

2

Podczas gdy opcja sed jest najprostsza i najłatwiejsza, jednowarstwowa LJ niestety nie jest najbardziej przenośna. Ci, którzy utknęli z wersją pocisku C, będą musieli uciec od grzywki:

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

To niestety nie działa w bash i in.


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

możesz użyć polecenia grep, ponieważ nie jesteś zainteresowany sekwencją wzoru.

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

przykład

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

grep -lznajdzie wszystkie pliki, które pasują do pierwszego wzorca, a xargs będzie grepował dla drugiego wzorca. Mam nadzieję że to pomoże.


1
To by zignorowało kolejność „wzorzec1” i „wzorzec2” pojawiające się w pliku, jednak - OP wyraźnie określa, że ​​tylko pliki, w których „wzorzec2” pojawia się PO „wzorzec1”, powinny zostać dopasowane.
Emil Lundberg,

1

Ze srebrnym wyszukiwarką :

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

podobny do odpowiedzi na okaziciela dzwonka, ale zamiast niego z ag. Korzyści płynące ze srebrnej wyszukiwarki mogą tu zabłysnąć.


1
To nie wydaje się działać. (echo abctest; echo efg)|ag 'abc.*(\n|.)*efg'nie pasuje
phiresky

1

Użyłem tego do wyodrębnienia sekwencji fasta z pliku multi fasta przy użyciu opcji -P dla grep:

grep -Pzo ">tig00000034[^>]+"  file.fasta > desired_sequence.fasta
  • P dla wyszukiwań opartych na perlu
  • z do tworzenia końca linii w 0 bajtach zamiast znaku nowej linii
  • o po prostu przechwytuje to, co pasuje, ponieważ grep zwraca całą linię (co w tym przypadku, ponieważ zrobiłeś -z, to cały plik).

Rdzeniem wyrażenia regularnego jest to, [^>]co przekłada się na „nie większy niż symbol”


0

Jako alternatywę dla odpowiedzi Balu Mohana, możliwe jest, aby wymusić kolejność wzorów przy użyciu tylko grep, headi 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

Ten jednak nie jest zbyt ładny. Sformatowane bardziej czytelnie:

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

To wypisuje nazwy wszystkich plików, w których "pattern2"pojawia się po "pattern1", lub w przypadku gdy obie pojawiają się na tej samej linii :

$ 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

Wyjaśnienie

  • tail -n +i- wydrukuj wszystkie wiersze po ith, włącznie
  • grep -n - poprzedzać pasujące linie ich numerami linii
  • head -n1 - wydrukuj tylko pierwszy rząd
  • cut -d : -f 1- wydrukuj pierwszą wyciętą kolumnę, używając :jako separatora
  • 2>/dev/null- tailwyjście błędu ciszy, które występuje, jeśli $()wyrażenie zwróci puste
  • grep -q- milcz grepi wróć natychmiast, jeśli zostanie znalezione dopasowanie, ponieważ interesuje nas tylko kod wyjścia

Czy ktoś może wyjaśnić &>? Ja też go używam, ale nigdzie go nie udokumentowałem. BTW, dlaczego właściwie musimy tak uciszyć grep? grep -qteż nie zrobi tej sztuczki?
syntaxerror

1
&>nakazuje bashowi przekierowanie zarówno standardowego wyjścia, jak i standardowego błędu, zobacz REDIRECTION w instrukcji bash. Jesteś bardzo rację, że możemy równie dobrze zrobić grep -q ...zamiast grep ... &>/dev/null, dobry połów!
Emil Lundberg,

Tak myślałem. Pozbędzie się bólu związanego z nieporęcznym dodatkowym pisaniem. Dzięki za wyjaśnienie - musiałem więc trochę pominąć instrukcję. (Jakiś czas temu
odszukałem

0

To też powinno działać ?!

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

$ARGVzawiera nazwę bieżącego pliku podczas czytania z file_list /swyszukiwań modyfikatorów w nowej linii.


0

Plik *.shjest ważny, aby zapobiec przeglądaniu katalogów. Oczywiście niektóre testy mogłyby temu zapobiec.

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 

wyszukuje maksymalnie 1 pasujące i zwraca (-n) numer bielizny. Jeśli znaleziono dopasowanie (test -n ...), znajdź ostatnie dopasowanie efg (znajdź wszystko i weź ostatnie z tail -n 1).

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

jeszcze dalej.

Ponieważ wynik jest podobny 18:foofile.sh String alf="abc";, musimy odciąć „:” do końca linii.

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

Powinien zwrócić wynik dodatni, jeśli ostatnie dopasowanie 2. wyrażenia minęło pierwsze dopasowanie pierwszego.

Następnie zgłaszamy nazwę pliku echo $f.


0

Dlaczego nie coś prostego, takiego jak:

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

zwraca 0 lub dodatnią liczbę całkowitą.

egrep -o (Pokazuje tylko dopasowania, trick: wiele dopasowań w tym samym wierszu daje wynik wieloliniowy, tak jakby były w różnych wierszach)

  • grep -A1 abc (wypisz abc i wiersz po nim)

  • grep efg | wc -l (0-n liczba linii efg znalezionych po abc w tej samej lub kolejnych liniach, wynik może być użyty w „jeśli”)

  • grep można zmienić na egrep itp., jeśli potrzebne jest dopasowanie wzorca


0

Jeśli masz jakieś oszacowanie odległości między dwoma ciągami „abc” i „efg”, którego szukasz, możesz użyć:

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

W ten sposób pierwszy grep zwróci linię z „abc” plus # num1 linii po niej i # num2 linii po niej, a drugi grep przesieje wszystkie te, aby uzyskać „efg”. Wtedy będziesz wiedział, w których plikach pojawiają się razem.


0

Z ugrep wydanym kilka miesięcy temu:

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

To narzędzie jest wysoce zoptymalizowane pod kątem szybkości. Jest także kompatybilny z GNU / BSD / PCRE-grep.

Pamiętaj, że powinniśmy używać leniwego powtarzania +?, chyba że chcesz dopasować wszystkie linie efgrazem do ostatniej efgw pliku.


-3

To powinno działać:

cat FILE | egrep 'abc|efg'

Jeśli jest więcej niż jedno dopasowanie, możesz je odfiltrować za pomocą grep -v


2
Chociaż ten fragment kodu jest mile widziany i może stanowić pewną pomoc, zostałby znacznie ulepszony, gdyby zawierał wyjaśnienie, w jaki sposób i dlaczego rozwiązuje problem. Pamiętaj, że odpowiadasz na pytanie czytelników w przyszłości, a nie tylko osoby, która zadaje teraz pytanie! Proszę edytować swoje odpowiedzi, aby dodać wyjaśnienie, i dać wskazówkę co zastosować ograniczenia i założenia.
Toby Speight

1
To nie wyszukuje w wielu wierszach , jak stwierdzono w pytaniu.
n.
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.