Como encontrar padrões em várias linhas usando grep?

208

Quero encontrar arquivos que tenham "abc" AND "efg" nessa ordem e essas duas seqüências de caracteres estejam em linhas diferentes nesse arquivo. Por exemplo: um arquivo com conteúdo:

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

Deve ser correspondido.

Saobi
fonte
4
possível duplicata de Como posso procurar um padrão de múltiplas linhas em um arquivo?
Ciro Santilli escreveu

Respostas:

225

Grep não é suficiente para esta operação.

O pcregrep, encontrado na maioria dos sistemas Linux modernos, pode ser usado como

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

onde -M, --multiline permita que os padrões correspondam a mais de uma linha

Há um pcre2grep mais recente também. Ambos são fornecidos pelo projeto PCRE .

pcre2grep está disponível para Mac OS X através de portas Mac como parte da porta pcre2:

% sudo port install pcre2 

e via Homebrew como:

% brew install pcre

ou para pcre2

% brew install pcre2

pcre2grep também está disponível no Linux (Ubuntu 18.04+)

$ sudo apt install pcre2-utils # PCRE2
$ sudo apt install pcregrep    # Older PCRE
portador de anel
fonte
11
@StevenLu -M, --multiline- permite que os padrões correspondam a mais de uma linha.
portador do anel
7
Observe que. * (\ N |.) * É equivalente a (\ n |.) * E o último é mais curto. Além disso, no meu sistema, "pcre_exec () erro -8" ocorre quando eu executo a versão mais longa. Então tente 'abc (\ n |.) * Efg'!
Daveagp
6
Você precisa fazer a expressão não-ganancioso nesse caso exemplo:'abc.*(\n|.)*?efg'
portador de anel
4
e você pode omitir o primeiro .*-> 'abc(\n|.)*?efg'para fazer a regex mais curto (e para ser pedante)
Michi
6
pcregrepfacilita as coisas, mas greptambém funciona. Por exemplo, consulte stackoverflow.com/a/7167115/123695
Michael Mior
113

Não tenho certeza se é possível com o grep, mas o sed facilita muito:

sed -e '/abc/,/efg/!d' [file-with-content]
LJ.
fonte
4
Isso não encontrar arquivos, ele retorna a parte correspondente de um único arquivo
shiggity
11
@Lj. por favor, você pode explicar esse comando? Estou familiarizado sed, mas se nunca vi essa expressão antes.
Anthony
1
@ Anthony, está documentado na página de manual do sed, sob o endereço. É importante perceber que / abc / & / efg / é um endereço.
Squidly
49
Eu suspeito que essa resposta teria sido útil se tivesse um pouco mais de explicação e, nesse caso, eu teria votado novamente mais uma vez. Conheço um pouco de sed, mas não o suficiente para usar esta resposta para produzir um código de saída significativo após meia hora de mexer. Dica: 'RTFM' raramente recebe votos positivos no StackOverflow, como mostra seu comentário anterior.
Michael Scheper
25
Explicação rápida por exemplo: sed '1,5d': exclua linhas entre 1 e 5. sed '1,5! D': exclua linhas que não estejam entre 1 e 5 (ou seja, mantenha as linhas entre) e, em vez de um número, você pode procure uma linha com / padrão /. Veja também o mais simples abaixo: sed -n '/ abc /, / efg / p' p é para impressão e o sinalizador -n não exibe todas as linhas
phil_w
86

Aqui está uma solução inspirada nesta resposta :

  • se 'abc' e 'efg' puderem estar na mesma linha:

    grep -zl 'abc.*efg' <your list of files>
  • se 'abc' e 'efg' devem estar em linhas diferentes:

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

Params:

  • -zTrate a entrada como um conjunto de linhas, cada uma terminada por um byte zero em vez de uma nova linha. ie grep trata a entrada como uma linha grande.

  • -l imprima o nome de cada arquivo de entrada do qual a saída normalmente seria impressa.

  • (?s)ativar PCRE_DOTALL, o que significa que '.' localiza qualquer caractere ou nova linha.

atti
fonte
@ syntaxerror Não, acho que é apenas uma letra minúscula l. AFAIK não há -1opção de número .
Sparhawk 5/10
Parece que você está certo, afinal, talvez eu tenha cometido um erro de digitação ao testar. De qualquer forma, desculpe-me por deixar uma trilha falsa.
Syntaxerror 5/10
6
Isto e excelente. Eu só tenho uma pergunta sobre isso. Se as -zopções especificam grep para tratar as novas linhas, zero byte charactersentão porque precisamos do (?s)no regex? Se já é um caractere que não .é de nova linha, não deve ser possível correspondê-lo diretamente?
Durga Swaroop
1
-z (aka --null-data) e (? s) são exatamente o que você precisa para combinar várias linhas com um grep padrão. Pessoas no MacOS, por favor, deixe comentários sobre a disponibilidade das opções -z ou --null-data em seus sistemas!
Zeke Fast
4
-z definitivamente não está disponível no MacOS
Dylan Nicholson
33

sed deve ser suficiente como o pôster LJ indicado acima,

em vez de! d, você pode simplesmente usar p para imprimir:

sed -n '/abc/,/efg/p' file
Kara
fonte
16

Eu confiei muito no pcregrep, mas com o grep mais recente, você não precisa instalar o pcregrep para muitos de seus recursos. Apenas usegrep -P .

No exemplo da pergunta do OP, acho que as seguintes opções funcionam bem, com a segunda melhor correspondência de como eu entendo a pergunta:

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

Copiei o texto como / tmp / test1 e excluí o 'g' e salvei como / tmp / test2. Aqui está a saída que mostra que o primeiro mostra a sequência correspondente e o segundo mostra apenas o nome do arquivo (típico -o é para mostrar correspondência e típico -l é para mostrar apenas nome do arquivo). Observe que o 'z' é necessário para a multilinha e o '(. | \ N)' significa corresponder 'qualquer coisa que não seja nova linha' ou 'nova linha' - ou seja, qualquer coisa:

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

Para determinar se sua versão é nova o suficiente, execute man grepe veja se algo semelhante a esse aparece na parte superior:

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

Isso é do GNU grep 2.10.

sábio
fonte
14

Isso pode ser feito facilmente usando primeiro trpara substituir as novas linhas por algum outro caractere:

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

Aqui, estou usando o caractere de alarme \a(ASCII 7) no lugar de uma nova linha. Isso quase nunca é encontrado no seu texto, e greppode corresponder a um ., ou especificamente a ele \a.

Gavin S. Yancey
fonte
1
Esta foi a minha abordagem, mas eu estava usando \0e, portanto, necessário grep -ae combinando \x00... Você me ajudou a simplificar! echo $log | tr '\n' '\0' | grep -aoE "Error: .*?\x00Installing .*? has failed\!" | tr '\0' '\n'agora éecho $log | tr '\n' '\a' | grep -oE "Error: .*?\aInstalling .*? has failed\!" | tr '\a' '\n'
Charlie Gorichanaz 28/03
1
Use grep -o.
Kyb
7

awk one-liner:

awk '/abc/,/efg/' [file-with-content]
Swynndla
fonte
4
Felizmente, isso será impresso do abcfinal ao final do arquivo se o padrão final não estiver presente no arquivo ou se o último padrão final estiver ausente. Você pode corrigir isso, mas isso complicará bastante o script.
tripleee
Como excluir /efg/da saída?
kyb 8/07/19
6

Você pode fazer isso com muita facilidade se puder usar o Perl.

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

Você pode fazer isso com uma única expressão regular também, mas isso envolve levar todo o conteúdo do arquivo em uma única sequência, o que pode acabar consumindo muita memória com arquivos grandes. Para completar, eis o método:

perl -e '@lines = <>; $content = join("", @lines); print "Found in $ARGV\n" if ($content =~ /abc.*efg/s);' yourfilename.txt
sundar - Restabelecer Monica
fonte
A segunda resposta encontrada foi útil para extrair um bloco de várias linhas inteiro com correspondências em algumas linhas - teve que usar a correspondência não gananciosa ( .*?) para obter uma correspondência mínima.
RichVel
5

Não sei como faria isso com o grep, mas faria algo assim com o awk:

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

Você precisa ter cuidado ao fazer isso, no entanto. Deseja que o regex corresponda à substring ou à palavra inteira? adicione tags \ w, conforme apropriado. Além disso, embora isso esteja em conformidade estrita com a maneira como você citou o exemplo, ele não funciona quando abc aparece uma segunda vez após o efg. Se você quiser lidar com isso, adicione um if conforme apropriado no / abc / case etc.

frankc
fonte
3

Infelizmente, você não pode. Dos grepdocumentos:

O grep pesquisa os FILEs de entrada nomeados (ou entrada padrão se nenhum arquivo for nomeado ou se um único hífen-menos (-) for fornecido como nome do arquivo) por linhas que contenham uma correspondência com o PATTERN fornecido.

Kaleb Pederson
fonte
que tal #grep -Pz
Navaro
3

Se você estiver disposto a usar contextos, isso pode ser alcançado digitando

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

Isso exibirá tudo entre "abc" e "efg", desde que estejam a 500 linhas um do outro.

agouge
fonte
3

Se você precisar que as duas palavras estejam próximas umas das outras, por exemplo, não mais que 3 linhas, você pode fazer o seguinte:

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

Mesmo exemplo, mas filtrando apenas arquivos * .txt:

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

E também você pode substituir grepcomando por egrepcomando, se desejar encontrar também com expressões regulares.

Mariano Ruiz
fonte
3

Lancei uma alternativa grep há alguns dias atrás, que suporta isso diretamente, seja por correspondência multilinha ou usando condições - espero que seja útil para algumas pessoas que pesquisam aqui. É assim que os comandos do exemplo se pareceriam:

Multilinha:

sift -lm 'abc.*efg' testfile

Condições:

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

Você também pode especificar que 'efg' deve seguir 'abc' dentro de um certo número de linhas:

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

Você pode encontrar mais informações em sift-tool.org .

svent
fonte
Eu não acho que o primeiro exemplo sift -lm 'abc.*efg' testfilefuncione, porque a correspondência é gananciosa e devora todas as linhas até a última efgno arquivo.
Dr. Alex RE
2

Embora a opção sed seja a mais simples e fácil, o one-liner do LJ infelizmente não é o mais portátil. Aqueles presos com uma versão do C Shell precisarão escapar da franja:

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

Infelizmente, isso não funciona em bash et al.

erro
fonte
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
fonte
1

você pode usar o grep, caso não esteja interessado na sequência do padrão.

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

exemplo

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

grep -lencontrará todos os arquivos que correspondem ao primeiro padrão, e xargs fará grep para o segundo padrão. Espero que isto ajude.

Balu Mohan
fonte
1
Isso ignoraria a ordem em que "padrão1" e "padrão2" aparecerão no arquivo - OP especifica especificamente que apenas os arquivos em que "padrão2" aparece APÓS "padrão1" devem ser correspondidos.
Emil Lundberg
1

Com o pesquisador prateado :

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

semelhante à resposta do portador do anel, mas com um ag. As vantagens de velocidade do buscador de prata podem brilhar aqui.

Shwaydogg
fonte
1
Isso não parece funcionar. (echo abctest; echo efg)|ag 'abc.*(\n|.)*efg'não corresponde
phiresky 23/02
1

Eu usei isso para extrair uma sequência fasta de um arquivo multi fasta usando a opção -P para grep:

grep -Pzo ">tig00000034[^>]+"  file.fasta > desired_sequence.fasta
  • P para pesquisas baseadas em perl
  • z para fazer com que uma linha termine em 0 bytes, em vez de char de nova linha
  • o para capturar apenas o que correspondeu desde que o grep retorna a linha inteira (que neste caso desde que você fez -z é o arquivo inteiro).

O núcleo do regexp é o [^>]que se traduz em "não maior que o símbolo"

Jon Boyle
fonte
0

Como uma alternativa para a resposta de Balu Mohan, é possível impor a ordem dos padrões usando apenas 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

Este não é muito bonito, no entanto. Formatado mais facilmente:

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

Isto irá imprimir os nomes de todos os arquivos onde "pattern2"aparece depois "pattern1", ou onde ambos aparecem na mesma linha :

$ 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

Explicação

  • tail -n +i- imprimir todas as linhas após o ith, inclusive
  • grep -n - acrescente as linhas correspondentes aos seus números de linha
  • head -n1 - imprime apenas a primeira linha
  • cut -d : -f 1- imprima a primeira coluna cortada usando :como delimitador
  • 2>/dev/null- tailsaída de erro de silêncio que ocorre se a $()expressão retornar vazia
  • grep -q- silencie grepe retorne imediatamente se uma correspondência for encontrada, pois estamos interessados ​​apenas no código de saída
Emil Lundberg
fonte
Alguém por favor pode explicar o &>? Também estou usando, mas nunca o vi documentado em lugar algum. BTW, por que temos que silenciar o grep dessa maneira, na verdade? grep -qnão vai fazer o truque também?
syntaxerror 23/09/14
1
&>diz ao bash para redirecionar a saída padrão e o erro padrão, consulte REDIRECÇÃO no manual do bash. Você está muito certo no que poderíamos fazer em grep -q ...vez de grep ... &>/dev/null, boa captura!
Emil Lundberg
Pensei isso. Vai acabar com a dor de muitas digitações extras estranhas. Obrigado pela explicação - por isso devo ter pulado um pouco no manual. (Parecia algo remotamente relacionado nele há algum tempo.) --- Você pode até pensar em mudar-lo em sua resposta :).
SyntaxError
0

Isso deve funcionar também ?!

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

$ARGVcontém o nome do arquivo atual ao ler as file_list /spesquisas de modificadores na nova linha.

PS12
fonte
0

O padrão de arquivo *.shé importante para impedir que os diretórios sejam inspecionados. É claro que alguns testes podem impedir isso também.

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

o

grep -n -m1 abc $f 

pesquisa no máximo 1 correspondência e retorna (-n) o número da roupa. Se uma correspondência foi encontrada (teste -n ...), encontre a última correspondência do efg (encontre tudo e leve a última com a cauda -n 1).

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

mais continue.

Como o resultado é algo como 18:foofile.sh String alf="abc";precisamos cortar ":" até o final da linha.

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

Deve retornar um resultado positivo se a última correspondência da 2ª expressão tiver passado da primeira correspondência da primeira.

Em seguida, reportamos o nome do arquivo echo $f.

Usuário desconhecido
fonte
0

Por que não algo simples como:

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

retorna 0 ou um número inteiro positivo.

egrep -o (mostra apenas correspondências, truque: várias correspondências na mesma linha produzem saída em várias linhas como se estivessem em linhas diferentes)

  • grep -A1 abc (imprima abc e a linha depois)

  • grep efg | wc -l (Contagem de 0 n de linhas efg encontradas após abc na mesma ou nas linhas seguintes, o resultado pode ser usado em um 'se ")

  • grep pode ser alterado para egrep etc., se for necessária a correspondência de padrões

kevins
fonte
0

Se você tem alguma estimativa sobre a distância entre as duas cadeias 'abc' e 'efg' que está procurando, você pode usar:

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

Dessa forma, o primeiro grep retornará a linha com o 'abc' mais # num1 linhas depois dele, e # num2 linhas depois dele, e o segundo grep examinará todos eles para obter o 'efg'. Então você saberá em quais arquivos eles aparecem juntos.

Benjamin Berend
fonte
0

Com o ugrep lançado há alguns meses:

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

Essa ferramenta é altamente otimizada para velocidade. Também é compatível com GNU / BSD / PCRE-grep.

Observe que devemos usar uma repetição lenta +?, a menos que você queira combinar todas as linhas efgjuntas até a última efgno arquivo.

Dr. Alex RE
fonte
-3

Isso deve funcionar:

cat FILE | egrep 'abc|efg'

Se houver mais de uma correspondência, você poderá filtrar usando grep -v

Guru
fonte
2
Embora esse trecho de código seja bem-vindo e possa fornecer alguma ajuda, ele seria bastante aprimorado se incluísse uma explicação de como e por que isso resolve o problema. Lembre-se de que você está respondendo à pergunta dos leitores no futuro, não apenas à pessoa que está perguntando agora! Por favor edite sua resposta para adicionar explicação, e dar uma indicação do que limitações e premissas se aplicam.
precisa
1
Na verdade, isso não pesquisa em várias linhas , como indicado na pergunta.
n.st