Como posso "grep" padrões em várias linhas?


24

Parece que eu estou fazendo mau uso grep/ egrep.

Eu estava tentando procurar seqüências de caracteres em várias linhas e não consegui encontrar uma correspondência enquanto sei que o que estou procurando deve corresponder. Originalmente, pensei que minhas expressões regulares estavam erradas, mas acabei lendo que essas ferramentas operam por linha (também minhas expressões regulares eram tão triviais que não poderiam ser o problema).

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



11
@CiroSantilli - Eu não acho que este Q e o que você vinculou sejam duplicados. O outro Q está perguntando como você faria a correspondência de padrões de várias linhas (ou seja, qual ferramenta devo / posso usar para fazer isso) enquanto esta pergunta como fazer isso grep. Eles estão intimamente relacionados, mas não são bobos, IMO.
slm

@ sim, esses casos são difíceis de decidir: entendo seu ponto de vista. Eu acho que esse caso em particular é melhor como duplicado porque o usuário disse "grep"sugerindo o verbo "to grep" e as principais respostas, incluindo aceitas, não usam grep.
Ciro Santilli

Respostas:


24

A seguir, apresentamos um comportamento semelhante a sedvocê grepem 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-o a combinar fooe fazer o que vem dentro dos rabiscos para as linhas correspondentes. Substituirfoo pela parte inicial do padrão.
  • :start é um rótulo de ramificação para nos ajudar a continuar em loop até encontrarmos o fim de nossa regex.
  • /bar/!{}executará o que está nos squigglies nas linhas que não correspondem bar. Substituirbar pela parte final do padrão.
  • N anexa a próxima linha ao buffer ativo (sed chama isso de espaço padrão)
  • b startincondicionalmente ramificará para o startrótulo que criamos anteriormente, para continuar anexando a próxima linha, desde que o espaço do padrão não contenhabar .
  • /your_regex/pimprime o espaço do padrão, se corresponder your_regex. Você deve substituir your_regexpor toda a expressão que deseja corresponder em várias linhas.

11
+1 Adicionando isso ao toolikt! Obrigado.
precisa saber é o seguinte

Nota: No MacOS, isso dá:sed: 1: "/foo/{:start /bar/!{N;b ...": unexpected EOF (pending }'s)
Stan James

11
Obtendo sed: unterminated {erro
Nomaed 29/04

@Nomaed Shot in the dark aqui, mas o seu regex contém alguns caracteres "{"? Nesse caso, você precisará escapar-barra invertida.
Joseph R.

11
@Nomaed Parece que tem a ver com as diferenças entre sedimplementações. Tentei seguir as recomendações nessa resposta para tornar o script acima compatível com os padrões, mas ele me disse que "start" era um rótulo indefinido. Portanto, não tenho certeza se isso pode ser feito de maneira compatível com os padrões. Se você o gerencia, sinta-se à vontade para editar minha resposta.
Joseph R.

19

Eu geralmente uso uma ferramenta chamada pcregrepque pode ser instalada na maior parte do sabor do Linux usando yumor apt.

Por exemplo.

Suponha que você tenha um arquivo nomeado testfilecom conteúdo

abc blah
blah blah
def blah
blah blah

Você pode executar o seguinte comando:

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

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

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

$ sed -e '/abc/,/def/!d' testfile

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 seguiu o sedcaminho , roubarei descaradamente sua sugestão )

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

Explicação

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

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

Atenção:

Você não fazer isso para arquivos grandes, ele vai carregar o arquivo inteiro na memória e que pode ser um problema.


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ão alguns Perl que correspondem a 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, dividido:

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

E a saída:

foo line 1
bar line 2
foo
bar line 6

3
Só passei para dizer que seu Perl pode ser encurtado para o mais idiomática:perl -n0777E 'say $& while /^foo.*\nbar.*\n/mg' foo
Joseph R.

2

A alternativa grep Sift suporta correspondência de várias linhas (disclaimer: Eu sou o autor).

Suponha que testfilecontenha:

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


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

Resultado:

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


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

Resultado:

description = "Lorem ipsum dolor sente-se entre, consectetur
  elip adipiscing, sed do eiusmod tempor incididunt ut
  labore et dolore magna aliqua "

11
Ferramenta muito boa. Parabéns! Tente incluí-lo em distribuições como o Ubuntu.
Lourenco

2

Simplesmente um grep normal que suporta Perl-regexpparâmetros Pfará esse 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 o ponto no seu regex corresponder não apenas aos caracteres, mas também às quebras de linha.


Quando tento esta solução, a saída não termina em 'def', mas vai para o final do arquivo 'blah'
buckley 25/10/10

talvez seu grep não suporte a -Popção
Avinash Raj

1

Eu resolvi este aqui 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. Obviamente, depende da sua combinação de arquivos e palavras. Mas, para mim, era a solução mais rápida e confiável.


pseudônimo grepp = 'grep --color = auto -B10 -A20 -i' e depois gato algum arquivo | grepp blá | grepp foo | grepp bar ... sim aqueles -A e -B são muito úteis ... você tem a melhor resposta
Scott Stensland 10/04

1

Suponha que tenhamos o arquivo test.txt contendo:

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

O código a seguir 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

1

Se quisermos colocar o texto entre os 2 padrões, excluindo-se.

Suponha que tenhamos o arquivo test.txt contendo:

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

O código a seguir 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 passo a passo

  1. /foo/{ é acionado quando a linha contém "foo"
  2. n substitua o espaço do padrão pela próxima linha, ou seja, a palavra "aqui"
  3. b gotoloop ramo para o rótulo "gotoloop"
  4. :gotoloop define o rótulo "gotoloop"
  5. /bar/!{ se o padrão não contiver "barra"
  6. h substitua o espaço de espera pelo padrão, para que "aqui" seja salvo no espaço de espera
  7. b loop ramificar para o rótulo "loop"
  8. :loop define o rótulo "loop"
  9. N anexa o padrão ao espaço de espera.
    Agora, o espaço de espera contém:
    "aqui"
    "é o"
  10. :gotoloop Agora estamos na etapa 4 e fazemos um loop até que uma linha contenha "bar"
  11. /bar/ loop for concluído, "barra" foi encontrada, é o espaço do padrão
  12. g o espaço do padrão é substituído pelo espaço de espera que contém todas as linhas entre "foo" e "bar" que foram salvas durante o loop principal
  13. p copie o espaço padrão para a saída padrão

Feito !


Muito bem, +1. Normalmente, evito usar esses comandos inserindo as novas linhas no SOH e executando comandos sed normais e substituindo as novas linhas.
A.Danischewski 8/11
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.