Come trovare modelli su più linee usando grep?


208

Voglio trovare i file che hanno "abc" E "efg" in quell'ordine, e quelle due stringhe sono su righe diverse in quel file. Ad esempio: un file con contenuto:

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

Dovrebbe essere abbinato.


Risposte:


225

Grep non è sufficiente per questa operazione.

pcregrep che si trova nella maggior parte dei moderni sistemi Linux può essere usato come

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

dove -M, --multiline consentire ai pattern di abbinare più di una riga

C'è anche un nuovo pcre2grep . Entrambi sono forniti dal progetto PCRE .

pcre2grep è disponibile per Mac OS X tramite porte Mac come parte della porta pcre2:

% sudo port install pcre2 

e via Homebrew come:

% brew install pcre

o per pcre2

% brew install pcre2

pcre2grep è disponibile anche su Linux (Ubuntu 18.04+)

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

11
@StevenLu -M, --multiline: consente ai pattern di abbinare più di una riga.
portatore dell'anello

7
Nota che. * (\ N |.) * È equivalente a (\ n |.) * E quest'ultimo è più corto. Inoltre sul mio sistema, "pcre_exec () error -8" si verifica quando eseguo la versione più lunga. Quindi prova 'abc (\ n |.) * Efg' invece!
daveagp,

6
In questo caso devi rendere l'espressione non avida:'abc.*(\n|.)*?efg'
portatore dell'anello

4
e puoi omettere il primo .*-> 'abc(\n|.)*?efg'per accorciare il regex (ed essere pedante)
Michi

6
pcregreprende le cose più facili, ma grepfunzionerà anche. Ad esempio, vedi stackoverflow.com/a/7167115/123695
Michael Mior,

113

Non sono sicuro che sia possibile con grep, ma sed lo rende molto semplice:

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

4
Questo non trova i file, restituisce la parte corrispondente da un singolo file
shiggity

11
@Lj. per favore puoi spiegare questo comando? Conosco bene sed, ma se non avessi mai visto una simile espressione prima.
Anthony,

1
@Anthony, è documentato nella pagina man di sed, sotto l'indirizzo. È importante rendersi conto che / abc / & / efg / è un indirizzo.
Calamari

49
Sospetto che questa risposta sarebbe stata utile se avesse avuto qualche spiegazione in più, e in quel caso l'avrei votata ancora una volta. Conosco un po 'di sed, ma non abbastanza per usare questa risposta per produrre un codice di uscita significativo dopo mezz'ora di giocherellare. Suggerimento: "RTFM" raramente ottiene voti positivi su StackOverflow, come mostra il tuo commento precedente.
Michael Scheper,

25
Spiegazione rapida con l'esempio: sed '1,5d': elimina le righe tra 1 e 5. sed '1,5! D': elimina le righe non comprese tra 1 e 5 (ovvero mantieni le righe tra) quindi, anziché un numero, puoi cerca una linea con / pattern /. Vedi anche quello più semplice qui sotto: sed -n '/ abc /, / efg / p' p è per la stampa e la bandiera -n non mostra tutte le righe
phil_w

87

Ecco una soluzione ispirata a questa risposta :

  • se 'abc' ed 'efg' possono essere sulla stessa riga:

    grep -zl 'abc.*efg' <your list of files>
  • se 'abc' ed 'efg' devono trovarsi su linee diverse:

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

Parametri:

  • -zConsidera l'input come un insieme di linee, ciascuna terminata da un byte zero anziché da una nuova riga. ie grep considera l'input come un'unica linea.

  • -l stampa il nome di ciascun file di input dal quale normalmente sarebbe stato stampato l'output.

  • (?s)attivare PCRE_DOTALL, che significa che '.' trova qualsiasi personaggio o newline.


@syntaxerror No, penso sia solo una minuscola l. AFAIK non esiste -1un'opzione numerica .
Sparhawk,

Sembra che tu abbia ragione dopo tutto, forse avevo fatto un refuso durante il test. In ogni caso mi dispiace per aver tracciato una falsa pista.
syntaxerror,

6
Questo è eccellente Ho solo una domanda al riguardo. Se le -zopzioni specificano grep per trattare le nuove righe come zero byte charactersallora perché abbiamo bisogno (?s)di regex? Se è già un personaggio non newline, non dovresti .riuscire ad abbinarlo direttamente?
Durga Swaroop,

1
-z (aka --null-data) e (? s) sono esattamente ciò di cui hai bisogno per abbinare la multilinea con un grep standard. Persone su MacOS, lasciate commenti sulla disponibilità delle opzioni -z o --null-data sui vostri sistemi!
Zeke Fast,

4
-z sicuramente non disponibile su MacOS
Dylan Nicholson il

33

sed dovrebbe essere sufficiente come indicato sopra nel poster LJ,

invece di! d puoi semplicemente usare p per stampare:

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

16

Ho fatto molto affidamento su pcregrep, ma con grep più recente non è necessario installare pcregrep per molte delle sue funzionalità. Basta usaregrep -P .

Nell'esempio della domanda del PO, penso che le seguenti opzioni funzionino bene, con la seconda migliore corrispondenza per capire la domanda:

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

Ho copiato il testo come / tmp / test1 e cancellato 'g' e salvato come / tmp / test2. Ecco l'output che mostra che il primo mostra la stringa corrispondente e il secondo mostra solo il nome file (tipico -o è mostrare la corrispondenza e tipico -l è mostrare solo il nome file). Si noti che "z" è necessario per multilinea e che "(. | \ N)" corrisponde a "qualsiasi cosa diversa da newline" o "newline", ovvero qualsiasi cosa:

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

Per determinare se la tua versione è abbastanza nuova, esegui man grepe vedi se qualcosa di simile a questo appare nella parte superiore:

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

Questo è da GNU grep 2.10.


14

Questo può essere fatto facilmente usando prima trper sostituire le nuove righe con qualche altro personaggio:

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

Qui sto usando il carattere di allarme, \a(ASCII 7) al posto di una nuova riga. Questo non si trova quasi mai nel tuo testo e greppuò abbinarlo con un ., o abbinarlo specificamente con \a.


1
Questo era il mio approccio, ma lo stavo usando \0e quindi avevo bisogno grep -ae corrispondevo su \x00... Mi hai aiutato a semplificare! echo $log | tr '\n' '\0' | grep -aoE "Error: .*?\x00Installing .*? has failed\!" | tr '\0' '\n'è oraecho $log | tr '\n' '\a' | grep -oE "Error: .*?\aInstalling .*? has failed\!" | tr '\a' '\n'
Charlie Gorichanaz,

1
Usa grep -o.
Kyb

7

awk one-liner:

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

4
Questo verrà stampato felicemente abcfino alla fine del file se il modello finale non è presente nel file o manca l'ultimo modello finale. Puoi risolverlo, ma complicherà in modo significativo lo script.
Tripleee

Come escludere /efg/dall'output?
Kyb,

6

Puoi farlo molto facilmente se puoi usare Perl.

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

Puoi farlo anche con una singola espressione regolare, ma ciò comporta il prendere l'intero contenuto del file in una singola stringa, che potrebbe finire per occupare troppa memoria con file di grandi dimensioni. Per completezza, ecco quel metodo:

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

La seconda risposta trovata è stata utile per estrarre un intero blocco multi-linea con corrispondenze su un paio di righe - è stato necessario utilizzare la corrispondenza non avida ( .*?) per ottenere una corrispondenza minima.
RichVel

5

Non so come farei con grep, ma farei qualcosa del genere con awk:

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

Devi stare attento a come lo fai, però. Vuoi che il regex corrisponda alla sottostringa o all'intera parola? aggiungere tag \ w come appropriato. Inoltre, sebbene ciò sia strettamente conforme al modo in cui hai affermato l'esempio, non funziona del tutto quando abc appare una seconda volta dopo efg. Se vuoi gestirlo, aggiungi un if come appropriato in / abc / case ecc.


3

Purtroppo non puoi. Dai grepdocumenti:

grep cerca nei FILE di input nominati (o input standard se non è stato nominato alcun file o se viene dato un singolo trattino-meno (-) come nome del file) per le righe che contengono una corrispondenza con il PATTERN dato.


che diregrep -Pz
Navaro

3

Se sei disposto a utilizzare i contesti, questo potrebbe essere ottenuto digitando

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

Questo mostrerà tutto tra "abc" e "efg", purché si trovino entro 500 righe l'una dall'altra.


3

Se hai bisogno che entrambe le parole siano vicine tra loro, ad esempio non più di 3 righe, puoi farlo:

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

Stesso esempio ma filtrando solo i file * .txt:

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

E anche puoi sostituire il grepcomando con il egrepcomando se vuoi trovare anche con le espressioni regolari.


3

Qualche giorno fa ho rilasciato un'alternativa grep che supporta direttamente questa opzione, tramite la corrispondenza multilinea o utilizzando le condizioni - si spera sia utile per alcune persone che effettuano ricerche qui. Ecco come sarebbero i comandi per l'esempio:

multilinea:

sift -lm 'abc.*efg' testfile

condizioni:

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

Puoi anche specificare che 'efg' deve seguire 'abc' entro un certo numero di righe:

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

Puoi trovare maggiori informazioni su sift-tool.org .


Non credo che il primo esempio sift -lm 'abc.*efg' testfilefunzioni, perché la partita è avida e divora tutte le righe fino all'ultima efgnel file.
Dr. Alex RE

2

Mentre l'opzione sed è la più semplice e la più semplice, il one-liner di LJ non è purtroppo il più portatile. Coloro che sono bloccati con una versione della C Shell dovranno sfuggire ai loro colpi:

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

Questo purtroppo non funziona in 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

1

puoi usare grep in caso non ti piaccia la sequenza del pattern.

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

esempio

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

grep -ltroverà tutti i file che corrispondono al primo modello, e xargs farà il grep per il secondo modello. Spero che questo ti aiuti.


1
Ciò ignorerebbe l'ordine "pattern1" e "pattern2" compaiono nel file, tuttavia - OP specifica in modo specifico che solo i file in cui appare "pattern2" DOPO "pattern1" devono essere abbinati.
Emil Lundberg,

1

Con cercatore d'argento :

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

simile alla risposta del portatore dell'anello, ma con ag invece. I vantaggi di velocità di Silver Searcher potrebbero risplendere qui.


1
Questo non sembra funzionare. (echo abctest; echo efg)|ag 'abc.*(\n|.)*efg'non corrisponde
phiresky

1

Ho usato questo per estrarre una sequenza fasta da un file multi-fasta usando l'opzione -P per grep:

grep -Pzo ">tig00000034[^>]+"  file.fasta > desired_sequence.fasta
  • P per ricerche basate su perl
  • z per far terminare una riga in 0 byte anziché nel carattere newline
  • o per catturare ciò che corrisponde poiché grep restituisce l'intera riga (che in questo caso da quando hai fatto -z è l'intero file).

Il nucleo del regexp è quello [^>]che si traduce in "non maggiore del simbolo"


0

Come alternativa alla risposta di Balu Mohan, è possibile far rispettare l'ordine degli schemi utilizzando solo grep, heade 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

Questo non è molto carino, però. Formattato in modo più leggibile:

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

Questo stamperà i nomi di tutti i file in cui "pattern2"appare dopo "pattern1", o dove sia appaiono sulla stessa linea :

$ 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

Spiegazione

  • tail -n +i- stampa tutte le righe dopo il ith, incluso
  • grep -n - anteporre le righe corrispondenti ai rispettivi numeri di riga
  • head -n1 - stampa solo la prima riga
  • cut -d : -f 1- stampa la prima colonna tagliata usando :come delimitatore
  • 2>/dev/null- tailoutput dell'errore di silenzio che si verifica se l' $()espressione ritorna vuota
  • grep -q- silenzio grepe ritorna immediatamente se viene trovata una corrispondenza, poiché siamo interessati solo al codice di uscita

Qualcuno può spiegare il &>? Lo sto usando anche io, ma non l'ho mai visto documentato da nessuna parte. A proposito, perché dobbiamo tacere grep in quel modo, in realtà? grep -qnon farà anche il trucco?
syntaxerror,

1
&>dice a bash di reindirizzare sia l'output standard sia l'errore standard, vedere REDIRECTION nel manuale di bash. Sei molto proprio in quella che potremmo benissimo fare grep -q ..., invece di grep ... &>/dev/null, buona pesca!
Emil Lundberg,

Pensavo così. Porterà via il dolore di un sacco di goffa digitazione extra. Grazie per la spiegazione, quindi devo aver saltato un po 'il manuale. (Ho cercato qualcosa di remoto in esso collegato qualche tempo fa.) --- Potresti anche considerare di cambiarlo nella tua risposta. :)
syntaxerror

0

Anche questo dovrebbe funzionare ?!

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

$ARGVcontiene il nome del file corrente durante la lettura dalle file_list /sricerche del modificatore su newline.


0

Il filepattern *.shè importante per impedire che le directory vengano ispezionate. Naturalmente alcuni test potrebbero impedire anche quello.

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

Il

grep -n -m1 abc $f 

cerca al massimo 1 corrispondenza e restituisce (-n) il numero di biancheria. Se è stata trovata una corrispondenza (prova -n ...) trova l'ultima partita di efg (trova tutto e prendi l'ultima con tail -n 1).

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

altro continua.

Dato che il risultato è qualcosa di simile, 18:foofile.sh String alf="abc";dobbiamo tagliare da ":" fino alla fine della riga.

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

Dovrebbe restituire un risultato positivo se l'ultima corrispondenza della seconda espressione ha superato la prima corrispondenza della prima.

Quindi riportiamo il nome del file echo $f.


0

Perché non qualcosa di semplice come:

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

restituisce 0 o un numero intero positivo.

egrep -o (mostra solo le corrispondenze, trucco: più corrispondenze sulla stessa riga producono output su più righe come se fossero su righe diverse)

  • grep -A1 abc (stampa abc e la riga successiva)

  • grep efg | wc -l (Conteggio 0-n delle righe efg trovate dopo abc sulle stesse righe o seguenti, il risultato può essere utilizzato in un 'if ")

  • grep può essere cambiato in egrep ecc. se è necessaria la corrispondenza del modello


0

Se hai qualche stima sulla distanza tra le 2 stringhe 'abc' e 'efg' che stai cercando, potresti usare:

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

In questo modo, il primo grep restituirà la riga con 'abc' più # num1 righe dopo di essa, e # num2 righe dopo di essa, e il secondo grep passerà in rassegna tutte quelle per ottenere 'efg'. Quindi saprai a quali file compaiono insieme.


0

Con ugrep rilasciato alcuni mesi fa:

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

Questo strumento è altamente ottimizzato per la velocità. È anche compatibile GNU / BSD / PCRE-grep.

Nota che dovremmo usare una ripetizione pigra +?, a meno che tu non voglia abbinare tutte le righe efginsieme fino all'ultima efgnel file.


-3

Questo dovrebbe funzionare:

cat FILE | egrep 'abc|efg'

Se esiste più di una corrispondenza, puoi filtrare usando grep -v


2
Sebbene questo frammento di codice sia il benvenuto e possa fornire qualche aiuto, sarebbe notevolmente migliorato se includesse una spiegazione di come e perché questo risolve il problema. Ricorda che stai rispondendo alla domanda per i lettori in futuro, non solo per la persona che chiede ora! Si prega di modificare la risposta di aggiungere una spiegazione, e dare un'indicazione di ciò si applicano le limitazioni e le assunzioni.
Toby Speight,

1
Questo in realtà non cerca su più righe , come indicato nella domanda.
n.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.