Como obter várias linhas de um arquivo por uma regex?

10

Como obter várias linhas de um arquivo por uma regex?

Muitas vezes eu gostaria de obter várias linhas / modificar várias linhas por um regex. Um exemplo de caso:

Eu estou tentando ler parte de um arquivo XML / SGML (eles não são necessariamente bem formados ou em uma sintaxe previsível, portanto, um regex seria mais seguro que um analisador adequado. Além disso, eu gostaria de poder fazer isso também completamente arquivos não estruturados, onde apenas algumas palavras-chave são conhecidas.) em um script de shell (em execução no Solaris e Linux).

XML de exemplo:

<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>

A partir disso, eu gostaria de ler o <tag1>se ele contém fooalgum lugar dentro dele.

Um regex como (<tag1>.*?foo.*?</tag1>)deve dar a parte certa, mas ferramentas como grepe sedsó funcionam para mim com linhas únicas. Como posso obter

<tag1>
 <tag2>foo</tag2>
</tag1>

neste exemplo?

Den
fonte
3
Link obrigatório
evilsoup
@evilsoup Isso é verdade, mas minha pergunta não é especificamente sobre arquivos XML / SGML, apenas sobre arquivos de texto.
Den

Respostas:

7

Se você possui o GNU grep instalado, você pode fazer uma pesquisa multilinha passando o -Psinalizador (perl-regex) e ativando PCRE_DOTALLcom(?s)

grep -oP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
<tag1>
<tag2>foo</tag2>
</tag1>

Se o acima não funcionar em sua plataforma, tente passar a -zbandeira além disso, isso força o grep a tratar NUL como separador de linhas, fazendo com que o arquivo inteiro pareça uma única linha.

grep -ozP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
iruvar
fonte
Isso não gera saída no meu sistema quando executado no arquivo de exemplo do OP.
terdon
Funciona para mim. +1. Obrigado pela (?s)dica
Nathan Wallace
@terdon, qual versão do GNU grep você está executando?
Iruvar
@ 1_CR (GNU grep) 2.14no Debian. Copiei o exemplo dos OPs como está (adicionando apenas uma nova linha final) e executei o seu grepnele, mas não obtive resultados.
terdon
1
@ slm, estou no pcre 6.6, GNU grep 2.5.1 no RHEL. Você se importa de tentar em grep -ozPvez de grep -oPnas suas plataformas?
Iruvar
3
#begin command block
#append all lines between two addresses to hold space 
    sed -n -f - <<\SCRIPT file.xml
        \|<tag1>|,\|</tag1>|{ H 
#at last line of search block exchange hold and pattern space 
            \|</tag1>|{ x
#if not conditional ;  clear buffer ; branch to script end
                \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
#do work ; print result; clear buffer ; close blocks
    s?*?*?;p;s/.*//;h;b}}
SCRIPT

Se você fizer o acima, com os dados exibidos, antes da última linha de limpeza, deverá trabalhar com um sedespaço padrão que se parece com:

 ^\n<tag1>\n<tag2>foo</tag2>\n</tag1>$

Você pode imprimir seu espaço padrão sempre que quiser com look. Você pode endereçar os \ncaracteres.

sed l <file

Mostrará a você que cada linha a sedprocessa no estágio em que lé chamada.

Então, eu apenas testei e precisava de mais um \backslashapós o ,commana primeira linha, mas, caso contrário, funciona como está. Aqui eu o coloco _sed_functionpara que eu possa chamá-lo facilmente para fins de demonstração ao longo desta resposta: (funciona com comentários incluídos, mas são removidos aqui por uma questão de brevidade)

_sed_function() { sed -n -f /dev/fd/3 
} 3<<\SCRIPT <<\FILE 
    \|<tag1>|,\|</tag1>|{ H
        \|</tag1>|{ x
            \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
    s?*?*?;p;s/.*//;h;b}}
#END
SCRIPT
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
FILE


_sed_function
#OUTPUT#
<tag1>
 <tag2>foo</tag2>
</tag1>

Agora vamos mudar ppara um lpara que possamos ver com o que estamos trabalhando enquanto desenvolvemos nosso script e removemos a demonstração não operacional, s?para que a última linha do nosso se sed 3<<\SCRIPTpareça com:

l;s/.*//;h;b}}

Então eu vou executá-lo novamente:

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

Está bem! Então, eu estava certa - é um sentimento bom. Agora, vamos embaralhar nosso lredor para ver as linhas que ele puxa, mas exclui. Removeremos nossa atual le adicionaremos uma à !{block}que se parece com:

!{l;s/.*//;h;b}

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$

É assim que parece antes de acabarmos.

Uma última coisa que quero mostrar é o Hantigo espaço à medida que o construímos. Espero que possa demonstrar alguns conceitos-chave. Então, removo o último lOK novamente e altero a primeira linha para adicionar uma espiada no Hespaço antigo no final:

{ H ; x ; l ; x

_sed_function
#OUTPUT#
\n<tag1>$
\n<tag1>\n <tag2>bar</tag2>$
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$
\n<tag1>$
\n<tag1>\n <tag2>foo</tag2>$
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

Ho espaço antigo sobrevive aos ciclos de linha - daí o nome. Então, o que as pessoas geralmente tropeçam - ok, o que eu tropeço frequentemente - é que ele precisa ser excluído depois que você o usa. Nesse caso, eu apenas xtroco uma vez, para que o espaço de espera se torne o espaço padrão e vice-versa, e essa mudança também sobrevive aos ciclos de linha.

O efeito é que eu preciso excluir meu espaço de espera, que costumava ser meu espaço de padrão. Eu faço isso limpando primeiro o espaço do padrão atual com:

s/.*//

O que simplesmente seleciona todos os personagens e os remove. Não posso usá-lo dporque isso encerraria meu ciclo de linha atual e o próximo comando não seria concluído, o que seria um lixo para o meu script.

h

Isso funciona de maneira semelhante a, Hmas substitui o espaço de espera, então eu apenas copiei meu espaço de padrão em branco por cima do meu espaço de espera, excluindo-o efetivamente. Agora eu posso apenas:

b

Fora.

E é assim que escrevo sedscripts.

mikeserv
fonte
Obrigado @slm! Você é realmente um cara legal, sabia?
Mikeerv
Obrigado, bom trabalho, subida muito rápido para 3k, próxima até 5k 8-)
SLM
Não sei, @slm. Estou começando a ver que estou aprendendo cada vez menos aqui - talvez superemos sua utilidade. Eu tenho que pensar sobre isso. Eu mal cheguei ao site nas últimas duas semanas.
Mikeerv #
Pelo menos chegar a 10k. Tudo o que vale a pena desbloquear está nesse nível. Continue cortando, 5k virá bastante rápido agora.
Slm
1
Bem, @ slm - você é uma raça rara de qualquer maneira. Eu concordo com as múltiplas respostas. É por isso que me incomoda quando alguns qs são fechados. Mas isso raramente acontece, na verdade. Mais uma vez obrigado, slm.
Mikeerv
2

A resposta de @jamespfinn funcionará perfeitamente bem se o seu arquivo for tão simples quanto o seu exemplo. Se você tiver uma situação mais complexa em que <tag1>possa abranger mais de 2 linhas, precisará de um truque um pouco mais complexo. Por exemplo:

$ cat foo.xml
<tag1>
 <tag2>bar</tag2>
 <tag3>baz</tag3>
</tag1>
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
$ perl -ne 'if(/<tag1>/){$a=1;} 
            if($a==1){push @l,$_}
            if(/<\/tag1>/){
              if(grep {/foo/} @l){print "@l";}
               $a=0; @l=()
            }' foo.xml
<tag1>

  <tag2>foo</tag2>
 </tag1>
<tag1>
  <tag2>bar</tag2>

  <tag2>foo</tag2>
  <tag3>baz</tag3>
 </tag1>

O script perl processará cada linha do seu arquivo de entrada e

  • if(/<tag1>/){$a=1;}: a variável $aé configurada para 1se uma tag de abertura ( <tag1>) for encontrada.

  • if($a==1){push @l,$_}: para cada linha, se $ahouver 1, adicione essa linha à matriz @l.

  • if(/<\/tag1>/) : se a linha atual corresponder à tag de fechamento:

    • if(grep {/foo/} @l){print "@l"}: se alguma das linhas salvas na matriz @l(estas são as linhas entre <tag1>e </tag1>) corresponder à sequência foo, imprima o conteúdo de @l.
    • $a=0; @l=(): esvazie a lista ( @l=()) e $avolte para 0.
terdon
fonte
Isso funciona bem, exceto no caso em que há mais de um <tag1> contendo "foo". Nesse caso ele imprime tudo, desde o início do primeiro <tag1> ao final da última </ tag1> ...
Den
@den eu testei com o exemplo mostrado na minha resposta, que contém 3 <tag1>com fooe funciona bem. Quando isso falhar para você?
terdon
isso parece tão errado xml parsing usando regex :)
Braiam
1

Aqui está uma sedalternativa:

sed -n '/<tag1/{:x N;/<\/tag1/!b x};/foo/p' your_file

Explicação

  • -n significa não imprimir linhas, a menos que seja instruído.
  • /<tag1/ corresponde primeiro à tag de abertura
  • :x é um rótulo para permitir pular para esse ponto mais tarde
  • N adiciona a próxima linha ao espaço do padrão (buffer ativo).
  • /<\/tag1/!b xsignifica que, se o espaço do padrão atual não contiver nenhuma marca de fechamento, ramifique para o xrótulo criado anteriormente. Assim, continuamos adicionando linhas ao espaço do padrão até encontrarmos a tag de fechamento.
  • /foo/psignifica que, se o espaço do padrão atual corresponder foo, ele deverá ser impresso.
Joseph R.
fonte
1

Acho que você poderia fazer isso com o GNU awk, tratando a tag final como um separador de registros, por exemplo, para uma tag final conhecida </tag1>:

gawk -vRS="\n</tag1>\n" '/foo/ {printf "%s%s", $0, RT}'

ou mais geralmente (com uma regex para a tag final)

gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}'

Testando-o no @ terdon's foo.xml:

$ gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}' foo.xml
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
chave de aço
fonte
0

Se o seu arquivo estiver estruturado exatamente como você mostrou acima, você poderá utilizar os sinalizadores -A (linhas depois) e -B (linhas antes) para grep ... por exemplo:

$ cat yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
$ grep -A1 -B1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
$ grep -A1 -B1 foo yourFile.txt 
<tag1>
 <tag2>foo</tag2>
</tag1>

Se a sua versão do grepsuporta, você também pode usar a opção mais simples -C(para contexto) que imprime as N linhas circundantes:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
jamespfinn
fonte
Obrigado, mas não. Este é apenas um exemplo e as coisas reais parecem bastante imprevisíveis ;-)
Den
1
Isso não é encontrar uma etiqueta com foo, é apenas encontrar foo e exibir linhas de contexto
Nathan Wallace
@ NathanWallace sim, que é exatamente o que o OP estava pedindo, esta resposta funciona perfeitamente bem no caso apresentado na pergunta.
terdon
@terdon, isso não é o que a pergunta faz. Citação: "Gostaria de ler o <tag1> se ele contém foo em algum lugar dentro dele." Esta solução é como "Gostaria de ler 'foo' e 1 linha de contexto, independentemente de onde 'foo' apareça". Seguindo sua lógica, uma resposta igualmente válida para essa pergunta seria tail -3 input_file.xml. Sim, funciona para este exemplo específico, mas não é uma resposta útil para a pergunta.
Nathan Wallace
@NathanWallace meu argumento foi que o OP declarou especificamente que este não é um formato XML válido; nesse caso, poderia ter sido suficiente imprimir as N linhas ao redor da string que o OP está procurando. Com as informações disponíveis, essa resposta foi decente o suficiente.
terdon