Usando o AWK
por Augusto C. Campos – augusto@br-linux.org.
Publicado originalmente em 2001.
Este artigo pretende apresentar para usuários já com noções de programação o awk, um utilitário poderoso aplicável a qualquer situação onde seja necessário extrair conteúdo a partir de arquivos texto com formatação coerente. O nome awk não tem tradução, e consiste nas iniciais dos seus três autores, um dos quais é também co-autor de outra inguagem famosa: o C. O awk está disponível desde 1977, mas as versões disponíveis atualmente são mais recentes, datando da segunda metade da década de 80.
Antes de continuar a leitura, é ideal que você obtenha uma versão de awk que possa ser usada para praticar os exemplos fornecidos. A sugestão é fazer o download diretamente em http://www.gnu.org/software/gawk/gawk.html – site da mais popular e moderna implementação do awk. Versões para DOS e Windows podem ser encontradas em http://www.weihenstephan.de/~syring/win32/UnxUtils.html ou http://www.crossmyt.com/hc/htmlchek/awk-perl.html mas atenção: estas versões têm limitações estranhas, e usualmente não são tão estáveis e flexíveis como as disponíveis para outros sistemas operacionais. ;-)
O início
Um programa em awk tem a seguinte forma geral:
padrão_1 { ação_1 }
padrão_2 { ação_2 }
padrão_n { ação_n }
Você pode gravar os programas em awk na forma de arquivos texto (em geral com a extensão “.awk”, mas isto não é obrigatório), e executá-los invocando o interpretador awk da seguinte forma:
gawk -f teste.awk arq-entrada1.txt arq-entrada2.txt
No exemplo, seria executado o programa teste.awk, e o programa leria como entradas os arquivos arq-entrada1.txt e arq-entrada2.txt. Existem muitas outras maneiras de invocar o awk, e veremos algumas delas no decorrer do artigo. Conte com a documentação (farta!) para conhecer as outras formas alternativas.
Para entender o mecanismo do awk, nada melhor que um exemplo. Considere um relatório de presenças de uma turma de universidade, contendo o nome de cada aluno, o código de sua turma e o número de faltas no semestre corrente, gravado com o nome de chamada.txt. Seu formato é como segue:
Augusto Cesar Campos;3001;6
Ronaldo Hornburg;3001;2
Flavio Luis Costa;3002;3
Gabriela Silva;3002;8
Arnaldo Cesar Coelho;3003;15
Note que temos um registro por linha, e todos os campos são separados por um símbolo fixo e que não ocorre no conteúdo – o ponto-e-vírgula. Estas são condições ideais para o processamento em awk, e muitas vezes você passará muito mais tempo tentando garantir que seus arquivos de entrada alcancem a forma ideal, do que realmente processando-os para obter o resultado esperado.
Vamos a um exemplo simples: a partir do arquivo chamada.txt, gerar uma listagem simples dos alunos que tiverem ultrapassado o limite de faltas aceitável, que é de 12 ausências.
Crie o seguinte arquivo com o nome de faltosos.awk:
$3 > 12 {
OFS=";" # define qual deverá ser
# o separador de campos
# na saída
print $1,$2,$3 # imprime os 3 campos
}
e execute-o assim:
awk -F “;” -f faltosos.awk chamada.txt
Note que há um parâmetro extra, o -F, que serve para definir qual o caracter separador de campos no arquivo de entrada. Se você não informa o -F, o awk sempre assume que os brancos (um ou mais espaços ou tabs) serão os separadores.
Como esperado, o programa acima produzirá a seguinte saída:
Arnaldo Cesar Coelho;3003;14
Vamos agora dissecá-lo. Note que o programa não tem nenhuma espécie de cabeçalho, iniciando diretamente com uma estrutura típica de awk – uma condição, seguida por uma ação colocada entre chaves.
A condição oferecida é $3 > 12 – ou seja, a ação correspondente será efetuada apenas para as linhas do arquivo de entrada onde o valor do terceiro campo for maior que 12. Neste caso, a ação é bastante simples: é definido o separador dos campos do registro de saída como sendo o ponto-e-vírgula na variável interna OFS (algo que você não precisa fazer, mas a tradição dos filtros de Unix manda sempre que possível devolver a saída no mesmo formato da entrada), e em seguida imprime na saída padrão (em geral a tela) os três campos do arquivo de entrada.
O awk irá ler o arquivo de entrada, linha por linha, e para cada linha da entrada irá testar todas as condições (nao apenas a primeira encontrada!), executando as ações pertinentes. Você pode ter um número indefinido de conjuntos de condições e ações, todas elas com o mesmo formato acima.
Processando cabeçalhos e rodapés
Muitas vezes você irá precisar mudar o formato do arquivo, acrescentar cabeçalhos e rodapés (ao arquivo de saída, e não a cada página individual), calculando totais para o conjunto das linhas e outras operações comuns. O awk tem suporte a tudo isto, como veremos a seguir.
Experimente o seguinte programa resumo_chamada1.awk:
$3 > 12 {
reprovados++
}
{
alunos++
total_faltas = total_faltas + $3
}
END {
if (reprovados>0) {
print "Neste semestre,",reprovados,"alunos rodaram por faltas."
}
print "Foi registrado um total de",total_faltas,"faltas,"
print "perfazendo uma média de",total_faltas/alunos," por aluno."
}
Execute-o com o comando
awk -F “;” -f resumo_chamada1.awk chamada.txt
e você deverá ver a seguinte saída:
Neste semestre, 1 alunos rodaram por faltas.
Foi registrado um total de 33 faltas,
perfazendo uma média de 5.66667 por aluno.
Creio que a estrutura do programa deve parecer óbvia para qualquer pessoa com experiência em linguagens de terceira geração, mas alguns comentários podem ser úteis.
Em primeiro lugar, note que nenhuma variável é declarada. Os tipos e tamanhos das variáveis são definidos pelo contexto, e você pode perfeitamente somar valores a uma variável que tem conteúdo string, ou concatenar uma string a uma variável que tem conteúdo numérico. Uma consulta à documentação explicará como o awk trata este tipo de operação incomum.
Note também que não é necessário definir nada em especial para manter múltiplas condições em um mesmo programa. O interpretador awk irá testar todas elas.
Propositalmente usei duas condições especiais no mesmo programa. Note que o segundo bloco de ações não está associado a condição nenhuma – isto significa que ele deverá ser executado para todas as linhas.
O terceiro bloco está associado a outra condição especial – a condição END (sempre em maiúsculas), que é executada após a leitura do último registro de entrada. Uma condição análoga é a BEGIN, executada antes do interpretador realizar a abertura do primeiro arquivo de entrada.
Note que usei dois estilos de atribuições de expressões a variáveis: o estilo usual, como em total_faltas = total_faltas + $3, e o estilo reduzido, como em alunos++. O awk suporta os dois formatos, e eu poderia ter escrito de outras maneiras, como em alunos = alunos + 1 e alunos += 1. Note que o sinal de = sempre significa atribuição – se você precisar fazer uma comparação, será preciso escrever um duplo sinal de igual (“==”). Veja:
Certo: if (a == 0) { print “zero!” }
Errado: if (a = 0) { print “zero!” }
Analisando a saída do programa, veja que a formatação dos valores numéricos está bastante feia – nem sempre você irá querer valores com 5 casas decimais. Para evitar isto, você pode usar o printf (típico da linguagem C e do mundo Unix) com sua sintaxe padrão, inclusa na documentação do awk. Se escrevêssemos a última linha do programa assim:
printf ("perfazendo uma média de %5.2f por aluno.
",
total_faltas/alunos);
Teríamos o seguinte resultado:
Neste semestre, 1 alunos rodaram por faltas.
Foi registrado um total de 34 faltas,
perfazendo uma média de 5.67 por aluno.
Se você não conhece o printf, consulte a documentação – vale a pena. Se você sabe usar o printf mas nunca decora o significado das letras de controle de conteúdo, tenha à mão a seguinte tabela:
%c = um caracter
%d, %i = um inteiro decimal
%e, %E = um número em notação científica
%f = um número em ponto flutuante
%G, %g = um número em notação científica ou ponto flutuante – o que for menor
%o = um inteiro octal sem sinal
%s = uma string
%x, %X = um número hexadecimal
Por exemplo, para escrever um número real em notação de ponto flutuante, ocupando 6 casas, sendo 2 após o ponto, use %6.2f.
As expressões
Um dos pontos que diferenciam as linguagens de programação entre si é a notação utilizada para descrever suas expressões. Já vimos alguns exemplos, e vamos agora tentar listar todas as possibilidades.
Em primeiro lugar, as definições de tipos. O awk irá reconhecer como número qualquer valor que seja um inteiro, um valor decimal ou um número em notação científica. Exemplos: 102, 102.0, 1.02e+2, 1020e-1
Uma string em geral será definida como uma sequência de caracteres incluída entre aspas duplas. Exemplo: “eleitorado da flórida”.
O awk também reconhece expressões regulares, embora não estejamos abordando o assunto neste artigo. A notação é a inclusão da expressão entre duas barras. Exemplos: /^TRE-S[CP]/ (esta expressão, se colocada como condição de uma ação em awk, seria verdadeira para qualquer linha que começasse com TRE-SC ou TRE-SP); /TRE-?SC/ (verdadeira para linhas que contenham a string TRESC ou TRE-SC. Consulte a documentação para aprender a trabalhar com expressões regulares.
Variáveis não precisam de declaração – elas recebem seu valor logo no seu primeiro uso. Ao contrário de outras linguagens como o perl, as variáveis não precisam de nenhum prefixo especial (como $variavel). O prefixo $ é utilizado apenas para referenciar os campos do registro de entrada, onde $1 é o primeiro campo, $2 é o segundo campo, e assim por diante – $0 é o registro inteiro. A variável NF sempre conterá o número de campos do registro corrente, e NR indica o número do registro corrente.
As operações aritméticas disponíveis são as seguintes, já em ordem de precedência:
-x – negação
x^y – potenciação
x*y – multiplicação
x/y – divisão
x%y – resto da divisão
x+y – soma
x-y – subtração
A única operação com strings é a concatenação, e ela é feita sem necessidade de sinais. Exemplos:
detalhe = "O resultado é: " result
print "Atenção: " detalhe
frase = palavra1 " " palavra2 palavra3
Há toda uma lista de variáveis internas úteis, e você pode encontrá-las na documentação. Alguns destaques:
OFMT – formato (estilo do printf) usado na conversão de números para visualização.
FS – separador de campos da entrada
OFS – separador dos campos de saída
ORS – separador de registros de saída
ARGC, ARGV – descritores de parâmetros de chamada
ENVIRON – vetor contendo todas as variáveis do ambiente
ERRNO – número do erro ocorrido na última chamada ao sistema
NF – número de campos na linha corrente
NR – número do registro corrente
As funções internas
O awk não inova nas funções disponíveis para o programador. Vejamos uma lista das mais importantes; deixamos à documentação a tarefa de explicar os detalhes. Lembre-se de que você também pode definir suas próprias funções, o que contribui bastante para a criação de programas modularizados.
Funções numéricas:
int(x) – “trunca” o valor de x, desprezando a parte decimal.
sqrt(x) – raiz quadrada positiva
exp(x) – exponencial de x, ou seja, e elevado a x
log(x) – logaritmo natural de x
sin(x) – seno de x – em radianos
cos(x) – cosseno de x – em radianos
atan2(y,x) – arco tangente de y/x em radianos
rand() – número real pseudo-aleatório entre 0 e 1
srand(x) – inicializa o gerador de números aleatórios
Funções de strings
index(string, substring) – posição da primeira ocorrência da substring dentro da string.
length(string) – comprimento da string
match(string,regexp) – verifica se a expressão regular regexp ocorre dentro da string
split(string, array, [separador]) – separa os componentes da string em elementos do array, usando opcionalmente o separador.
sprintf(formato, expressões) – formata as expressões de acordo com as definições do formato, de maneira análoga ao comando printf.
sub(regexp,valor,string) – procura uma ocorrência da expressão regexp em string, e substitui por valor.
gsub(regexp,valor,string) – similar a sub, mas substitui todas as ocorrências encontradas, e não apenas a primeira.
gensub(regexp,replacement,tipo,string) – substituição genérica
substr(string,inicio,tamanho) – retorna uma substring de string, começando da posição inicio e contendo tamanho caracteres.
tolower(string) – converte para minúsculas
toupper(string) – converte para maiúsculas
Entrada e saída
close(nome) – fecha um arquivo ou pipe – desnecessária em scripts simples
fflush(nome) – esvazia o buffer
system(comando) – executa o comando através da shell
Tempo
systime() – retorna o número de segundos decorrido desde 1/1/1970
strftime(formato,tempo) – retorna uma data formatada de acordo com as definições do comando date do Unix.
Controle de fluxo
Já vimos exemplos da forma mais simples de seleção, o if. O awk suporta o if com a seguinte sintaxe:
if (x%2 == 0) {
print "x é par"
} else {
print "x é ímpar"
}
A condição sempre deve estar entre parênteses!
O conceito de iteração também está implementado, com teste no começo ou no final do bloco. Exemplo:
# teste no inicio
while (i <= 3) {
print $i
i++
}
# teste no final
do {
i++
print $i
} while (i <= 3)
A contagem através do for funciona com a sintaxe da linguagem C: for (inicialização; condição; incremento). Exemplo:
for (i=1; i <= 3; i++) {
print $i
}
O comando break pode ser usado para sair de um loop a qualquer momento. O comando continue pode ser usado para forçar o próximo ciclo de um loop, pulando os próximos passos ainda restantes.
Outros comandos ainda têm influência sobre o fluxo da execução de programas. O comando next força a leitura do próximo registro de entrada, sem verificar as demais condições do programa. O nextfile fecha o arquivo de entrada corrente, e passa a processar o próximo, se houver. O comando exit encerra a execução do programa como um todo.
Vetores (arrays)
A linguagem awk permite o uso de vetores (unidimensionais por definição), com sintaxe parecida com a da linguagem pascal, colocando o índice entre colchetes ao lado do nome do vetor. As regras para atribuição de valores a vetores são as mesmas do que as das variáveis comuns:
vetor[15] = "fosfosol";
Vamos a mais um exemplo baseado no nosso arquivo de chamadas - chame-o de por_turma.awk:
{
if (!($2 in faltas)) {
conta_turmas++;
turmas[conta_turmas]=$2
}
faltas[$2] += $3
alunos[$2]++;
}
END {
for (i=1;i<=conta_turmas;i++) {
print "Média de faltas da turma",turmas[i],
":",faltas[turmas[i]]/alunos[turmas[i]]
}
}
Execute-o com:
awk -F ";" -f por_turma.awk chamada.txt
para ver o seguinte resultado:
Média de faltas da turma 1 : 4
Média de faltas da turma 2 : 5.5
Média de faltas da turma 3 : 15
Novamente não vamos explicar linha por linha o programa, já que pressupomos que você tem condições de seguir uma estrutura simples. Vamos direto aos detalhes!
Note que mantemos três vetores separados - o primeiro, chamado turmas, é indexado de forma sequencial (índice crescente a partir de 1). Os outros dois, chamados de faltas e alunos, são indexados pelo código da turma.
O primeiro if (if (!($2 in faltas))) verifica se o segundo campo da linha corrente ainda não consta como índice do vetor faltas. Caso ainda não conste, o valor de conta_turmas é incrementado, e um novo item é acrescentado ao vetor turmas. Note o uso de um ponto de exclamação para negar (not lógico) a condição do if!
Logo em seguida, o total de faltas do registro corrente é somado ao registro da turma correspondente (caso ele ainda não exista, é criado automaticamente), e o contador de alunos da turma é incrementado. Tudo isto em uma condição vazia, executada para todas as linhas de entrada.
Em seguida, em uma condição END (executada apenas ao fim do arquivo) de entrada, fazemos uma repetição for simples, iterando todos os itens do vetor turmas e utilizando-os como índice dos vetores de faltas e alunos para calcular as médias - um array dentro do outro. Simples, não?
Outros programas
O awk não tem a riqueza de linguagens similares como o Perl, capaz de interfacear diretamente com as chamadas do sistema operacional. Desta forma, muito se faz através da interação com outros utilitários, como o sort (embora no gawk em geral ele não seja necessário, vamos usá-lo como exemplo em nome com a compatibilidade POSIX). Você pode chamar estes utilitários diretamente a partir do seu programa awk, ou pode montar encadeamentos através das pipes da shell. Vamos a um exemplo prático, lembrando que esta não é a melhor maneira de fazer isto - é apenas para fins didáticos!
Sabendo que o comando ls -l do Unix tem o seguinte formato de saída:
-rw-r--r-- 1 brain users 377106 nov 10 10:46 rejeitados.zip
-rw-r--r-- 1 brain users 417 nov 16 13:19 resumo_chamada1.awk
drwxr-xr-x 2 brain root 1024 nov 7 13:27 scripts
-rw-r--r-- 1 brain users 2211 nov 14 16:08 sedes.txt
-rw-r--r-- 1 brain users 7314 nov 9 14:22 segemail.htm
-rw-r--r-- 1 brain users 235 nov 9 13:32 segemail.htm~
drwxr-xr-x 3 brain users 1024 set 29 12:01 sources
drwxr-xr-x 49 brain users 1024 nov 10 16:43 src
vamos construir um pequeno script capaz de mostrar os nomes e tamanhos dos 10 maiores arquivos de um diretório, e ao final exibir a soma total dos seus tamanhos.
Embora o ls tenha seu próprio mecanismo de ordenação, vamos usar o comando sort para ter maior efeito didático. Em primeiro lugar, edite o script awk capaz de realizar a parte "quente" da nossa tarefa, e salve-o como 10maiores.awk
FNR <= 10 {
total=total+$5
print FNR,$9,$5
}
END {
print total
}
Note que a primeira condição verifica se o número da linha corrente é menor ou igual a 10!
Agora execute, na shell, a seguinte linha contendo uma cadeia de comandos:
ls -l | sort -nrk5 | awk -f 10maiores.awk
Esta linha tira proveito do mecanismo conhecido como pipe (simbolizado pela barra vertical), onde a saída de um comando é utilizada como entrada pelo próximo comando. Note que o comando sort ordena uma lista qualquer, e os parâmetros utilizados (nrk5) significam: "ordenar em ordem numérica [n] invertida [r] pelo quinto campo [k5]". Você deverá ver uma saída como a que segue:
1 bigfile.gzgz 2167512
2 rejeitados.zip 377106
3 kmago-0.4.tar.gz 270939
4 timofometro_1.1.tar.gz 204782
5 acomjufa.zip 145368
6 ATT00190.jpg 52861
7 edy3.jpg 45230
8 pppcosts-0.66.src.tar.gz 40031
9 pppcosts-0.66.ELF.tar.gz 23274
10 procmail-sanitizer.tar.gz 22987
3350090
Não cabe aqui ensinar a usar os filtros do Unix, mas se você realmente quer tirar proveito dos scripts do Unix, não deixe de se informar sobre os filtros mais comuns. Aprenda também a usar filtros menos tradicionais como o lynx (sempre com os parâmetros -dump e -nolist), capaz de converter para formas facilmente "digeríveis" pelo awk os onipresentes arquivos html da atualidade. Procure também pelos filtros capazes de converter o formato doc para texto, e você poderá fazer maravilhas com os arquivos típicos de Windows que todo mundo lhe manda pelo e-mail!
Conclusão
Isto encerra nossa introduçao ao awk, um filtro bastante dinâmico e completo, capaz de simplificar muitas tarefas em ambiente Unix. Há muito mais a saber sobre esta ferramenta, mas não temos como sintetizar todo este conhecimento neste espaço. Portanto, leia as referências abaixo, e ganhe um novo aliado no tratamento de textos!
Para saber mais
FAQ - http://www.landfield.com/faqs/computer-lang/awk/faq/
Livro - http://www.oreilly.com/catalog/sed2/
gawk - http://www.gnu.org/software/gawk/gawk.html
manual online - http://www.polaris.net/docs/gawk/
Parabéns pelo artigo, Augusto!
Eu sempre gostei muito da linguagem AWK. É uma linguagem realmente interessante, e uma mão-na-roda para fazer scripts.
Para quem gostou da linguagem, eu gostaria de recomendar também o manual oficial do GNU awk. Trata-se de uma leitura bastante agradável.
Para atiçar a curiosidade do leitor, o GNU awk possui diversas extensões com relação ao awk padrão. Por exemplo, é possível até mesmo fazer um servidor HTTP utilizando a extensão de sockets.
As últimas versões do GNU awk podem até mesmo ser estendidas com novas funções carregadas de bibliotecas dinâmicas em C.
Embora seja uma sigla, a linguagem hoje é bem AWKward mesmo.
Fazia sentido na época do Unix original, pois era sua linguagem de script nativa. Mas foi ultrapassada ao longe por Perl/Python/Ruby em termos de recursos e facilidade sintática.
Por outro lado, tem um recurso em awk que acho que não tem em nenhuma outra: delimitar uma busca por padrões dentro de uma região textual especificada. Não me lembro da sintaxe ou switches necessários. Sim, possivelmente é possível fazer o mesmo em Perl com uma ER mais complicada, mas não da forma simples como em awk.
No contexto dos scripts para tarefas do sistema, quando é possível implementar algo em awk, há uma vantagem bem específica sobre Perl, Python, Ruby, PHP, etc., que é a portabilidade maior pelo fato de o awk fazer parte do POSIX, e assim estar presente (ao menos na forma definida no posix, que é bem mais restrita do que interpretadores modernos implementam) em todos os sistemas operacionais que aderem ao padrão, e nos que seguem parcialmente também. Até sistemas embarcados usando o busybox podem interpretar scripts awk.