Utilità Unix: awk (parte II)

Carissima,
lo so che non vuoi dirmelo, ma non ti sono affatto mancato in questi due mesi. L'unica persona che sembra realmente interessata ad avere l'articolo è l'ineffabile Luciano Giustini[1], ma temo che nemmeno lui in realtà legga quello che scrivo.

Beh, io ho riletto la puntata precedente, e ne approfitto per farti il riassunto. In pratica, ho detto

  1. che awk esiste e non è unico,
  2. che può valere la pena di usarlo ogni tanto,
  3. che si lancia o così:
    awk [ -F fs ] [ -v var=valore ] [--] 'programma' file ... o cosà:
    awk [ -F fs ] [ -v var=valore ] -f progfile [--] file ...
  4. che i programmi awk sono della forma
    pattern { azione }
    function nome(lista_parametri) { azione }
  5. che ci sono tanti bei pattern, e specialmente BEGIN, che significa "prima dell'inizio del file", ed END, che significa "dopo la fine del file".
Ora possiamo andare avanti tranquilli!

Variabili e vettori

Prima di parlare delle azioni, penso sia opportuno introdurre le variabili, visto che ce le troveremo in ogni momento. Tanto non è difficile usarle: se ci serve una variabile a un certo punto del nostro programma, basta usarla. Non dobbiamo neppure definire il suo tipo: essa può essere indifferentemente un numero floating point oppure una stringa.

La cosa più divertente è che le variabili in awk modificano persino la parsificazione dell'input. Non ti stupisce certo scoprire che per awk un file è un insieme di record, ciascuno dei quali composto da una serie di campi; e non ti stupirà troppo venire a sapere che la variabile FS specifica come dividere i campi. Generalmente è uno spazio (che corrisponde a una qualunque successione di spazi o tab); può essere un carattere qualunque; o ancora un'espressione regolare, di quelle della volta scorsa[2].

Ti stupirà un po' di più scoprire che anche i record possono venire parsificati a piacere. La variabile che conta in questo caso è RS (l'avresti mai detto?). Il default è un newline; può anche essere un carattere qualunque o (solo in gawk) un'espressione regolare, nel qual caso RTconterrà il testo attuale; o infine la stringa nulla. In questo caso, i record sono separati da righe vuote o con white space, e il carattere di newline fa da separatore di campi[3]. Ti risparmio, anche perché è amncora sperimentale, il trattamento dei campi a lunghezza fissa.

Ora che hai diviso i tuoi record in tanti campi, dovrai pur leggerli. Non perdo tempo a dirti che sono della forma $1, $2, ..., $NF. Visto che sei sveglia, hai anche intuito che la variabile NF indica il numero dei campi esistenti. Tutto il record viene messo nella variabile $0. Ma non è finita qui! Supponi di scrivere qualcosa tipo $(NF+3)=7. Così facendo, sei riuscita (a) a creare non solo la nuova variabile $(NF+3) ma anche tutte quelle tra $NF e $(NF+3), che sono inizializzate alla stringa nulla; (b) a modificare il valore di $NF (eh sì, adesso i campi sono di più!); (c) a ricalcolare il valore di $0. Niente male, vero? A proposito, mentre stiamo parlando dei campi ti ricordo che c'è una bella differenza tra NF e $NF. La prima, infatti, indica il numero dei campi presenti, mentre la seconda rappresenta il valore dell'ultimo campo. Insomma, tutte le volte che c'è un dollaro bisogna considerare quello che c'è a destra del dollaro come un numero e fare un secondo livello di indirezione[6] per trovare il valore. NOn pensare insomma che il dollaro indichi una variabile!

Oltre alle variabili, come dicevo, ci sono anche i vettori. Questi sono della forma a[n], dove n non è necessariamente un numero, ma una qualunque lista di espressioni expr1, ... exprm. La cosa più divertente è che, anche se a vedere l'espressione

   i="A"; j="B"; k="C"; v[i,j,k]="ciao, mondo!\n"
si direbbe che v è un vettore tridimensionale, in realtà è tutto un trucco. Per awk, infatti, l'indice di v è la stringa A\034B\034C. Il carattere \034, tra l'altro4, o qualunque altro sia stato definito dalla variabile SUBSEP, è quello che si digita con ctl-\ . Se sei stata attenta, avrai capito che in realtà questo vettore non c'entra un tubo con l'idea classica di un vettore: se scrivi a[1]=42; a[10]=666 tu non hai affatto creato un vettore di dieci elementi, ma hai semplicemente definito questi due valori5. È però possibile scorrere tutti i valori definiti del vettore, usando la parola chiave in: lo vedremo dopo. Dico invece subito che si può cancellare un elemento del vettore, usando il comando delete.

Ci sono alcune variabili speciali predefinite: tra di esse ricordo ARGC e ARGV, che hanno lo stesso significato che in C - e quindi la seconda è in realtà un vettore; CONVFMT che e` la stringa printf per convertire i numeri in stringhe - per default %.6g; ENVIRON, che è un vettore contenente l'ambiente; FILENAME, il cui nome dice tutto; FNR e NR, che indicano il numero del record corrente rispetto al file corrente nel primo caso e in assoluto nel secondo. gawk aggiunge tra l'altro l'utilissima IGNORECASE che, quando ha un valore diverso da zero, permette di fare confronti indipendenti da maiuscole e minuscole. A proposito di separatori di campo e record, ricordo che esiste anche la forma usata nell'output: OFS (inizializzato a uno spazio) e ORS (all'inizio un newline) rispettivamente, mentre OFMT specifica come un numero viene stampato per default.

Azioni

L'azione in awk è semplicemente una successione di statement. E gli statement sono molto simili a quelli C[7]. Quelli di controllo sono

e direi non v'è nulla da aggiungere, se non che in gawk è anche possibile cancellare un intero vettore, naturalmente scrivendo delete vett.

Passando agli statement di input, troviamo close(file) che serve a chiudere un input file, in modo che una volta riaperto la lettura inizi da capo; next che smette di processare il record corrente, ne prende uno nuovo e ricomincia a leggere le pattern (con gawk c'è anche nextfile) e il multiforme getline.

Senza parametri, getline setta $0 prendendo il record successivo dall'input e aggiorna NF, NR e FNR; nella forma getline var è la variabile var che si piglia il nuovo record di input, mentre vengono aggiornate NR e FNR; in entrambi i casi si può aggiungere < file, e in questo caso non si tocca il nostro input ma si prende il record da file; infine si può scrivere comando | getline var, e in questo caso var (oppure $0 se var non è presente) viene riempita con il risultato di comando[8]. Tra l'altro, se getline è arrivata all'EOF ritorna 0 e se dà errore ritorna -1, secondo le buone regole del galateo C.

Tra gli statement di output, abbiamo di nuovo close(file), che naturalmente chiude un file aperto in output; gawk aggiunge anche fflush([file]), che termina di salvare tutti gli dati non ancora scritti su disco per file o, senza parametro, sullo standard output. Tutti i sistemi decenti[9] hanno poi system(cmd), per richiamare un comando esterno. Se scrivo a=system("/bin/ls"), quindi, mi verrà stampato il contenuto della directory corrente, e la variabile a avrà il valore zero[10].

Per stampare ci sono due possibilità. printf funziona come quella del C, con la stringa di formattazione e la lista dei parametri; nulla di nuovo. Se però non ti importa di controllare strettamente l'output, puoi usare la semplice print. Senza parametri, essa stampa il record corrente, e lo termina con il contenuto della variabile ORS; con parametri, stampa il valore di questi parametri, separati dal contenuto della variabile OFS. Sia printf che print possono venire redirette con >> file (appende il contenuto), con > file (lo stesso, ma alla prima occorrenza tronca il file originario se esso esisteva), o con | comando (indovina che fa?)

Operatori

Anche gli operatori awk sono simili a quelli C. Eccoti una loro lista, in ordine decrescente di precedenza.
(...) Parentesi per raggruppamento, sia di operazioni (a+b)*c che nel riferimento $(NF-2).
$ Riferimento a un campo, come in $(NF-2).
++   -- Incremento e decremento. Possono essere prefissi o postfissi, a seconda se si vuole che la modifica del valore avvenga prima o dopo l'utilizzo.
^ Esponenziazione.
!   +   - Negazione logica, più e meno unari.
*   /   % Moltiplicazione, divisione, modulo.
+   - Addizione, sottrazione.
spazio Concatenazione di stringhe (cioè, se abbiamo a=343,b="sds" e definiamo c=a b, c varrà "343sds").
<   >   <=   >=   !=   == Gli operatori relazionali standard.
~ !~ Match, e match negato, di espressioni regolari. La man page ricorda che non si devono usare espressioni costanti, tipo /pippo/, sul lato sinistro, ma solo sul destro.
in Appartenenza a un vettore.
&& AND logico.
|| OR logico.
?: L'operatore C ternario "espressione condizionale": se si ha expr1 ? expr2 : expr3, allora il valore finale e` quello di expr2 se expr1 è vera, quello di expr3 altrimenti. Si garantisce che viene valutata solo una delle due espressioni dopo il punto interrogativo.
=   +=   -=   *=   /=   %=   ^= Gli assegnamenti, semplici o con un'operazione.

Funzioni

Le funzioni sono il sale di un linguaggio di programmazione; anche awk le possiede, stai tranquilla. Il guaio è che ci hanno pensato dopo, a introdurle, e il risultato si vede. La sintassi di una funzione è la seguente:

  function nome(parametri) { statements }
dove si può scrivere func al posto di function, anche se ciò è deprecato. Fin qua nulla di male, dirai: né ti preoccupi del fatto che nome non possa essere usato come variabile. Magari storcerai il naso al sapere che devi per forza scrivere la parentesi aperta attaccata al nome, ma lo capisci, soprattutto se sei abituata alle idiosincrasie di sh. È poi naturale che i parametri scalari siano passati per value, mentre i vettori lo sono per reference.

Ma adesso tienti forte: non è possibile definire variabili locali all'interno di una funzione. Ma prima che tu decida di buttare via awk come giocattolo inutile, mi affretto ad aggiungere che è possibile avere variabili locali all'interno di una funzione. No, non mi sono bevuto il cervello, ma se lo sono bevuti i signori A, W e K. Se tu chiami una funzione con n parametri, ma la definisci con m > n parametri, tutti quelli in più sono per awk delle variabili locali, inizializzate a 0 o alla stringa nulla a seconda di quello per cui serviranno. Roba da non credere.

Ultima notiziuola a proposito delle funzioni definite dall'utente: si possono usare prima di trovare la definizione, possono stare in file separati, e possono chiamarsi l'un l'altra ed essere ricorsive. In bocca al lupo.

Se invece ti vuoi limitare ad usare le funzioni predefinite, ne hai un simpatico numero. Tra l'altro, in questo caso non sei costretto ad aprire la parentesi subito dopo il nome, perché awk sa che in fin dei conti quello che hai scritto deve essere una funzione. Stupido, ma non stupidissimo.

Cominciando con le funzioni numeriche, abbiamo quelle logaritmico/trigonometriche: log(expr), exp(expr), sin(expr), cos(expr), atan2(y/x); quest'ultima calcola l'arcotangente di y/x in radianti. La radice quadrata è sqrt(expr), il troncamento ad intero int(expr). Abbiamo poi la generazione di numeri casuali, con rand() che ritorna un numero tra 0 e 1, e srand([seme]) che serve a inizializzare il generatore di numeri casuali con seme, o con l'ora se non viene usato il parametro, e ritorna il vecchio valore del generatore.

Le funzioni su stringhe fanno naturalmente la parte del leone: ricordo solo le più importanti.

Chi ha scritto gawk ha poi pensato che in fin dei conti l'uso principale è quello di massaggiare i file di log, e quindi ha introdotto due funzioni "temporali". Esse sono systime, che ritorna il numero di secondi dalla epoca[11], e strftime( [format [, timestamp ]] ) che formatta secondo la variabile format la data, espressa nel formato di systime e indicata in timestamp oppure quella corrente.

Un'ultima parola

Prima di lasciarti, aggiungo velocemente una nota su alcuni nomi di pseudofile, che hanno un significato speciale per awk, e possono venire usati da getline, print e printf. I principali pseudofile sono /dev/stdin (o più prosaicamente "-"), /dev/stdout e /dev/stderr che rappresentano rispettivamente lo standard input, lo standard output e lo standard error come ereditati dal processo padre[13]. gawk è come sempre generoso, e aggiunge anche /dev/fd/num per accedere direttamente al file con descrittore num. Inoltre definisce altri pseudofile:

/dev/pid
ritorna l'identificativo del processo corrente;
/dev/pid
ritorna l'identificativo del processo corrente;
/dev/pgrpid
ritorna l'identificativo di gruppo del processo;
/dev/user
ritorna un record con tutti gli identificativi utente: nell'ordine quelli di getuid, geteuid (l'effective user id, quello che si ottiene lanciando un programma col bit setUID settato), getgid, getegid ed eventualmente i gruppi ausiliari di getgroups.
Corrono voci che questi ultimi verranno eliminati nella versione 3.1 di gawk, ma non ne sarei così sicuro.

L'uso di questi pseudofile è molto comodo per i messaggi di errore: vuoi mettere la facilità di scrivere

    print "Errore nei dati" > "/dev/stderr"
piuttosto che
    print "Errore nei dati" | "cat 1>&2"
e soprattutto la comprensibilità del testo?

Bene, con questo ho proprio terminato. Spero ti sia divertita e che ti venga voglia di aspettare il prossimo programma che tratterò, con ogni probabilità sed[12].

Esempi

Ecco la sezione più interessante, come al solito. Non so te, ma io mi sono sempre sentito deluso, quando dopo una sfilza di pagine incomprensibili mi trovo degli esempietti che un ragazzino di otto anni avrebbe scritto al volo. Cercherò di fare di meglio, anche se non credo sia così difficile. Tra l'altro, questo è un dei rari casi in cui nawk è più generoso di gawk: la sua man page ha infatti degli esempi migliori.

Per prima cosa, vuoi sapere come intercalare tra loro, una riga ciascuno, i file f1.txt e f2.txt? Banale: basta fare una getline del secondo ogni volta che si legge una riga del primo. In soldoni,

   awk '{print ; getline < "f2.txt" ; print }' f1.txt
Nascondi tutto in un alias, e sei a posto!

Se invece vuoi sapere quanto è lunga la riga più lunga del file f.txt, il comando da eseguire è

   awk 'BEGIN {a=0}; {if (length() > a) a = length()}; END {print a}' f.txt

Supponiamo ancora di avere un file f.txt con tante righe che vogliamo compattare, andando a capo solo quando c'è una riga che contiene un solo punto ".". Il programma awk corrispondente è il seguente:

           BEGIN {ORS=" "}
	   {
	      if ($0==".") 
	         printf "\n"
	      else 
	         print
	   }
	   END {printf "\n"}
mentre se vogliamo semplicemente radunare a cinque a cinque le righe occorre digitare
           BEGIN {ORS=" " ; a=0}
	   {
	      print
	      if (!(++a%5))
	         printf "\n"
	   }
	   END {printf "\n"}

Note & Chiose

[1]
Stavo pensando se fosse meglio scrivere il suo nome TUTTO IN MAIUSCOLO, cambiare font, o magari fare un gif animato con effetti speciali; poi mi sono ricordato che il Nostro Direttore è fondamentalmente un tipo modesto, e mi sono limitato alla scritta standard.
[2]
Sì, devi per forza cercarti la puntata precedente.
[3]
Sto usando un po' troppo inglese, hai ragione. Ma se riesco ancora ad accettare mentalmente "acapo", il termine "spazio bianco" mi dà convulsioni.
[4]
E che effettivamente ha come nome ufficiale FS, un vero e proprio field separator!
[5]
La man page afferma che bisogna dire "i vettori sono associativi". Bene, l'ho detto.
[6]
Insomma, calcolare il risultato di un risultato. Ma detto così sembra più interessante.
[7]
Non per niente awk comprende la K di Kernighan.
[8]
A dire il vero non ho capito bene cosa succeda. Ho fatto una prova, però: se ho quattro file nella mia directory e scrivo awk '{ echo "ls" | getline ; print}' f.txt, l'output consiste delle quattro righe dell'ls, e poi dei record dal quinto in poi di f.txt. Chi ci capisce è bravo.
[9]
Magari anche win9x? chi lo sa...
[10]
Ammesso che tu non abbia cancellato in precedenza la directory con un altra system, nel qual caso ti verrà stampato /bin/ls: ../dsf: No such file or directory, mentre a avrà valore 1. Sempre meglio essere precisi.
[11]
Ah, lo sai che abbiamo appena compiuto i 900 milioni di secondi dall'epoca?
[12]
Il che significa che se ti sei divertito a chiudere e aprire un file, continui però ad avere accesso alle definizioni iniziali.
[13]
Anche se mi è stato fatto notare che per prima cosa dovrei parlare di ed, l'editor dei Veri Programmatori. Io l'ho usato, ed è simpatico, ma non penso ti interessi.

.mau.

Copyright © 1998 Maurizio Codogno e BETA, sotto la Licenza Pubblica BETA.