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 foo
algum lugar dentro dele.
Um regex como (<tag1>.*?foo.*?</tag1>)
deve dar a parte certa, mas ferramentas como grep
e sed
só funcionam para mim com linhas únicas. Como posso obter
<tag1>
<tag2>foo</tag2>
</tag1>
neste exemplo?
Respostas:
Se você possui o GNU grep instalado, você pode fazer uma pesquisa multilinha passando o
-P
sinalizador (perl-regex) e ativandoPCRE_DOTALL
com(?s)
Se o acima não funcionar em sua plataforma, tente passar a
-z
bandeira 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.fonte
(?s)
dica(GNU grep) 2.14
no Debian. Copiei o exemplo dos OPs como está (adicionando apenas uma nova linha final) e executei o seugrep
nele, mas não obtive resultados.grep -ozP
vez degrep -oP
nas suas plataformas?Se você fizer o acima, com os dados exibidos, antes da última linha de limpeza, deverá trabalhar com um
sed
espaço padrão que se parece com:Você pode imprimir seu espaço padrão sempre que quiser com
l
ook. Você pode endereçar os\n
caracteres.Mostrará a você que cada linha a
sed
processa no estágio em quel
é chamada.Então, eu apenas testei e precisava de mais um
\backslash
após o,comma
na primeira linha, mas, caso contrário, funciona como está. Aqui eu o coloco_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 uma questão de brevidade)Agora vamos mudar
p
para uml
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 do nosso sesed 3<<\SCRIPT
pareça com:Então eu vou executá-lo novamente:
Está bem! Então, eu estava certa - é um sentimento bom. Agora, vamos embaralhar nosso
l
redor para ver as linhas que ele puxa, mas exclui. Removeremos nossa atuall
e adicionaremos uma à!{block}
que se parece com:É assim que parece antes de acabarmos.
Uma última coisa que quero mostrar é o
H
antigo espaço à medida que o construímos. Espero que possa demonstrar alguns conceitos-chave. Então, removo o últimol
OK novamente e altero a primeira linha para adicionar uma espiada noH
espaço antigo no final:H
o 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 apenasx
troco 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:
O que simplesmente seleciona todos os personagens e os remove. Não posso usá-lo
d
porque 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.Isso funciona de maneira semelhante a,
H
mas 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:Fora.
E é assim que escrevo
sed
scripts.fonte
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:O script perl processará cada linha do seu arquivo de entrada e
if(/<tag1>/){$a=1;}
: a variável$a
é configurada para1
se uma tag de abertura (<tag1>
) for encontrada.if($a==1){push @l,$_}
: para cada linha, se$a
houver1
, 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ênciafoo
, imprima o conteúdo de@l
.$a=0; @l=()
: esvazie a lista (@l=()
) e$a
volte para 0.fonte
<tag1>
comfoo
e funciona bem. Quando isso falhar para você?Aqui está uma
sed
alternativa: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 tardeN
adiciona a próxima linha ao espaço do padrão (buffer ativo)./<\/tag1/!b x
significa que, se o espaço do padrão atual não contiver nenhuma marca de fechamento, ramifique para ox
rótulo criado anteriormente. Assim, continuamos adicionando linhas ao espaço do padrão até encontrarmos a tag de fechamento./foo/p
significa que, se o espaço do padrão atual corresponderfoo
, ele deverá ser impresso.fonte
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>
:ou mais geralmente (com uma regex para a tag final)
Testando-o no @ terdon's
foo.xml
:fonte
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:
Se a sua versão do
grep
suporta, você também pode usar a opção mais simples-C
(para contexto) que imprime as N linhas circundantes:fonte
tail -3 input_file.xml
. Sim, funciona para este exemplo específico, mas não é uma resposta útil para a pergunta.