Utilità Unix: awk (parte I)

Cara lettrice[1],
è tanto che non ci sentiamo, vero? Spero che tu ti sia divertita ugualmente e abbia imparato tante cose nuove leggendo BETA. D'altra parte, è noto che le cose che scrivo io non le scrive nessun altro, e quindi se ti eri appassionata alla serie "i comandi buffi di Unix" ti sarai trovata in crisi di astinenza. Però di comandi buffi non è che ce ne siano più tanti, e comunque avevo voglia di cambiare un poco. Ho pensato allora di iniziare una miniserie sulle utilità Unix: quei programmi cioè che chiunque usi il Sistema Operativo Più Bello del Mondo dà per scontate, e che usa come costruzioni ausiliarie per creare, assieme ai mattoncini dei "comandi buffi", quelle opere di alta scuola che suscitano sempre ammirazione in chi ci guarda dietro le spalle.

awk: un nome, un acronimo

Per cominciare, ho scelto di parlare di awk, forse la più misconosciuta di queste utilità. Anzi, i casi sono due: o non sai cosa sia, e io ti rispondo prontamente "un linguaggio interpretato che permette di manipolare dei file di testo"[2]; o lo sai, e sei già lì a chiedermi "Ma che diavolo me ne faccio, quando esiste perl che fa di tutto, di più?" La domanda è intelligente, tanto che sta persino nelle FAQ di comp.lang.awk[3]. Tra le risposte che si possono trovare lì e quelle mie personali, penso che le più azzeccate siano: (a) awk è più semplice; (b) ci mette meno tempo a caricarsi in memoria ed eseguire[4]; (c) non devi mettere i dollari $ davanti ai nomi delle variabili.

Detto questo, mi sembra opportuno ricordare subito che il nome awk non è stato scelto perché era bellino a pronunciarsi, ma perché i suoi autori originali sono stati - in ordine alfabetico - Alfred V. Aho, Brian W. Kernighan e Peter J. Weinberger. Saprai certamente chi è il signor K, l'amico di Dennis Ritchie; posso aggiungere per la tua cultura personale spicciola che il signor A è uno dei massimi esperti di linguaggi per calcolatore (ci ho fatto gli esami sui suoi libri...). Del signor W non so nulla, ma non penso che sia stato scelto semplicemente perché il programma uscisse fuori con un bel nome; mi fido insomma delle sue capacità.

Per terminare l'introduzione, mi preme soltanto ricordarti una cosa: non usare mai awk! Beh, se stai usando linux, in realtà awk è un link simbolico a gawk, quindi il problema non si pone. Ma negli unix commerciali la cosa è ben diversa. Il linguaggio ha subito infatti una profonda mutazione quando è stato standardizzato POSIX[5], e i programmi tipici che farò come esempio non è detto funzionino con il linguaggio originale. La cosa buffa è che, a parte l'onnipresente gawk che è arrivato alla versione 3.03 mentre sto scrivendo questo articolo, gli unix commerciali hanno l'awk POSIX, che si chiama nawk (new awk, per i pignoli). Non ho mai capito perché mai non abbiano potuto sostituire il vecchio awk... magari non volevano fare un dispiacere ai signori A, W e K. Infine, per complicare vieppiù le cose, esiste un'altra implementazione gratuita di awk, preparata da Michael Brennan e chiamata mawk. Un purista come me l'avrebbe chiamato bawk, ma tant'è.

Ad ogni modo, io parlerò dei comandi gawk, notando quando c'è qualcosa che non si trova sotto POSIX e sperando di ricordarmi di avvisare se qualcosa non è nemmeno nel "vecchio" awk.

Ma non farci troppo conto.

Opzioni e funzionamento

Per iniziare, come al solito, ti sparerò la lunga lista di opzioni del comando: ma prima è meglio che ti faccia un ripasso molto veloce su come funzionano tipicamente i comandi Unix.

L'unità fondamentale di misura unixara è il record, cioè un insieme di caratteri che finisce con un newline (Ctrl-L per noi). Questo record viene poi diviso magicamente in tanti campi (field). In pratica, chi scrive un programma unix pensa di avere un record per volta, a cui potrà accedere con $0, record formato dai campi $1, $2, ... Inoltre, il programma sarà scritto in modo che prenda il file da processare o nello standard input o tra gli ultimi parametri della riga di comando, e scriva il risultato nello standard output. E i comandi per processare il file? Beh, se di comandi ce ne sono tanti, li si scrive su un file e si dice al programma di leggerselo; ma se si ha fretta e i comandi sono uno o due, il metodo più comodo è scrivere i comandi dopo le opzioni, infilarli tra due apici ' ' per tenere tutto insieme[6], e vedere l'effetto che fa (spesso un errore di sintassi, almeno prima di avere letto i miei articoli :-)).

Adesso sei pronta a sciropparti la lista dei comandi. Quelli comuni a tutti non sono poi tanti: le due forme standard sono infatti

   awk [ -F fs ] [ -v var=valore ] [--] 'programma' file ...
quando il programma è interno alla riga di comando, e
   awk [ -F fs ] [ -v var=valore ] -f progfile [--] file ...
quando il programma è contenuto in un file. Come vedi, puoi usare quanti file di input vuoi, e ciò è bello. Puoi anche non averne nessuno, nel qual caso awk leggerà dallo standard input, e ciò è ancora più bello. Inoltre, anche se non l'ho indicato negli esempi, puoi anche avere tanti file di programma quanti vuoi: basta scrivere tante opzioni -f, e il programma si occuperà di concatenarli nell'ordine prima di leggerli. E che me ne faccio, dirai? Non potevo già concatenarli io? Beh, non ci sarebbe gusto a farsi tanti bei file di libreria. Puoi anche definirti delle variabili che verranno create e inizializzate prima di leggere il programma: è l'opzione -v, come sicuramente hai intuito. L'ultima opzione, -F, serve ad indicare il separatore dei campi. Se lo si omette, i campi sono divisi da un numero qualunque di spazi e tab, come conviene a un default che si rispetti: altrimenti si usa l'espressione regolare indicata. Sì: espressione regolare, non semplice carattere. Devo ammettere che non ho ben capito l'utilità della cosa: però posso garantire che funziona. Ho provato a scrivere in un file 1::2:::3: :5 e ho controllato che l'espressione ":\*" (con il backslash perché altrimenti la shell si lamenta) separa correttamente i campi, che sono "1", "2", "3", " " e "5".

Se hai deciso di usare gawk, hai poi a disposizione tutta una serie più o meno utile di opzioni, tutte della forma -W opzione (o anche --opzione se invece che POSIX preferisci lo stile GNU). Alcune sono standard, come -W version che stampa semplicemente il numero di versione; -W copyright (o copyleft che dir si voglia) che stampa la versione ridotta del copyright GNU; -W help (oppure usage) che dà l'elemco dei comandi, e -W posix per essere pienamente compatibili con lo standard POSIX. Altre sono specifiche: -W traditional elimina tutte le estensioni GNU, girando in "compatibility mode"; -W re-interval permette di usare le cosiddette interval expression come espressioni regolari (sono quelle che dicono "da m a n occorrenze di un'espressione", POSIX le ha volute ma in genere gli awk non ce l'hanno. Te ne parlerò meglio la prossima volta); -W lint avvisa di costrutti non portabili; -W lint-old avvisa di costrutti non portabili verso il vecchio awk originale[7].

Resta ancora da dire una cosa: se non si gira in compatibility mode e si danno opzioni ignote, queste vengono infilate nella variabile di ambiente ARGV da dove possono accedute dal programma.

Pattern ed espressioni regolari

Se hai già visto come è fatto un programma awk, avrai notato delle buffe frasi BEGIN e END, e ti sarai magari chiesta se il linguaggio assomiglia un po' al Pascal[8]. La risposta è no. Semmai, le affinità sono più con il C (e ti credo...). Il bello è che puoi mettere prima END e poi BEGIN, e non cambia nulla.

Ma procediamo con ordine: i programmi awk sono formati da una serie di blocchi della forma pattern-azione e da funzioni opzionali:

   pattern { azione }
   function nome(lista_parametri) { azione }
Come avrai capito, BEGIN e END sono in realtà dei pattern speciali, e nulla più: ecco perché la loro posizione relativa è ininfluente.

Come viene eseguito un programma awk? per prima cosa, vengono letti ordinatamente tutti i file di programma, siano essi stati definiti con -f o ce ne sia uno solo all'interno della riga di comando. gawk si distingue come al solito: se viene definita la variabile d'ambiente AWKPATH, i file vengono cercati in tutte le directory indicate in essa. Letto il programma, si fanno tutte le assegnazioni indicate dalle opzioni -v; poi si eseguono le azioni nel blocco (o nei blocchi!) BEGIN, nell'ordine in cui vengono trovate. Solo a questo punto si cominciano a leggere i file di input; per ogni record si guarda quali pattern vengono attivati, e si eseguono le azioni corrispondenti. Infine, finito l'input, si cercano i blocchi END e si eseguono questi ultimi. Ricordati solo che se ci sono solo blocchi BEGIN, l'input non viene letto...

Nei blocchi, possono mancare o il pattern o l'azione. Nel secondo caso, l'azione implicita è {print}, cioè viene stampata la riga di input; nel primo caso, l'azione viene eseguita per ogni riga di input.

Altri costrutti sintattici: i commenti iniziano con il carattere #, e terminano alla fine della riga; righe vuote sono considerate commenti; uno statement termina alla fine di una riga, a meno che non finisca in ",", "{", "?", ":", "&&", "||" o alla peggio con un bel backslash; si possono mettere più statement in una riga separandoli con un puntoevirgola, sia all'interno di un'azione che per separare le coppie pattern/azioni. Il bello è che, almeno nel caso di un programma scritto nella riga di comando, si possono giustapporre più coppie pattern-azioni senza infilarci un ";" in mezzo: provare per credere.

I pattern possono essere di varie forme:

  • BEGIN
  • END
  • /espressione regolare/
  • espressione relazionale
  • pattern && pattern
  • pattern || pattern
  • ! pattern
  • pattern ? pattern : pattern
  • (pattern)
  • pattern1, pattern2
  • Dei pattern speciali BEGIN e END (che non si possono mischiare con gli altri, e che devono per forza avere un'azione!) ho già parlato sopra. Per quasi tutte le altre forme di pattern, il significato è quello usuale: le espressioni regolari sono quelle di egrep; le espressioni regolari sono le stesse che vedremo nelle azioni (maggiore, minore...); abbiamo AND, OR e NOT logici e l'operatore ?: come in C, e le parentesi per definire l'ordine di valutazione. L'unico pattern un po' strano è l'ultimo, il range pattern; anch'esso non si può combinare con gli altri, e serve per attivare tutti i record a partire da uno che contiene il primo pattern a uno che contiene il secondo pattern (compreso). Se ad esempio abbiamo un file con una serie di programmi, tutti compresi tra una riga -- inizio e una -- fine, e del testo esplicativo tra i vari programmi, una riga di awk tipo '/^-- inizio/,/^-- fine/' mi permette di eliminare tutti questi commenti[9].

    Già che ci sono, magari, potrebbe però essere utile rammentarti le principali espressioni regolari (evito quelle particolari GNU, mi paiono esagerate). Eccoti una tabellina da ritagliare:

    Espr.
    significato
    c
    il carattere c (tranne i metacaratteri!)
    \c
    il carattere c (anche se metacarattere)
    .
    un qualunque carattere, compreso il newline.
    ^
    l'inizio di una stringa.
    $
    la fine di una stringa.
    [abc...]
    lista di caratteri, uno qualunque di a,b,c,... c-f indica un qualunque carattere tra c ed f compresi.
    [^abc...]
    lista di caratteri, uno qualunque che non sia a,b,c,...
    r1|r2
    alternanza: una qualunque tra r1 ed r2.
    r1r2
    concatenazione: prima r1, poi r2.
    r+
    una o più occorrenze di r.
    r*
    zero o più occorrenze di r.
    r?
    zero o una occorrenza di r.
    (r)
    (semplice raggruppamento, per indicare la precedenza)
    r{n}
    r{n,}
    r{n,m}
    interval expression: queste sono standard POSIX ma non generalmente implementate negli awk. Nel primo caso, l'espressione corrisponde ad esattamente n occorrenze di r; nel secondo. ad almeno n occorrenze; nel terzo, tra n ed m occorrenze di r.

    Il buon POSIX ha anche definito alcune classi di caratteri: queste sono della forma [: nome :] e sono delle utili abbreviazioni. Ecco le classi che esistono, in rigoroso ordine alfabetico: [:alnum:] corrisponde ai caratteri alfanumerici, [:alpha:] agli alfabetici, [:blank:] a spazio o tab, [:cntrl:] ai caratteri di controllo, [:digit:] alle cifre, [:graph:] ai caratteri stampabili e visibili (lo spazio è stampabile, ma non visibile...), [:lower:] ai caratteri minuscoli, [:print:] a quelli stampabili (non i vari control, insomma), [:punct:] a quelli di punteggiatura, [:space:] ai generici caratteri di spaziatura (quindi anche il formfeed, tanto per dirne uno), [:upper:] ai caratteri maiuscoli e [:xdigit:] alle cifre esadecimali (0-9,A-F,a-f).

    Se ti stai chiedendo perché mai abbiano inventato [:alpha:] quando si faceva più in fretta a scrivere [A-Za-z], significa che non sei una brava italiana. Queste classi di caratteri conoscono infatti il locale: ergo, la è viene riconosciuta come carattere alfabetico, anche se viene rappresentata a 8 bit. Un gran vantaggio, no?

    Chi ben comincia...

    Bene, per questa prima puntata direi che basta. In pratica, come hai visto, puoi già usare awk per lanciare i programmini banali, quelli per cui non ti viene voglia di usare perl. Ti basta sapere che se fai operazioni numeriche come in C e stampi con print o printf viaggi tranquilla. Ad esempio, per calcolare la somma in byte dei file di una directory, compreso lo spazio occupato dalle directory che wc -c omette, puoi lanciare

       ls -l | awk 'END {print a}; {a+=$5}'
    
    dove mi sono divertito a iniziare dal fondo. Se invece ti capita di avere troppi processi sendmail che girano, e devi cancellarli tutti in un colpo, puoi usare
       kill -1 `ps ax | grep "sendmail" | awk '{print $1}'`
    
    Nella prossima puntata ti insegnerò a leggere al volo questi comandi, oltre a parlare di azioni, variabili, vettori, funzioni e operatori predefiniti.[10]

    Se intanto vuoi provare a divertirti da sola, ti lascio i riferimenti FTP delle tre versioni di awk: