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

10

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

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

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, gostaria de poder fazer isso. também com arquivos completamente não estruturados onde apenas algumas palavras-chave são conhecidas.) em um shell script (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 contiver foo em algum lugar dentro dele.

Um regex como (<tag1>.*?foo.*?</tag1>) deve fornecer a parte correta, mas ferramentas como grep e sed funcionam apenas para mim com linhas únicas. Como posso conseguir

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

neste exemplo?

    
por Den 21.10.2013 / 16:34

6 respostas

7

Se você tiver o GNU grep instalado, poderá fazer a pesquisa de múltiplas linhas passando o sinalizador -P (perl-regex) e ativando PCRE_DOTALL com (?s)

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

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

grep -ozP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
    
por 21.10.2013 / 17:18
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, considerando os dados mostrados, antes da última linha de limpeza, você deve estar trabalhando com um espaço de padrão sed semelhante a:

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

Você pode imprimir seu espaço padrão sempre que quiser com l ook. Você pode então endereçar os caracteres \n .

sed l <file

Irá mostrar-lhe cada linha sed processa-a na fase em que l é chamado.

Então, eu acabei de testá-lo e ele precisou de mais um \backslash após o ,comma na primeira linha, mas funciona como está. Aqui eu coloco em um _sed_function para 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 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, alteraremos o p para um l , para 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 de nosso sed 3<<\SCRIPT apenas se parece com:

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

Então eu vou executá-lo novamente:

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

Ok! Então eu estava certo - isso é uma sensação boa. Agora, vamos embaralhar nosso l ook para ver as linhas que ele puxa, mas exclui. Removeremos nosso l atual e adicionaremos um ao !{block} para que pareça:

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

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

Isso é o que parece antes de acabarmos com isso.

Uma última coisa que quero mostrar é o espaço H old conforme o desenvolvemos. Existem alguns conceitos-chave que espero poder demonstrar. Então eu removo o último l ook novamente e altero a primeira linha para adicionar uma espiada no H old space 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>$

H old space sobrevive ciclos de linha - daí o nome. Então, o que as pessoas costumam fazer em viagens frequentes - ok, o que eu costuma atrapalhar - é que ele precisa ser excluído depois de usá-lo. Neste caso eu só e x muda uma vez, então o espaço hold torna-se o espaço padrão e vice-versa e esta mudança também sobrevive aos ciclos de linha.

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

s/.*//

Que simplesmente seleciona todos os caracteres e os remove. Eu não posso usar d porque isso encerraria meu ciclo de linha atual e o próximo comando não seria concluído, o que praticamente acabaria com meu script.

h

Isso funciona de maneira semelhante a H , mas substitui o espaço ocupado, então acabei de copiar meu espaço em branco padrão sobre a parte superior do meu espaço de armazenamento, excluindo-o. Agora eu posso apenas:

b

fora.

E é assim que escrevo sed scripts.

    
por 27.04.2014 / 22:50
2

A resposta do @jamespfinn funcionará perfeitamente 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 duas linhas, será necessário 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 está definida como 1 se uma tag de abertura ( <tag1> ) for encontrada.

  • if($a==1){push @l,$_} : para cada linha, se $a for 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 à string foo , imprima o conteúdo de @l .
    • $a=0; @l=() : esvazie a lista ( @l=() ) e defina $a de volta para 0.
por 21.10.2013 / 17:01
1

Aqui está uma alternativa sed :

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 saltar para este ponto mais tarde
  • N adiciona a próxima linha ao espaço padrão (buffer ativo).
  • /<\/tag1/!b x significa que, se o espaço de padrão atual não contiver nenhuma tag de fechamento, ramifique para o rótulo x criado anteriormente. Assim, continuamos adicionando linhas ao espaço de padrões até encontrarmos nossa tag de fechamento.
  • /foo/p significa que se o espaço de padrão atual corresponder a foo , ele deve ser impresso.
por 28.04.2014 / 01:37
1

Você poderia fazer isso com o GNU awk, ao tratar a tag final como um separador de registro , para uma tag final conhecida </tag1> :

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

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

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

Teste-o em% ter_de%:

$ 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>
    
por 28.04.2014 / 01:42
0

Se o seu arquivo estiver estruturado exatamente como mostrado acima, você poderá utilizar as linhas -A (linhas após) & -B (linhas antes) sinalizadores 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 de grep oferecer suporte, você também pode usar a opção mais simples -C (para o contexto) que imprime as N linhas adjacentes:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
    
por 21.10.2013 / 16:41

Tags