AWK é uma “arma secreta“: faz parte daquele conjunto de ferramentas que sempre estiveram ao seu lado (surge em 1977 e quase toda distribuição Linux a inclui), você a ignora durante toda a sua vida e em um belo dia resolve experimentá-la. É o momento em que aquele sentimento de “ENTÃO QUER DIZER QUE SOFRI TODOS ESTES ANOS A TOA???” aparece. :)
O que é AWK?
Trata-se de uma linguagem de programação cujo nome vêm das iniciais dos seus criadores (Alfred Aho, Peter Weinberger e Brian Kernighan – galerinha bruta) e nos permite escrever programas que tenham como fonte de dados arquivos texto. O mais impressionante é que você consegue resultados incríveis com pouquíssimas linhas de código.
É uma excelente ferramenta quando desejamos extrair inteligência de arquivos texto, especialmente logs. Sei que esta descrição é muito ruim, sendo assim, que tal irmos direto ao código?
Exemplo real: log de acesso do /dev/All
Sempre que alguém acessa o /dev/All é incluída uma linha em um arquivo de log nos informando a data, hora, IP do visitante e página visitada tal como no exemplo a seguir:
03/06/2015 10:20:00 186.213.73.169 http://www.devall.com.br 03/06/2015 10:20:01 34.13.3.69 http://www.devall.com.br 03/06/2015 10:20:20 186.213.73.169 http://www.devall.com.br 04/06/2015 10:20 186.213.73.169 http://www.devall.com.br/blog/info/3
Se quisermos saber quantos acessos tivemos por dia fica fácil pensar em um programa: dado que o caractere de espaço separa estes quatro campos basta escrever um parser simples na minha linguagem favorita e voilá: crio um acumulador para cada dia lendo o arquivo do início ao fim.
Mas e se eu te disser que consigo o mesmo efeito com apenas uma linha de AWK?
{contador[$1]++} END {for (j in contador) print j, contador[j]}
E como eu uso isto?
cat acessos.log | awk '{contador[$1]++} END {for (j in contador) print j, contador[j]}'
Gerando a saída:
03/06/2015 3 04/06/2015 1
Parece até mágica: agora vamos dissecar este código.
Entendendo o AWK
A sintaxe da linguagem é composta por dois elementos fundamentais: uma condição referente à linha corrente do arquivo sendo lido e o que deve ser executado caso esta condição seja satisfeita. Podemos resumir a sintaxe ao código abaixo:
condição {ações a serem executadas}
No nosso código temos claramente duas condições. Vamos à primeira:
{contador[$1]++}
Não há uma condição estabelecida, sendo assim o bloco entre chaves sempre será executado a cada linha lida. O código é tão simples que assusta: criamos uma tabela de hash cuja chave corresponda ao primeiro campo do arquivo.
Campo? Sim: por padrão AWK interpreta o caractere de espaço como um separador de campos. “$1” representa o primeiro campo (data), “$2” o segundo (hora) e por aí vai. Ao aplicar o operador “++” em meu hash, AWK automaticamente o interpreta como um valor numérico e o incrementa.
Ao final da leitura do arquivo temos uma tabela de hash tal como a exposta a seguir:
["03/06/2015" => 3, "04/06/2015" => 1]
Note que não precisamos declarar nossas variáveis antes de usá-las, o que nos gera um código muito mais simples. Agora vamos à segunda condição:
END { for (j in contador) print j, contador[j]}
AWK possuí algumas variáveis e condições embutidas. Dentre estas encontra-se “END”: uma condição que é satisfeita apenas quando chegamos ao final do arquivo. E o que fazemos quando isto ocorre? Geramos um relatório de saída ao nosso usuário final.
Apenas iteramos sobre as chaves presentes em nossa tabela de hash usando a estrutura de controle “for” e, para cada uma destas, iremos imprimir seu valor, seguido do número de acessos que fomos incrementando a cada linha do arquivo.
Outro exemplo: validando arquivos
Alguns anos atrás precisei interagir com um sistema que salvava notas fiscais em arquivos textuais similares ao exposto a seguir:
0100|Bolacha Mabel|3.40 0100|Leite Itambé|2.00 0200|5,40
O caractere “|” era usado para separar os campos e havia diferentes tipos de registros dentro do mesmo arquivo. Aqueles que começavam com “0100” representavam um item da nota fiscal (nome do item e valor pago respectivamente), e “0200” o valor total da nota. Uma validação simples deste arquivo é verificar se o valor total bate com o somatório do valor pago em cada item.
O validador em AWK abaixo ilustra melhor a linguagem. Iremos salvá-lo em um arquivo chamado “prog.awk”.
BEGIN {FS = "|"} $1 == "0100" { total += $3} $1 == "0200" && $2 != total { print "Valor inválido. Total = ", $2, "Somatório = ", total} $1 == "0200" && $2 == total {print "Arquivo OK"}
A primeira condição é executada quando o programa é iniciado, o que é representado pela condição “BEGIN”. Nesta linha definimos que o caractere separador de campos será “|” mudando o valor de uma variável embutida da linguagem que é o FS (Field Separator).
A segunda linha verifica se o campo 1 contém o texto “0100”. Se for o caso, iremos criar uma variável chamada “total” na qual iremos incrementar o valor presente no terceiro campo. (nota importante: evite o caractere vírgula ao lidar com números de pontos flutuante com AWK).
A terceira linha verifica se o arquivo é inválido. Primeiro checamos se o valor do primeiro campo é “0200” e se o valor do segundo campo desta linha é diferente ao que calculamos na variável “total”. Se for, iremos imprimir o erro para o usuário.
A quarta linha valida o arquivo fazendo o oposto do que foi feito na terceira.
E como executamos este programa no Linux?
cat notafiscal.txt | awk -f prog.awk
Mais recursos sobre AWK
O objetivo por trás deste post foi apenas mostrar com o que se parece a linguagem de programação para despertar sua curiosidade sobre o assunto. Claro que não cabiam aqui todos os detalhes a seu respeito, mas você os encontrará nos links abaixo:
AWK Community Portal – http://awk.info – o nome já diz tudo: é o portal da comunidade mundial com muitos links e textos interessantes a respeito.
AWK User Guide – http://www.math.utah.edu/docs/info/gawk_toc.html – é o texto que estou usando para me aprofundar na linguagem. Excelente leitura!
JAWK – http://jawk.sourceforge.net/ – Se você usa a JVM vai gostar de conhecer este projeto: é uma implementação completa do AWK para Java. Muito bom como linguagem embarcada!
AWK.net – https://awkdotnet.codeplex.com/ – Curte .net? Encontrei esta implementação, mas não cheguei a experimentá-la.
Notas finais
Minhas experiências recentes com AWK têm sido extremamente satisfatórias: em um de nossos projetos em que precisamos lidar com arquivos de log conseguimos um excelente ganho de produtividade conhecendo muito pouco a linguagem.
Espero que com este post mais pessoas incluam AWK em sua caixa de ferramentas, pois é algo que vale muito à pena aprender.
Muito bacana, Henrique. O natural muitas vezes é pensarmos em usar nossa linguagem de propósito geral preferidas para resolver qualquer problema. Parece doer menos que aprender outra linguagem mais apropriada, ou uma DSL específica para o problema em questão, como é o caso de AWK e processamento de arquivos texto. Com pouco tempo o investimento que fazemos para incluir essas novas ferramentas no nosso arsenal se paga, e com juros.
Sem dúvida: nos salvou um projeto aqui cuja primeira opção era implementar uma DSL em Groovy! :)
Muito legal Kico!
Eu ja usei o AWK, porém de modo mais basico.
A grande boa dele, na minha opinião, é a integração direta com o bash, como foi feito com o comando cat!
Abç
AWK é uma linguagem bem poderosa se vc consegue dividir o seu problema em uma série de registros contendo um ou mais campos, veja como da pra fazer coisas divertidas com 15 linhas apenas
por ser uma linguagem orientada a fluxo de dados, vc tem os blocos BEGIN, END e vc pode ter blocos para executar em dadas condições ( ou sempre ). o GAWK atualmente suporta arrays multi-dimensionais porem eu não curto muito não — se vc precisa de algo assim vc precisa talvez de um programa de verdade, testes, etc.
uma coisa que eu gosto de usar o awk é para dividir um arquivo de acordo com o conteudo. imagine que vc tem algo como
01/01/2011 abobora 1 2 3 45 7
01/01/2011 verde 84 4 3 5 46 -09090
e vc quer um arquivo com todos os ‘abobora’ e outros com todos os ‘verde’ vc pode fazer algo como
awk '{
arquivo = $2 ".log" ; # para concatenar de strings basta colocar o conteudo lado-a-lado
$2 = "";
print $0 > arquivo # um redirecionamento semelhante ao do shell
}' todos.log