terça-feira, 28 de agosto de 2007

Metaprogramação com awk e sed

Criar um programa que cria programas é simples nas linguagens interpretadas.

Criamos o programa em uma string e executamos via eval() ou então gravamos em arquivo e executamos novamente.

Me deparei com o seguinte problema: inverter as palavras de uma frase ou arquivo mantendo a ordem em que aparecem.

É claro que eu pensei em usar o rev, mas ele inverte a linha como um todo. Depois de fazer um laço for muito feio em awk, fiquei pensando em como resolver de forma mais legível.

Tive esta ideia: vou fazer uma lista de palavras, inverte-las com o rev e, para cada palavra, vou substitui-la pela palavra invertida. Beleza, o sed faz isso com um pé nas costas.

Eu tenho um arquivo (poderia ser um named pipe) chamado 'direito' que contem uma lista de palavras sem repetição, com uma palavra por linha. Outro arquivo, com o mesmo conteudo mas revertido via 'rev'.

Uso o paste para colocar os arquivo lado a lado e uso o awk para gerar comandos como este:

s/\bpalavra\b/palavra_revertida/g;


Usei o awk pq a sintaxe fica mais clara, o sed ficou muito poluído. Perceba que eu uso o recurso de borda das expressões regulares. Isso me garante que vou trocar uma palavra inteira, e não um pedaço da string.

Agora vem o pulo do gato: mando estes comandos via stdin para o sed, fazendo uso de um pipe. eu informo para o sed que os comandos virão pela stdin passando a opção -f -

Vejam o resultado abaixo, espero que seja útil para alguem :)

$ cat stuff
Nosso fórum principal.

Problemas com hardware em geral,

temperaturas, comparação de desempenho,

compatibilidades de componentes, etc.

$ LC_ALL=pt_BR grep -oE '\w+' stuff | sort -u | tee direito | rev > reverso

$ paste direito reverso | awk '{
printf "s/\\b%s\\b/%s/g;\n",$1,$2 # facil, não?
}' | sed -f - stuff
ossoN muróf lapicnirp.

samelborP moc erawdrah me lareg,

sarutarepmet, oãçarapmoc ed ohnepmesed,

sedadilibitapmoc ed setnenopmoc, cte.

Como o arquivo possui acentos, precisei setar a variavel LC_ALL para pt_BR, caso contrario a expressão regular \w+ não iria casar com todas as palavras.

Ps: Julio, que tal chamar isso de "Inversor do Tiago"?

6 comentários:

match disse...

bem legaw, olha, estou usando o tput e para me familiarizar com ele resolvi fazer algo pratico brincando com ele e fazendo a "Screen Matrix" em shel, consiste em usar colunas fixas no tput com linhas aleatorias pra gerar akela chuva verde de caracters do Matrix Movie, saiu meio bagunçado e n tive tempo ainda de por uma ordem, talvez vc esteja interessando em ajudar o codigo estah em: http://crimeboy.110mb.com/neo.sh
[]z

NetWalker disse...

Cá denovo. :)
Depois de achar um exemplo teu de inversor em sed pela net (assustador lol), lhe pergunto: que faz/fez tanto com sed?? :D
Outra questão q intriga fora o uso real desse inversor (http://www.alltooflat.com/geeky/elgoog/ ?? weird heheh); é se já fez alguma avaliação sobre a performance do sed em relação ao awk, ou mesmo tarefas q possam ser substituídas por grep, cut, expansões e afins.
Pois em alguns casos sed me pareceu meio dispendioso. Porém não conheço sed a fundo para saber sobre o quanto os comandos estavam otimizados.
E adiantando, muito boa essa indicação do "On The Lot". Não conhecia. :) Ótimos posts como sempre.
Então é isso.
Farewell.

NetWalker

Tiago Peczenyj disse...

Ola NetWalker,

Pois bem, o sed pode parecer dispendioso, porém em alguns casos a perda de desempenho é imperceptível. Sem falar que a sintaxe dele é mais clara (99% dos casos eu uso a opção de busca e substituição).

Mas perceba que o sed pode ser usado de forma mais otimizada, como o caso de imprimir apenas a linha 105 de um grande arquivo:

sed -n '105q;d' arquivo

sed + awk são uma dupla muito interessante ;-)

Leandro disse...

Poderia também utilizar vetores, num forzão bem cabuloso.
Exemplo:
$ read string
o rato roeu a roupa do rei de roma
$ string=($string)
$ for (( i=0; i<=${#string[@]}; i+=1 ))
> {
> echo ${string[$i]} | rev | tr "\n" ' '
> }
o otar ueor a apuor od ier ed amor

Mas só funciona com uma só string... rsrs. Só é muito lento, pois o rev, que troca palavra por palavra, é executado muuitas vezes, quebrando a linha, o que me obriga a utilizar um tr para substituir as quebras por espaço... Fica muito lento... (mas funciona para uma string ;-))

Leandro Santiago disse...

Mas, se você transformar esse for acima numa função, tipo
InverteFrase()
{
local IFS=' ' # tab e espaço
string=($@)
for (( i=0; i<=${#string[@]}; i+=1 ))
{
echo ${string[$i]} | rev | tr "\n" ' '
}
}

Pode utilizar para fazer num arquivo, simplesmente tomando cada linha como uma string independente:

while read LINHA
do
InverteFrase $LINHA
echo ## esse daqui é para quebrar a linha, no final de cada frase
done < <(cat arquivo_de_texto)

Nossa, esse meu aí demorou 18 segundos num arquivo de textos simples, mas em compensação não precisa escrever em disco. Contra o seu método, que no mesmo arquivo demorou 0.12 segundo... hauhaau

Flw, e foi mal invadir assim o seu blog ;-)

Tiago Peczenyj disse...

O que acontece é o seguinte:

cada vez que vc invoca o comando rev, vc perde tempo com a inicialização do programa e seu término. Eu utilizei o rev apenas uma vez, mas vc executa a cada palavra.

É a mesma diferença de

for i in *.txt ; do rm $i ; done

e rm *.txt

A segunda forma recebe ja todos os parâmetros e só tem o trabalho de iterar internamente sobre esta lista. A primeira forma cria um custoso laço por conta do detalhe que eu ja lhe falei.

Shel é sensacional, porém não pode ser pensado como uma linguagem script sempre, ele é uma forma de interação do usuario com o sistema ;-)