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
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.
L'azione in awk è semplicemente una successione di statement. E gli statement sono molto simili a quelli C[7]. Quelli di controllo sono
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?)
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. |
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.
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:
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].
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.txtNascondi 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"}