Um pouco de sed e regex, excluindo várias ocorrencias de um mesmo arquivo

February 19th, 2010 No Comments   Postado em Bash, Linux

Recentemente, administrando remotamente (via ssh) um servidor linux, surgiu a necessidade de excluir várias ocorrencias de um mesmo arquivo em várias  árvores de diretórios, um amigo que se diz “Admin de infra” e  que detesta linha de comando, pois administra servidores Windows (obviamente via GUI) que acompanhava minha batalha fez a seguinte observação:  “Tá vendo? é por isto que eu não gosto da linha de comando, isto seria fácil em um sistema desktop, no windows, por exemplo, bastaria fazer uma busca em todo o disco, selecionar tudo e excluir, já na sua amada linha de comando é preciso ficar planejando um comando. Realmente eu não tinha de imediato o comando na mente, seria preciso um tempo para “compor” um comando, mas imediatamente me veio a mente a vantagem de se construir um comando desta natureza para te-lo como ferramenta e fiz o seguinte questionamento ao meu amigo: “E se voce precisasse fazer uma limpeza deste tipo todos os dias? ou 1 vez por semana?” Ele concordou que realmente isto as vezes é necessário (como por exemplo os malditos arquivos Thumbs.db que o windows gera quando a opção armazenar miniaturas está em on nas opções de pasta), neste contexto ter um ou um conjunto de comandos é bem mais vantajoso pois é só criar um script e coloca-lo no crontab (agendador de tarefas) em 10 ou 15 minutos encontrei a solução e algumas horas depois encontrei várias versões, para escrever este texto escolhi, propositalmente  a mais complexa somente para ter um pretexto para brincar com o sed e as expresressões regulares.

Suponha que se tenha uma pasta chamada testes no seguinte caminho:
srv/www/htdocs/testes/

Digamos que este diretório seja uma cópia de trabalho de um projeto svn, mas desejamos empacota-la (compactar) e distribuir os fontes, e por um motivo qualquer,  não podemos utilizar o svn para fazer um export, de qualquer forma não faz muito sentido enviar as pastas de controle .svn, então seria interessante exclui-las, primeiro vamos, com o comando find, verificar onde estão as pastas .svn, por mera curiosidade:

# find /srv/www/htdocs/testes -name .svn
/srv/www/htdocs/testes/scripts/.svn
/srv/www/htdocs/testes/api/.svn
/srv/www/htdocs/testes/include/client/.svn
/srv/www/htdocs/testes/include/pear/Mail/.svn
/srv/www/htdocs/testes/include/pear/Net/.svn
/srv/www/htdocs/testes/include/pear/Auth/.svn
/srv/www/htdocs/testes/include/pear/Auth/SASL/.svn
/srv/www/htdocs/testes/include/pear/.svn
/srv/www/htdocs/testes/include/pear/PEAR/.svn
/srv/www/htdocs/testes/include/staff/.svn
/srv/www/htdocs/testes/include/.svn
/srv/www/htdocs/testes/styles/.svn
/srv/www/htdocs/testes/scp/js/.svn
/srv/www/htdocs/testes/scp/.svn
/srv/www/htdocs/testes/scp/css/.svn
/srv/www/htdocs/testes/scp/images/icons/.svn
/srv/www/htdocs/testes/scp/images/.svn
/srv/www/htdocs/testes/.svn
/srv/www/htdocs/testes/setup/.svn
/srv/www/htdocs/testes/setup/inc/.svn
/srv/www/htdocs/testes/setup/images/.svn
/srv/www/htdocs/testes/images/captcha/.svn
/srv/www/htdocs/testes/images/icons/.svn
/srv/www/htdocs/testes/images/.svn

Agora vamos apagar todas estas pastas, aproveitando do próprio comando find:
# find /srv/www/htdocs/testes -name .svn | sed -e ‘s/\/srv/rm -rf \/srv/g’ > clearsvn.sh | chmod 770 clearsvn.sh;./clearsvn.sh

Prontinho, um outro find não encontraria mais nenhuma pasta chamada .svn, mas aí meu amigo falou “Cê tá brincando né? e quem entende esta macorranada aí a não ser os malucos do linux?”, Cazzo, eu disse a ele, um “admin de infra” não pode fugir deste tipo de codigo, o que você vai fazer com o powershell dos novos servidores da MS? e então passei a explicar a *assustadora* linha do comando:

Primeiro é preciso entender que na verdade são 2 segmentos (ou 2 linhas) de comandos, o primeiro vai até o ; (ponto e vírgula) e está dividido em 3 comandos separados pelo | (pipe), o pipe, em essencia, faz a comunicação entre processos ou como diz o guia foca de uma forma mais didática: “envia a saida de um comando para a entrada do comando seguinte”, vamos ver cada comando do primeiro seguimento:

# find /srv/www/htdocs/testes -name .svn |
Como mostrado no inicio deste texto, este comando apenas lista (neste caso na tela) os arquivos encontrados com o nome .svn, mais detalhes sobre este comando no guia foca. Lembrando que como o comando inclue um pipe, o resultado dele servirá como entrada para o proximo comando após o mesmo.

sed -e ‘s/\/srv/rm -rf \/srv/g’
O Comando sed (comando?), na verdade o sed não é propriamente um comando mas sim um editor, como diz o verde, “ Um editor de textos não interativo”, então: o sed neste contexto apenas faz substituições em cada linha que o comando find envia para ele,  ele substitui em cada início de linha fornecida pelo find a expressão /srv pela expressão rm -rf /srv , ou seja, ele acrescentou o comando rm seguido das opcoes -rf em cada linha, para explicar como o sed faz a substituição vamos supor que tenhamos um aquivo chamado arquivo.txt com o seguinte conteúdo:

Ontem eu não trabalhei, para hoje descansar.

Vamos substituir a expressão “não trabalhei” por “trabalhei muito”, para isto usamos o sed com a seguinte sintaxe:

sed -i ‘s/não trabalhei/trabalhei muito/g’ arquivo.txt

O conteúdo do arquivo seria alterado para:

Ontem eu trabalhei muito, para hoje descansar.

Veja que no meu comando original eu utilizei -e (para expressão) enquanto no exemplo acima foi utilizado -i, a explicação é simples, no primeiro caso o comando faz a alteração nas linhas que o comando find envia para o sed enquanto no segundo o sed faz a substituição de forma interativa, diretamente no arquivo.

Caramba, se a substituição é tão simples no sed então para o que serve aquelas barras invertidas no comando original? Ah sim! o problema está no fato de que o sed usa a / (um meta caracter) para separar a expressão a ser substituida pela expressão a substituir e, naquele caso, tinhamos que substituir uma expressão que começava extamente com uma /, tinhamos que substituir: /srv por rm -rf /srv, isto literalmente ficaria assim:

sed -e ‘//srv/rm -rf /srv/g’
Nossa, isto sim é confuso, como o sed ‘saberia’ qual das barras seria o meta caracter? na verdade a expressão dentro das aspas não passa de uma expressão regular,  então para resolver o problema é só utilizar  scape, ou seja, quando vc quer incluir uma barra em uma das bordas (inicio ou fim) da expressão basta “escapa-la” com uma barra invertida, veja que o comando sed -e ‘s/\/srv/rm -rf \/srv/g’ não escapa a barra logo após o srv, isto ocorre porque esta barra não ‘confunde’ o sed pois ela não está imediatamente antes ou após as barras que fazem parte do comando (o meta caracter).

> clearsvn.sh |
Este comando envia a saída (que seria enviada para a tela) para um arquivo chamado clearsvn.sh e usa o pipe para que seja processado o proximo comando, embora não envie nada para ele.

chmod 770 clearsvn.sh;
Este comando faz com o que arquivo gerado pelo comando anterior (clearsvn.sh) torne-se um executável, o ponto e vírgula encerra o segmento, funciona como um enter na linha de comando.

./clearsvn.sh
Finalmete o arquivo gerado é executado pelo comando acima.

Pô mas este monte de barra invertida realmente tornou a sequência de comandos bem macarrônica, não daria para ser mais simples? claro que daria, mas para que simplificar se podemos complicar? heim, heim? Brincadeirinha, a intenção foi mesmo mostrar o conceito do scape, a tal barra invertida, a solução para a simplificação seria a inserção ao invés da substituição, para isto fariamos:

find /srv/www/htdocs/testes -name .svn | sed -e ‘s/^/rm -rf /’ > clearsvn.sh | chmod 770 clearsvn.sh;./clearsvn.sh
Ficou menos macarronico? sei não! Se você conhece bem as expressões regulares (se não conhece e conseguiu chegar até aqui neste texto então está na hora de conhecer, aqui pode ser um bom lugar para começar) sim, para quem conhece regex é fácil entender a parte do sed agora, de qualquer forma para conhedores deste tema o comando anterior também não seria problema, vamos tentar explicar esta nova versão do sed:

sed -e ‘s/^/rm -rf /’
o s continua tendo a função de substituição, mas agora o ^ (Circunflexo) indica que a substituição será no inicio da linha como não existe nada após o circunflexo a expressão após a segunda barra acaba sendo inserida. Para deixar um pouco mais claro vamos usar esta forma de comando para inserirmos algo no início do arquivo “arquivo.txt” do exemplo anterior:

sed -i ‘s/^Ante/’
Isto faria com que o arquivo ficasse com o seguinte conteúdo:

AnteOntem eu trabalhei muito, para hoje descansar.

Ok, Isto é tudo? daria para melhorar ainda mais o conjunto de comandos? Claro, como o shel do linux é muito rico, é bem provável que para cada pessoa acostumada com a linha de comando que se apresentasse este problema, uma solução diferente seria encontrada, mas sem alterar em nada o comando e somente olhando para o resultado final podemos perceber que além de excluir as pastas .svn (atendendo ao nosso objetivo principal) a solução deixa, digamos… Um rastro, uma “sujeira”, que é o arquivo cleansvn.sh, então resta-nos acesecentar um comando para elimina-lo, eis a versão final:

# find /srv/www/htdocs/testes -name .svn | sed -e ‘s/^/rm -rf /’ > clearsvn.sh | chmod 770 clearsvn.sh;./clearsvn.sh;rm -rf clearsvn.sh

Considerações finais
Como disse no inicio do texto, esta é a solução mais complexa (ou mais macarronica) que encontrei, participando de grupos de discussões e foruns na rede, apresentaram-me solucoes com 1/3  do tamanho desta apresentada aqui, mas como já afirmei, escolhi esta apenas como pretexto para brincar um pouco com o sed e as expressões regulares, as outras soluções talvez possam vir a ser objeto de um outro texto.



Tags: , ,