Disclaimer :
La versione originale di questo articolo è stata pubblicata da IBM
developerWorks ed è di proprietà di Westtech Information Services. Questo
documento è una versione aggiornata dell'articolo originale, e contiene
numerosi miglioramenti apportati dal Gentoo Linux Documentation team.
Questo documento non è mantenuto attivamente.
|
Awk per esempi, Parte 2
1.
Record, cicli, e array
Record multiriga
Awk è uno strumento efficiente per leggere ed elaborare dati strutturati, come
il file di sistema /etc/passwd. /etc/passwd è il
database degli utenti di un sistema UNIX, ed è un file separato da due punti (:)
che contiene un sacco di informazioni importanti, fra cui tutti gli account
utente e gli user-ID. Nel mio articolo
precedente, vi ho mostrato come fare il parsing di questo file
(dall'inglese to parse, analizzare). Tutto ciò che abbiamo dovuto fare è
modificare il valore della variabile FS (Field Separator, separatore di
campo) al valore ":".
Settando correttamente la variabile FS, awk può essere configurato per
effettuare il parsing praticamente di ogni tipo di dato strutturato, a patto che
ci sia un record per linea. Tuttavia, modificare semplicemente il valore di FS
non ci servirà a niente nel caso in cui un record occupi più linee. In queste
situazioni, abbiamo bisogno anche di modificare la variabile RS (Record
Separator, separatore di record). La variabile RS informa awk della
terminazione di un record e dell'inizio del record successivo.
Per esempio, diamo un'occhiata a come risolveremmo il problema di gestire una
lista di indirizzi di partecipanti al Programma Nazionale di Protezione dei
Testimoni.
Codice 1.1: Esempio di voci nella lista di indirizzi del Programma Nazionale di Protezione dei Testimoni |
Giacomo il Faina
Viale dei Giardini 12
00100 Roma
Tonino
Largo Sconosciuto
20027 Spiate (MI)
|
Idealmente, vorremmo che awk riconoscesse ogni gruppo di tre righe come un
singolo record, piuttosto che come come tre record separati. Il nostro codice
diventerebbe molto più semplice se awk riconoscesse la prima riga dell'indirizzo
come il primo campo ($1), l'indirizzo come secondo campo ($2), e il codice
postale e la città come campo $3. Il seguente codice fa proprio quello che
vogliamo:
Codice 1.2: Creare un campo dall'indirizzo |
BEGIN {
FS="\n"
RS=""
}
|
Sopra settare FS a "\n", informa awk che ogni campo appare su una sua riga.
Settando RS a "", diciamo anche ad awk che ogni record è separato dal successivo
da una linea vuota. Una volta che awk sa come è formattato l'input, può fare
tutto il lavoro di parsing per noi, e la restante parte dello script è semplice.
Vediamo un esempio completo che fa il parsing dell'elenco di indirizzi e stampa
ogni record su un'unica linea, separando i campi con una virgola.
Codice 1.3: Script completo |
BEGIN {
FS="\n"
RS=""
}
{ print $1 ", " $2 ", " $3 }
|
Se questo script viene salvato come indirizzi.awk, e gli indirizzi
sono contenuti in un file chiamato indirizzi.txt, potete eseguire
questo script scrivendo awk -f indirizzi.awk indirizzi.txt. Questo codice
produce il seguente output:
Codice 1.4: L'output dello script |
Giacomo il Faina, Viale dei Giardini 12 , 00100 Roma
Tonino, Largo Sconosciuto, 20027 Spiate (MI)
|
Le variabili OFS e ORS
Nell'istruzione print di indirizzi.awk, potete notare che awk concatena (unisce)
stringhe che sono poste una di fianco all'altra sulla stessa riga. Abbiamo usato
questo metodo per inserire una virgola e uno spazio (", ") tra i tre campi che
appaiono su una riga. Sebbene questo metodo funzioni, è un po' brutto da vedere.
Invece di inserire la stringa costante ", " tra i nostri campi, possiamo
istruire awk perchè lo faccia per noi, settando la variabile speciale OFS. Date
un'occhiata a questo pezzo di codice:
Codice 1.5: Codice di esempio |
print "Hey", "ciao", "Jim!"
|
Le virgole su questa linea di codice non sono stringhe costanti. Invece dicono
ad awk che "Hey" "ciao" e "Jim!" sono campi (di uscita) distinti e che il
contenuto della variabile OFS deve essere stampato tra ciascuna stringa. Di
default, awk produce il seguente output:
Codice 1.6: Output prodotto da awk |
Hey ciao Jim!
|
Questo ci mostra che, di default, OFS è settata a " ", un singolo carattere di
spaziatura. Però possiamo facilmente ridefinire OFS in maniera che awk
inserisca il nostro separatore di campo preferito. Di seguito abbiamo una
versione rivista del nostro programma originale indirizzi.awk che
usa OFS per mandare in uscita come separatore la stringa ", ":
Codice 1.7: Ridefinire OFS |
BEGIN {
FS="\n"
RS=""
OFS=", "
}
{ print $1, $2, $3 }
|
Awk ha anche una variabile speciale chiamata ORS, (output record
separator, separatore dei record in uscita). Settando ORS, che di default
vale il carattere "\n" che crea una nuova linea, possiamo controllare ciò che
viene stampato automaticamente alla fine di un'istruzione di print. Il valore
di default di ORS fà in modo che awk mandi in uscita ogni istruzione di print su
una linea diversa. Se volessimo un output con interlinea doppia, dovremmo
settare ORS al valore "\n\n". O, se volessimo che i record fossero separati da
un singolo carattere di spaziatura (senza andare a capo), dovremmo settare ORS
al valore " ".
Dal multiriga alle tabulazioni
Ipotizziamo di aver creato uno script che ha convertito il nostro elenco di
indirizzi in un formato con un record per linea, in cui i campi sono delimitati
da tabulazioni. Tutto questo perchè, per esempio, abbiamo bisogno di importare
questi dati in un foglio elettronico. Dopo aver usato una versione leggermente
modificata di indirizzi.awk, sarà diventato chiaro che il nostro
programma funziona solamente per indirizzi su tre righe. Se awk incontrasse il
seguente indirizzo, la quarta linea sarebbe ignorata e non stampata:
Codice 1.8: Esempio di voce |
Cugino Vincenzo
Autorimessa Vincenzo
Via del Biscione, 657
40026 Imola (BO)
|
Per gestire situazioni come questa, sarebbe meglio che il nostro codice tenesse
in conto il numero di righe per indirizzo, stampando ciascuna riga in ordine.
Adesso, il codice stampa solamente i primi tre campi dell'indirizzo. Ecco un po'
di codice che fa quello che ci serve:
Codice 1.9: Codice migliorato |
BEGIN {
FS="\n"
RS=""
ORS=""
}
{
x=1
while ( x<NF ) {
print $x "\t"
x++
}
print $NF "\n"
}
|
Prima settiamo il separatore di campo a "\n" e il separatore di record a "",
cosicchè awk analizzi correttamente gli indirizzi su più righe, come prima. Poi
settiamo il separatore di record in uscita ORS a "", in modo da non mandare in
uscita una nuova riga alla fine di un'istruzione di print. Questo significa che,
se vogliamo che il testo sia su una nuova linea, dovremmo scrivere
esplicitamente print "\n".
Nel blocco principale, creiamo una variabile di nome x che contiene il numero
del campo che stiamo attualmente processando. All'inizio è settata a 1. Poi
usiamo un ciclo while (un costrutto iterativo identico a quello del C) per
scorrere tutti i campi eccetto l'ultimo, stampando il campo e un carattere di
tabulazione. Alla fine, stampiamo l'ultimo campo e andiamo a capo; ancora una
volta, poichè ORS vale "", print non andrà a capo da solo. L'uscita del
programma somiglierà a questa, proprio come volevamo.
Codice 1.10: L'uscita voluta. Non è carina a vedersi ma delimitata da tabulazione per una facile importazione in un foglio elettronico |
Giacomo il Faina Viale dei Giardini 12 00100 Roma
Tonino Largo Sconosciuto 20027 Spiate (MI)
Cugino Vincenzo Autorimessa Vincenzo Via del Biscione, 657 40026 Imola (BO)
|
Costrutti iterativi
Abbiamo già visto il costrutto per il ciclo while, identico al suo analogo in C.
Awk ha anche un ciclo "do...while" che valuta la condizione alla fine del blocco
di codice iterativo, piuttosto che all'inizio come uno ciclo while standard. E'
simile ai cicli "repeat...until" che si possono trovare in altri linguaggi. Ecco
un esempio.
Codice 1.11: Esempio di do...while |
{
count=1
do {
print "In ogni caso vengo stampato almeno una volta"
} while ( count != 1 )
}
|
Siccome la condizione viene valutata dopo il blocco di codice, un ciclo
"do...while", al contrario di un normale ciclo while, viene eseguito almeno una
volta. In altre parole m un ciclo while standard non verrà mai eseguito se la
condizione è falsa la prima volta che il ciclo viene incontrato.
Cicli for
Awk dà la possibilità di creare cicli for che, come i cicli while, sono identici
ai loro analoghi in C:
Codice 1.12: Ciclo di esempio |
for ( assegnamento iniziale; confronto; incremento ) {
blocco di codice
}
|
Ecco un esempio veloce:
Codice 1.13: Esempio veloce |
for ( x = 1; x <= 4; x++ ) {
print "iterazione",x
}
|
Questo pezzetto di codice stamperà:
Codice 1.14: Output del pezzetto di codice sopra |
iterazione 1
iterazione 2
iterazione 3
iterazione 4
|
Break e continue
Ancora una volta, proprio come il C, awk ci fornisce istruzioni di break e
continue, Queste istruzioni ci permettono di avere un miglior controllo sui vari
costrutti iterativi. Ecco un pezzetto di codice che ha un bisogno sdisperato di
un'istruzione di break.
Codice 1.15: Pezzetto di codice che ha bisogno di un break |
while (1) {
print "Sempre e per sempre..."
}
|
Siccome 1 è sempre una condizione vera, questo ciclo eseguirà all'infinito. Ecco
un ciclo che farà solo 10 iterazioni:
Codice 1.16: Ciclo che viene eseguito solo 10 volte |
x=1
while(1) {
print "iterazione",x
if ( x == 10 ) {
break
}
x++
}
|
Qui, l'istruzione di break è usata per rompere il ciclo più interno. "break"
farà terminare il ciclo immediatamente e farà continuare l'esecuzione
dall'istruzione successiva al codice del ciclo.
l'istruzione continue fa da complemento al break e funziona in questo modo:
Codice 1.17: L'istruzione continue è duale al break |
x=1
while (1) {
if ( x == 4 ) {
x++
continue
}
print "iterazione",x
if ( x > 20 ) {
break
}
x++
}
|
Questo codice stamperà "iterazione 1", "iterazione 2", ... fino a "iterazione
21", eccetto "iterazione 4". Infatti, quando x vale 4, esso viene incrementato e
si esegue l'istruzione continue che fa iniziare una nuova iterazione senza
eseguire il resto del blocco while. L'istruzione continue funziona, come la
break, con ogni tipo di costrutto iterativo di awk. Quando è usata nel corpo di
un ciclo for, continue fa sì che la variabile di controllo del loop sia
automaticamente incrementata. Ecco un ciclo for equivalente:
Codice 1.18: Ciclo for equivalente |
for ( x=1; x<=21; x++ ) {
if ( x == 4 ) {
continue
}
print "iterazione",x
}
|
Non è stato necessario incrementare x prima di chiamare continue come avevamo
fatto con il ciclo while, poichè il for incrementa automaticamente x.
Gli array
Sarete felici di sapere che awk anche supporto per gli array. Però, in awk, gli
indici degli array partono da 1 e non da zero:
Codice 1.19: Esempio di array in awk |
myarray[1]="jim"
myarray[2]=456
|
Quando awk incontra il primo assegnamento, viene creato l'array myarray e
l'elemento myarray[1] viene settato alla stringa "jim". Dopo che il secondo
assegnamento viene valutato, l'array ha due elementi.
Una volta definito un array, ci fornisce un utile meccanismo per scorrerne gli
elementi, come potete vedere nel codice seguente:
Codice 1.20: Scorrere tutti gli elementi di un array |
for ( x in myarray ) {
print myarray[x]
}
|
Questo codice stamperà ogni elemento dell'array myarray. In questo speciale tipo
di for, usando la parola chiave "in", awk assegnerà ogni indice esistente
dell'array myarray a x (la variabile di controllo del ciclo), eseguendo il corpo
del for dopo ogni assegnamento. Sebbene questa sia una caratteristica molto
utile di awk, soffre di uno svantaggio: quando awk scorre l'array, non segue un
ordine particolare. Questo significa che non c'è modo di sapere se l'output del
codice visto sopra sarà:
Codice 1.21: Output del codice visto sopra |
jim
456
|
o
Codice 1.22: Altro possibile output del codice visto sopra |
456
jim
|
Per parafrasare liberamente Forrest Gump, scorrere il contenuto di un array è
come una scatola di cioccolatini: non sai mai cosa puoi trovare. Questo ha in
qualche modo a che fare con la "stringosità" degli array awk, che adesso
vedremo.
La stringosità degli indici degli array
Nel mio articolo precedente, vi
ho mostrato che awk in realtà memorizza i valori numerici sotto forma di
stringhe. Mentre awk realizza le operazioni di conversione necessarie a far
funzionare il tutto, questo lascia la possibilità di scrivere del codice un po'
strano:
Codice 1.23: Codice strano |
a="1"
b="2"
c=a+b+3
|
Dopo l'esecuzione di questo codice, c vale 6. Poichè awk è "stringoso",
sommare le due stringhe "1" e "2" è funzionalmente uguale a sommare i numeri 1 e
2. In entrambi i casi, awk realizzerà correttamente l'operazione. La natura
"stringosa" di awk è discretamente intrigante; vi potete chiedere che cosa
succede se usate delle stringhe come indici di array. Per esempio, prendete il
seguente codice:
Codice 1.24: Codice di esempio |
myarr["1"]="Sig. Rossi"
print myarr["1"]
|
Come vi potete aspettare, questo codice stamperà "Sig. Rossi". Ma cosa succede
se togliamo le virgolette dal secondo indice "1"?
Codice 1.25: Senza le virgolette |
myarr["1"]="Sig. Rossi"
print myarr[1]
|
Ipotizzare il risultato di questo pezzo di codice è più difficile. Awk
considera myarr["1"] e myarr[1] come due elementi separati dell'array oppure no?
La risposta è che si riferiscono allo stesso elemento e stamperanno "Sig. Rossi"
come nel primo pezzo di codice. Sebbene possa sembrare strano, dietro le quinte
awk ha sempre usato indici stringa per i suoi array.
Dopo aver imparato ciò, alcuni di voi potrebbero essere tentati di eseguire del
codice strambo come questo:
Codice 1.26: Codice strambo |
myarr["nome"]="Sig. Rossi"
print myarr["nome"]
|
non solo questo codice non genera errori, ma è funzionalmente identico al nostro
esempio precedente, e stamperà "Sig. Rossi" proprio come prima! Come potete
vedere awk non ci costringe ad usare come indici solo variabili intere; possiamo
usare indici stringa se vogliamo, senza creare alcun problema. Ogni volta che
usiamo indici non numerici come myarr["nome"], stiamo usando array
associativi. Tecnicamente, dietro le quinte awk non fa niente di diverso
quando usiamo un indice stringa (poichè anche se usiamo un indice intero, awk lo
tratta sempre come una stringa). Ad ogni modo potete sempre chiamarli array
associativi; sembrerà fico e farà impressione sul vostro capo. Questa
cosa della stringosità degli indici sarà il nostro piccolo segreto. ;)
Strumenti per gli array
Parlando di array, awk ci dà un sacco di flessibilità. Possiamo usare indici
stringa e non siamo obbligati a rispettare una sequenza numerica continua per
gli indici (per esempio, possiamo definire myarr[1] e myarr[1000], ma lasciare
gli altri elementi non definiti). Anche se questo può essere molto utile, in
qualche circostanza può creare confusione. Fortunatamente, awk ci offre un paio
di utili funzionalità per rendere gli array più maneggevoli.
Primo, possiamo cancellare gli elementi di un array. Se volete cancellare
l'elemento 1 del vostro array fooarray, scrivete:
Codice 1.27: Cancellare elementi di un array |
delete fooarray[1]
|
E, se volete vedere se un particolare elemento di un array esiste, potete usare
lo speciale operatore booleano "in" come nell'esempio seguente:
Codice 1.28: Controllare se esiste un particolare elemento di un array |
if ( 1 in fooarray ) {
print "Sì! C'è."
} else {
print "No! Non lo trovo."
}
|
La prossima volta
Abbiamo scoperto un bel po' di cose in questo articolo. La prossima
volta affinerò la vostra conoscenza di awk mostrandovi come usare le funzioni
matematiche e sulle stringhe e insegnandovi a creare delle vostre funzioni. Vi
mostrerò anche la creazione di un programma per la contabilità. Per intanto vi
incoraggio a scrivere qualche programma da soli e a controllare i seguenti
riferimenti:
2.
Risorse
|