Processamento de texto em AWK

O AWK é uma linguagem de programação criada nos anos 70 com o objectivo de processar dados baseados em texto. Esta linguagem baseia-se fortemente na manipulação de strings e no uso de expressões regulares.

Porquê usar AWK

O AWK tem a vantagem de permitir executar tarefas simples sobre texto utilizando programas mais compactos que os equivalentes escritos em linguagens imperativas como a linguagem C. Isto acontece devido à inexistência de uma função main e de declaração de variáveis.

Estrutura de um programa em AWK

Os programas em AWK são constituídos por uma série de pares condição-acção, com a seguinte sintaxe:

condição { acção }

O programa vai ler o input sob a forma de registos (geralmente cada registo corresponde a uma linha) e dividir cada registo em campos (geralmente separados por espaços). Após isto, a condição vai ser testada para cada registo e nos casos em que seja verdadeira é executada a acção.

Exemplo simples: Hello World

Um programa “Hello World” em AWK tem o seguinte aspecto:

BEGIN { print "Hello World" }

Em que print é uma palavra reservada que escreve na saída os seus argumentos e BEGIN é uma condição que indica que a acção correspondente deve ser executada antes de percorrer o input (da mesma forma, existe um condição END cuja acção é executada no fim do input).

Este programa pode ser executado através de uma linha de comandos da seguinte forma:

awk 'BEGIN { print "Hello World" }'

Ou, sendo hello.awk um ficheiro contendo o código a executar:

awk -f hello.awk

Neste caso não existe input, mas caso exista ele pode ser enviado através do standard input.

Campos e variáveis

Como já foi dito, quando um registo é lido a partir do input, ele é dividido em campos (separados por espaços, por omissão). Cada um desses campos pode ser acedido através de uma variável de campo da forma $N, em que N é o número do campo ($1 representa o primeiro campo, $2 o segundo e assim sucessivamente). Existe o caso particular de $0 que contém todo o registo.

Por exemplo, a seguinte acção escreve apenas o segundo campo de cada registo:

print $2

Para além das variáveis de campo é possível definir e manipular variáveis (geralmente para guardar valores intermédios). Estas variáveis não têm associado um tipo e são automaticamente inicializadas com 0 ou "" (string vazia), dependendo da forma como forem utilizadas. Existem ainda, definidas por omissão as seguintes variáveis especiais:

  • NF: Número de campos do registo actual;
  • NR: Número do registo actual;
  • FS: Separador de campos (por omissão, espaço);
  • RS: Separador de registos (por omissão, newline).

Por exemplo, a seguinte acção escreve o último e o primeiro campo de cada registo:

print $NF,$1

Condições e expressões regulares

As condições podem ser de diversos tipos, como expressões lógicas e aritméticas (envolvendo variáveis de campo ou variáveis especiais). No entanto, o mais frequente é utilizar uma expressão regular como condição.

As expressões regulares permitem descrever de uma forma flexível padrões em strings, sendo úteis para filtrar apenas as linhas que contenham uma determinada palavra. Por exemplo, a seguinte condição define as linhas do input contendo a string programar:

/programar/

Neste caso a expressão regular equivale a uma string, mas podem ser usadas expressões regulares mais complexas:

  • /^programar/: Registos que começam com programar;
  • /programar$/: Registos que terminam com programar;
  • /^programar$/: Registos que sejam iguais a programar;
  • /[Pp]rogramar/: Registos que contêm Programar ou programar;
  • /p@p|programar/: Registos contendo p@p ou programar;
  • /[a-z]/: Registos contendo uma letra minúscula;
  • /[^a-z]/: Registos não contendo uma letra minúscula;
  • /[a-zA-Z]/: Registos contendo uma letra maiúscula ou minúscula.

Estas condições procuram a expressão regular em todo o registo. Para a procurar apenas num campo utiliza-se o operador ~. Por exemplo, a seguinte condição verifica se o segundo campo do registo contém uma letra maiúscula:

$2~[A-Z]

É também possível definir duas condições para delimitar um bloco de texto. Por exemplo, a seguinte condição define o bloco entre duas linhas que começam com programar:

/^programar/,/^programar/

Exemplos de programas em AWK

Em seguida estão alguns exemplos simples de programas escritos em AWK:

Contagem de palavras

BEGIN { words = 0 }
{
  words += NF # Cada palavra é um campo.
}
END { print words }

Contagem de linhas não-vazias

BEGIN { lines = 0 }
NF>0 { lines++ }
END { print lines }

Soma de uma lista de números (um número por linha)

BEGIN { sum = 0 }
{ sum += $1 }
END { print sum }

O mesmo que a anterior, mas somando apenas os números menores que 20

BEGIN { sum = 0 }
$1<20 { sum += $1 }
END { print sum }

Média de uma lista de números

BEGIN { sum = 0 }
{ sum += $1 }
END { print sum/NR }

Números das linhas que contêm a palavra “programar” (case-sensisitve)

/programar/ { print NR }

Números das linhas que contêm a palavra “programar” (case-insensisitve)

/[Pp][Rr][Oo][Gg][Rr][Aa][Mm][Aa][Rr]/ { print NR }

Selecção das linhas ímpares

NR%2==1 { print }

Selecção apenas das linhas entre “INÍCIO” e “FIM”

/^INÍCIO$/,/^FIM$/ { print }

Estes são apenas exemplos simples com o objectivo de indicar as capacidades do AWK. É no entanto possível construir programas mais complexos contendo vários pares condição-acção, utilizando variáveis para guardar parte de registos ou alterando os separadores de campo e registo. Estes tópicos mais avançados não são abrangidos neste artigo e podem ser consultados no guia do utilizador, disponível em http://www.gnu.org/software/gawk/manual.