Salta al contenuto principale

awk: elaborare testo a colonne

Inviato da tuxsa il

Hai un elenco, un registro o l'output di un comando fatto di colonne, e ti serve la terza, oppure la somma della quarta, oppure solo le righe in cui un valore supera una soglia. Per tutto questo, e per molto altro, c'è awk: un piccolo linguaggio di programmazione pensato per leggere il testo riga per riga e dividerlo in campi.

Cos'è

awk prende il nome dalle iniziali dei suoi autori (Aho, Weinberger, Kernighan) ed è presente praticamente su ogni sistema Unix e Linux. Dove cut si limita a estrarre colonne, awk sa anche confrontare, calcolare, contare, raggruppare e riformattare. Su Debian e Ubuntu il comando awk è per default mawk, una versione leggera; esiste anche gawk (la versione GNU, con funzioni in più), installabile con sudo apt install gawk. Tutto ciò che vedi in questo articolo funziona con entrambe, salvo dove indicato.

Cosa fa

Legge l'input una riga alla volta (un record), la divide in campi (per default separati da spazi o tabulazioni) e per ciascuna riga applica le regole che gli hai dato. Ogni regola ha la forma:

condizione { azione }

Se la riga soddisfa la condizione, viene eseguita l'azione. Se manca la condizione, l'azione vale per tutte le righe; se manca l'azione, la riga viene semplicemente stampata. I campi si chiamano $1, $2, $3…, mentre $0 è la riga intera. In più ci sono le variabili speciali, come NR (il numero della riga corrente) e NF (il numero di campi della riga).

Come si usa

$ awk [opzioni] 'programma' [file...]
$ comando | awk 'programma'

Il programma va quasi sempre tra apici singoli, perché $1, le graffe e le virgolette non vengano interpretate dalla shell. Per gli esempi usiamo un piccolo file di vendite, vendite.txt, con tre colonne: prodotto, quantità e prezzo unitario in euro (con il punto decimale, come vuole awk):

$ cat vendite.txt
mele 10 1.50
pere 4 2.20
arance 25 0.90
mele 6 1.50
pere 12 2.20

Esempio 1: stampare alcune colonne

$ awk '{print $1, $3}' vendite.txt
mele 1.50
pere 2.20
arance 0.90
mele 1.50
pere 2.20
$ awk '{print $3 " euro per " $1}' vendite.txt
1.50 euro per mele
2.20 euro per pere
...
$ awk '{print $NF}' vendite.txt
1.50
2.20
...

Con print si stampano i campi indicati. Le virgole tra i campi inseriscono uno spazio (il separatore di output); senza virgola e con un testo tra virgolette i pezzi vengono semplicemente uniti. $NF è l'ultimo campo, qualunque sia il numero di colonne: utile quando le righe hanno lunghezze diverse. Con $(NF-1) si ottiene il penultimo.

Esempio 2: un altro separatore con -F

$ awk -F: '{print $1, $7}' /etc/passwd | head -n 3
root /bin/bash
daemon /usr/sbin/nologin
bin /usr/sbin/nologin
$ awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd
enzo
$ awk -F, '{print $2}' dati.csv
$ awk -F'\t' '{print $1}' tabella.tsv

Con -F si sceglie il separatore dei campi: qui i due punti di /etc/passwd, dove la prima colonna è il nome utente, la terza il numero dell'utente e la settima la shell. Il secondo comando mostra gli utenti "reali": numero da 1000 in su, escludendo l'utente speciale nobody (65534). Il separatore può essere una virgola (file CSV semplici) o una tabulazione ('\t'). Per un comportamento simile a cut, ma con in più la possibilità di fare confronti, è la scelta naturale.

Esempio 3: numeri di riga e numero di campi

$ awk '{print NR ": " $0}' vendite.txt
1: mele 10 1.50
2: pere 4 2.20
3: arance 25 0.90
4: mele 6 1.50
5: pere 12 2.20
$ awk 'NR==2' vendite.txt
pere 4 2.20
$ awk 'NR>=2 && NR<=4' vendite.txt
$ awk 'NF != 3' vendite.txt
$ awk 'END {print NR}' vendite.txt
5

NR è il numero della riga: serve a numerare, a selezionare una riga (NR==2) o un intervallo, e alla fine (END) contiene il totale delle righe, come wc -l. NF != 3 trova le righe che non hanno tre campi: un modo veloce per scoprire righe malformate in un file.

Esempio 4: filtrare con condizioni

$ awk '$2 > 8' vendite.txt
mele 10 1.50
arance 25 0.90
pere 12 2.20
$ awk '$1 == "mele"' vendite.txt
mele 10 1.50
mele 6 1.50
$ awk '$1 == "pere" && $2 > 5 {print $1, $2}' vendite.txt
pere 12
$ awk '/ar/' vendite.txt
arance 25 0.90
$ awk '$1 ~ /^m/' vendite.txt
mele 10 1.50
mele 6 1.50
$ awk '$1 !~ /e$/' vendite.txt
arance 25 0.90

Una condizione può essere un confronto (>, <, ==, !=), una combinazione con && (e), || (o), ! (non), oppure un'espressione regolare tra barre (come in grep). Con ~ si verifica che un campo corrisponda a un modello (!~ che non corrisponda): qui ^m significa "comincia per m" e e$ "finisce con e". Con il solo confronto e senza azione, la riga viene stampata intera.

Esempio 5: calcoli per riga

$ awk '{print $1, $2 * $3}' vendite.txt
mele 15
pere 8.8
arance 22.5
mele 9
pere 26.4
$ awk '{$2 = $2 * 2; print}' vendite.txt
mele 20 1.50
...

Qui awk funziona da calcolatrice: $2 * $3 moltiplica quantità e prezzo, per avere l'incasso di ogni riga. Si possono anche modificare i campi: nel secondo esempio la quantità viene raddoppiata, e print senza argomenti stampa la riga intera con il campo cambiato (e gli spazi riuniti in uno solo).

Esempio 6: totali e medie con BEGIN ed END

$ awk '{tot += $2 * $3} END {print "Totale:", tot}' vendite.txt
Totale: 81.7
$ awk '{tot += $2 * $3} END {printf "Totale: %.2f euro\n", tot}' vendite.txt
Totale: 81.70 euro
$ awk '{s += $2} END {print "Media quantità:", s / NR}' vendite.txt
Media quantità: 11.4
$ awk 'BEGIN {print "--- Rapporto vendite ---"} {print $1, $2} END {print "--- Fine ---"}' vendite.txt
--- Rapporto vendite ---
mele 10
...
--- Fine ---

I blocchi speciali BEGIN ed END vengono eseguiti una volta sola, prima di leggere la prima riga e dopo l'ultima. Le variabili non hanno bisogno di essere dichiarate e partono da zero: tot += ... accumula una somma, e in END si stampa il risultato. printf permette di formattare l'output come nel linguaggio C (%.2f due decimali, %d intero, %s testo, %5d numero largo 5 caratteri), ma a differenza di print non va a capo da solo: serve \n.

Esempio 7: raggruppare con gli array

$ awk '{q[$1] += $2} END {for (k in q) print k, q[k]}' vendite.txt
mele 16
pere 16
arance 25
$ awk '{q[$1] += $2} END {for (k in q) print k, q[k]}' vendite.txt | sort -k2 -nr
arance 25
mele 16
pere 16
$ awk '{n[$1]++} END {for (k in n) print k, n[k]}' vendite.txt

Gli array associativi di awk usano un testo come indice: q[$1] += $2 somma le quantità separatamente per ciascun prodotto. Alla fine, for (k in q) scorre tutte le chiavi. Nota bene: l'ordine non è garantito, quindi per un elenco ordinato si passa il risultato a sort. Con n[$1]++ si conta quante volte compare ogni valore, come sort | uniq -c ma senza l'ordinamento preventivo.

Esempio 8: analizzare un registro di nginx o Apache

$ awk '{print $9}' access.log | sort | uniq -c | sort -nr
   8421 200
    312 304
     57 404
      9 301
$ awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -nr | head -n 3
     21 /wp-login.php
     14 /favicon.ico
      9 /vecchia-pagina
$ awk '{c[$1]++} END {for (ip in c) print c[ip], ip}' access.log | sort -nr | head -n 5
$ awk '{b += $10} END {printf "%.1f MB\n", b / 1048576}' access.log
$ awk '$9 >= 500' access.log | wc -l

Nel formato standard dei registri di accesso (combined), il campo 1 è l'indirizzo del client, il 7 la pagina richiesta, il 9 il codice di risposta HTTP e il 10 i byte inviati. Con poche righe si ottengono: la distribuzione dei codici di risposta (anche con cut, ma qui si può filtrare per condizione), le pagine che danno errore 404, i client più attivi (con awk che conta e sort che ordina), il traffico totale in megabyte e il numero di errori del server (codici 500 e oltre). È il caso d'uso in cui awk dà il meglio.

Esempio 9: filtrare l'output di altri comandi

$ df -h | awk 'NR > 1 && $5 + 0 > 80 {print $6, $5}'
/ 85%
$ free -m | awk '/^Mem/ {printf "Memoria usata: %.0f%%\n", $3 / $2 * 100}'
Memoria usata: 42%
$ ps aux | awk '{print $11}' | sort | uniq -c | sort -nr | head -n 3
$ ls -l | awk '{s += $5} END {print s " byte"}'

awk si mette volentieri in fondo a una pipe, per estrarre il dato che serve da un comando che stampa tabelle. Nel primo esempio, NR > 1 salta l'intestazione e $5 + 0 trasforma "85%" nel numero 85 (il segno di percentuale viene ignorato quando si fa un'operazione aritmetica), così da confrontarlo con 80: si ottengono i file system quasi pieni (vedi l'articolo su df). Il secondo calcola la percentuale di memoria usata: /^Mem/ seleziona la riga giusta e %% in printf stampa un segno di percentuale. L'ultimo somma la dimensione dei file elencati da ls -l.

Esempio 10: funzioni sul testo

$ awk '{print toupper($1), length($1)}' vendite.txt
MELE 4
PERE 4
ARANCE 6
MELE 4
PERE 4
$ awk '{print toupper(substr($1, 1, 1)) substr($1, 2)}' vendite.txt
Mele
Pere
Arance
...
$ echo "prezzo: 1,50 euro" | awk '{sub(",", "."); print}'
prezzo: 1.50 euro
$ echo "a-b-c-d" | awk '{gsub("-", "/"); print}'
a/b/c/d
$ echo "mele,pere,arance" | awk '{n = split($0, v, ","); print n, v[2]}'
3 pere

Le funzioni di base: length() (lunghezza), toupper() e tolower() (maiuscole e minuscole), substr(testo, inizio, lunghezza) (parte di un testo; si comincia a contare da 1), sub() e gsub() (sostituiscono la prima / tutte le occorrenze di un modello: sono il sed di awk), split() (divide un testo in un array) e index() (posizione di un testo in un altro). La sostituzione della virgola con il punto è un caso pratico importante: awk non capisce i numeri con la virgola decimale (1,50 viene letto come 1).

Esempio 11: variabili dalla shell con -v

$ awk -v soglia=8 '$2 > soglia {print $1}' vendite.txt
mele
arance
pere
$ PRODOTTO="pere"
$ awk -v p="$PRODOTTO" '$1 == p {print $2}' vendite.txt
4
12
$ awk -v OFS=";" '{print $1, $2, $3}' vendite.txt
mele;10;1.50
...

Nel programma tra apici singoli la shell non sostituisce le variabili, e per questo si passano con -v nome=valore. È il modo corretto e sicuro: usare gli apici doppi e scrivere $PRODOTTO dentro il programma funziona solo in casi semplici, e confonde i $ della shell con quelli di awk. Con -v OFS=";" si cambia il separatore di output, che permette di convertire un file in CSV in una riga.

Esempio 12: un programma in un file

$ cat riepilogo.awk
# riepilogo.awk: totale per prodotto
{
    q[$1] += $2
    incasso[$1] += $2 * $3
}
END {
    for (p in q)
        printf "%-8s %4d pezzi %8.2f euro\n", p, q[p], incasso[p]
}
$ awk -f riepilogo.awk vendite.txt | sort
arance     25 pezzi    22.50 euro
mele       16 pezzi    24.00 euro
pere       16 pezzi    35.20 euro

Quando il programma cresce, conviene scriverlo in un file e lanciarlo con -f. In un file si può andare a capo, indentare e commentare (con #). %-8s allinea il testo a sinistra in 8 caratteri e %8.2f allinea il numero a destra, con due decimali: così la tabella risulta ordinata. Un file di questo tipo si può anche rendere eseguibile con la prima riga #!/usr/bin/awk -f.

Esempio 13: uso negli script

#!/bin/bash
LOG="${1:?Indica il file di registro}"

ERRORI=$(awk '$9 >= 500' "$LOG" | wc -l)
TOTALE=$(awk 'END {print NR}' "$LOG")

if [ "$TOTALE" -eq 0 ]; then
    echo "Registro vuoto" >&2
    exit 1
fi

PERC=$(awk -v e="$ERRORI" -v t="$TOTALE" 'BEGIN {printf "%.2f", e / t * 100}')
echo "Richieste: $TOTALE, errori del server: $ERRORI ($PERC%)"

Nello script awk viene usato in tre modi: per filtrare, per contare e, con BEGIN e senza file di input, come calcolatrice per le percentuali (la shell sa fare solo calcoli con numeri interi). Il codice di uscita di awk è 0 se tutto va bene, e si può controllare con exit dentro il programma, per esempio awk '$1 == "x" {found = 1} END {exit !found}'.

Opzioni utili

  • -F separatore: separatore dei campi in ingresso (-F:, -F,, -F'\t', anche un'espressione regolare, come -F'[ ,;]+').
  • -v nome=valore: imposta una variabile prima di cominciare (si può ripetere).
  • -f file: legge il programma da un file.
  • Variabili speciali: NR (numero della riga), NF (numero di campi), FS e OFS (separatore dei campi in ingresso e in uscita), RS e ORS (separatore dei record in ingresso e in uscita), FILENAME (file corrente), FNR (numero di riga nel file corrente).
  • BEGIN { ... } / END { ... }: istruzioni prima della prima riga / dopo l'ultima.
  • Istruzioni: if (...) ... else ..., for, while, next (passa alla riga successiva), exit.
  • Funzioni: length, substr, index, split, sub, gsub, match, sprintf, toupper, tolower, int, sqrt…
  • Solo in gawk: -i inplace (modifica il file direttamente), strftime() (date), gensub(), asort() (ordina gli array) e, dalla versione 5.3, --csv (CSV con i campi tra virgolette).

Attenzione a

  • Apici singoli per il programma: awk "{print $1}" (apici doppi) fa sostituire $1 alla shell, e awk riceve un programma sbagliato. Il programma si scrive tra apici singoli; se ti servono dati dalla shell, usa -v.
  • Gli apici singoli dentro il programma non si possono scrivere. Per stampare un apice usa "\047", oppure metti il programma in un file con -f.
  • I campi si contano da 1, e $0 è la riga intera. Le posizioni nei testi (substr, index) cominciano pure da 1, non da 0 come in altri linguaggi.
  • Separatore predefinito: una serie di spazi o tabulazioni conta come un solo separatore, e gli spazi iniziali vengono ignorati. Per questo awk è migliore di cut con l'output di ps, df o ls -l. Con -F' ' il comportamento resta questo, mentre con -F'[ ]' ogni singolo spazio separa un campo.
  • Numeri o testo? awk confronta come numeri se entrambi i valori sembrano numeri, altrimenti come testo: "10" < "9" come testo è vero. Se serve, forza il tipo: $2 + 0 è numero, $2 "" è testo.
  • Virgola decimale: 1,50 viene letto come 1. Prima del calcolo si sostituisce la virgola con il punto: gsub(",", ".", $3). Anche l'output di ps, top e free, con le impostazioni italiane, può contenere la virgola decimale.
  • File CSV: -F, va bene solo per file semplici. Se i campi contengono virgole tra virgolette ("Rossi, Mario",42), il risultato è sbagliato. Servono gawk --csv o strumenti come csvkit o python.
  • L'ordine di for (k in array) non è definito: se serve ordinare, si usa sort a valle, o asort() in gawk.
  • print e printf: print aggiunge da solo il fine riga, printf no. Con printf il testo fra virgolette è un formato: se contiene un % da stampare, scrivi %%; e non passargli dati altrui come formato (printf $0), ma printf "%s\n", $0.
  • Non modifica il file originale: awk ... file > file svuota il file prima ancora di leggerlo! Scrivi su un file temporaneo e poi sostituisci l'originale (awk ... file > tmp && mv tmp file) oppure usa gawk -i inplace. Per le modifiche semplici, sed -i resta una scelta pratica.
  • Righe con ritorni a capo di Windows: un file con fine riga \r\n lascia un carattere invisibile in fondo all'ultimo campo, e i confronti falliscono. Si rimuove con tr -d '\r' (vedi l'articolo su tr) o con sub(/\r$/, ""). Con cat -A si vede il ^M.
  • Righe vuote e intestazioni: un'intestazione di tabella entra nei calcoli come riga normale. Si salta con NR > 1, e le righe vuote con NF o NF > 0.
  • Quando awk non basta: per programmi lunghi, per dati strutturati (JSON, XML) o per CSV complicati, un linguaggio come Python, o strumenti come jq per JSON, sono più sicuri. awk è perfetto per le "operazioni di una riga".
  • awk, sed o cut? cut per estrarre colonne a separatore fisso, sed per sostituire testo, grep per cercare, awk quando serve ragionare sulle colonne (confrontare, calcolare, raggruppare).

Riepilogo

ComandoCosa fa
awk '{print $1, $3}' fileStampa la prima e la terza colonna
awk '{print $NF}' fileStampa l'ultima colonna
awk -F: '{print $1}' /etc/passwdUsa i due punti come separatore
awk 'NR==2' fileStampa solo la seconda riga
awk '$2 > 8' fileStampa le righe con la seconda colonna maggiore di 8
awk '$1 == "mele"' fileStampa le righe con un valore preciso
awk '$1 ~ /^m/' fileFiltra con un'espressione regolare su una colonna
awk '{s += $2} END {print s}' fileSomma una colonna
awk '{c[$1]++} END {for (k in c) print k, c[k]}' fileConta le occorrenze di ogni valore
awk '{printf "%.2f\n", $2 * $3}' fileCalcola e formatta con due decimali
awk -v x=5 '$2 > x' filePassa una variabile dalla shell
awk -v OFS=";" '{print $1, $2}' fileCambia il separatore di output
awk 'NF' fileSalta le righe vuote
awk -f programma.awk fileEsegue un programma salvato in un file