Como posso “grep” padrões através de múltiplas linhas?

19

Parece que estou usando indevidamente grep / egrep .

Eu estava tentando pesquisar strings em várias linhas e não encontrei correspondência, embora saiba que o que estou procurando deve corresponder. Originalmente, achei que meus regexes estavam errados, mas acabei lendo que essas ferramentas operam por linha (também minhas regexes eram tão triviais que não poderia ser o problema).

Então, qual ferramenta seria usada para pesquisar padrões em várias linhas?

    
por Jim 02.02.2014 / 11:43

9 respostas

21

Aqui está um sed um que lhe dará um comportamento semelhante a grep em várias linhas:

sed -n '/foo/{:start /bar/!{N;b start};/your_regex/p}' your_file

Como funciona

  • -n suprime o comportamento padrão de imprimir todas as linhas
  • /foo/{} instrui para corresponder foo e fazer o que vem dentro do squigglies para as linhas correspondentes. Substitua foo pela parte inicial do padrão.
  • :start é um rótulo de ramificação para nos ajudar a manter o loop até encontrarmos o fim do nosso regex.
  • /bar/!{} executará o que está no squigglies para as linhas que não correspondem a bar . Substitua bar pela parte final do padrão.
  • N acrescenta a próxima linha ao buffer ativo ( sed chama isso de espaço padrão)
  • b start ramificará incondicionalmente para o rótulo start que criamos anteriormente, para continuar anexando a próxima linha, desde que o espaço padrão não contenha bar .
  • /your_regex/p imprime o espaço do padrão se corresponder a your_regex . Você deve substituir your_regex pela expressão inteira que deseja corresponder em várias linhas.
por 02.02.2014 / 14:31
19

Eu geralmente uso uma ferramenta chamada pcregrep que pode ser instalada na maior parte do sabor do linux usando yum ou apt .

Por exemplo,

Suponha que você tenha um arquivo chamado testfile com conteúdo

abc blah
blah blah
def blah
blah blah

Você pode executar o seguinte comando:

$ pcregrep -M  'abc.*(\n|.)*def' testfile

para fazer correspondência de padrões em várias linhas.

Além disso, você pode fazer o mesmo com sed também.

$ sed -e '/abc/,/def/!d' testfile
    
por 02.02.2014 / 12:00
5

Aqui está uma abordagem mais simples usando o Perl:

perl -e '$f=join("",<>); print $& if $f=~/foo\nbar.*\n/m' file

ou (desde que JosephR pegou o sed route , eu vou roubar descaradamente o seu suggestion )

perl -n000e 'print $& while /^foo.*\nbar.*\n/mg' file

Explicação

$f=join("",<>); : lê o arquivo inteiro e salva seu conteúdo (novas linhas e todos) na variável $f . Em seguida, tentamos corresponder foo\nbar.*\n e imprimi-lo se corresponder (a variável especial $& contém a última correspondência encontrada). O ///m é necessário para fazer a expressão regular corresponder em novas linhas.

O -0 define o separador de registro de entrada. Definir isso como 00 ativa o 'modo de parágrafo', onde o Perl usará novas linhas consecutivas ( \n\n ) como separador de registro. Nos casos em que não há novas linhas consecutivas, o arquivo inteiro é lido (slurped) de uma só vez.

Aviso:

Não faça isso para arquivos grandes, ele carregará o arquivo inteiro na memória e isso pode ser um problema.

    
por 02.02.2014 / 14:10
2

Uma maneira de fazer isso é com o Perl. por exemplo. aqui está o conteúdo de um arquivo chamado foo :

foo line 1
bar line 2
foo
foo
foo line 5
foo
bar line 6

Agora, aqui está um Perl que irá combinar com qualquer linha que comece com foo seguida por qualquer linha que comece com bar:

cat foo | perl -e 'while(<>){$all .= $_}
  while($all =~ /^(foo[^\n]*\nbar[^\n]*\n)/m) {
  print $1; $all =~ s/^(foo[^\n]*\nbar[^\n]*\n)//m;
}'

O Perl, detalhado:

  • while(<>){$all .= $_} Isso carrega toda a entrada padrão para a variável $all
  • while($all =~ Enquanto a variável all tem a expressão regular ...
  • /^(foo[^\n]*\nbar[^\n]*\n)/m O regex: foo no início da linha, seguido por qualquer número de caracteres não pertencentes à nova linha, seguido por uma nova linha, seguido imediatamente por "bar" e o resto da linha com a barra nela. /m no final da regex significa "correspondência em várias linhas"
  • print $1 Imprime a parte da regex que estava entre parênteses (neste caso, toda a expressão regular)
  • s/^(foo[^\n]*\nbar[^\n]*\n)//m Apaga a primeira correspondência para o regex, para que possamos combinar vários casos da regex no arquivo em questão

E a saída:

foo line 1
bar line 2
foo
bar line 6
    
por 02.02.2014 / 12:12
2

A alternativa grep sift suporta a correspondência multilinha (disclaimer: Eu sou o autor).

Suponha que testfile contenha:

<book>
  <title>Lorem Ipsum</title>
  <description>Lorem ipsum dolor sit amet, consectetur
  adipiscing elit, sed do eiusmod tempor incididunt ut
  labore  et dolore magna aliqua</description>
</book>


sift -m '<description>.*?</description>' (mostre as linhas contendo a descrição)

Resultado:

testfile:  <description>Lorem ipsum dolor sit amet, consectetur
testfile:  adipiscing elit, sed do eiusmod tempor incididunt ut
testfile:  labore  et dolore magna aliqua</description>


sift -m '<description>(.*?)</description>' --replace 'description="$1"' --no-filename (extraia e reformate a descrição)

Resultado:

description="Lorem ipsum dolor sit amet, consectetur
  adipiscing elit, sed do eiusmod tempor incididunt ut
  labore  et dolore magna aliqua"
    
por 23.02.2015 / 00:20
2

Simplesmente um grep normal que suporta Perl-regexp parameter P fará este trabalho.

$ echo 'abc blah
blah blah
def blah
blah blah' | grep -oPz  '(?s)abc.*?def'
abc blah
blah blah
def

(?s) chamado modificador DOTALL que faz com que o ponto no seu regex corresponda não apenas aos caracteres, mas também às quebras de linha.

    
por 23.02.2015 / 01:58
0

Eu resolvi esse para mim usando grep e -A opção com outro grep.

grep first_line_word -A 1 testfile | grep second_line_word

A opção -A 1 imprime 1 linha após a linha encontrada. Claro que depende da sua combinação de arquivos e palavras. Mas para mim foi a solução mais rápida e confiável.

    
por 13.04.2015 / 14:43
0

Suponha que temos o arquivo test.txt contendo:

blabla
blabla
foo
here
is the
text
to keep between the 2 patterns
bar
blabla
blabla

O seguinte código pode ser usado:

sed -n '/foo/,/bar/p' test.txt

Para a seguinte saída:

foo
here
is the
text
to keep between the 2 patterns
bar
    
por 06.12.2017 / 11:51
-1

Se quisermos obter o texto entre os dois padrões, excluindo eles mesmos.

Suponha que temos o arquivo test.txt contendo:

blabla
blabla
foo
here
is the
text
to keep between the 2 patterns
bar
blabla
blabla

O seguinte código pode ser usado:

 sed -n '/foo/{
 n
 b gotoloop
 :loop
 N
 :gotoloop
 /bar/!{
 h
 b loop
 }
 /bar/{
 g
 p
 }
 }' test.txt

Para a seguinte saída:

here
is the
text
to keep between the 2 patterns

Como funciona, vamos fazer isso passo a passo

  1. /foo/{ é acionado quando a linha contém "foo"
  2. n substitui o espaço do padrão pela próxima linha, ou seja, a palavra "aqui"
  3. b gotoloop branch para o rótulo "gotoloop"
  4. :gotoloop define o rótulo "gotoloop"
  5. /bar/!{ se o padrão não contiver "bar"
  6. h substitui o espaço de espera pelo padrão, então "aqui" é salvo no espaço de espera
  7. b loop branch para o rótulo "loop"
  8. :loop define o rótulo "loop"
  9. N acrescenta o padrão ao espaço de espera.
    Agora segure espaço contém:
    "aqui"
    "é o"
  10. :gotoloop Agora estamos no passo 4 e fazemos um loop até que uma linha contenha "bar"
  11. /bar/ loop terminou, "bar" foi encontrado, é o espaço padrão
  12. g pattern space é substituído por hold space que contém todas as linhas entre "foo" e "bar" que foram salvas durante o loop principal
  13. p copia o espaço padrão para a saída padrão

Feito!

    
por 06.12.2017 / 15:29