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 3
1.
Funzioni per le stringhe e ... estratto conto ?
Formattare l'output
Anche se l'istruzione print di awk nella maggior parte dei casi risolve i nostri
problemi, qualche volta c'è bisogno di qualcosa di più. In queste situazioni,
awk ci offre due vecchi amici di nome printf() e sprintf(). Sì, queste funzioni,
come molte altre parti di awk, sono identiche alle loro analoghe in C. printf()
stamperà una stringa formattata sullo standard output, mentre sprintf()
restituisce una stringa formattata che può così essere assegnata ad una
variabile. Se printf() e sprintf() non vi sono familiari, un testo base di C vi
metterà velocemente in pari su queste due funzioni di stampa essenziali. Potete
vedere la pagina di manuale di printf() scrivendo "man 3 printf" sul vostro
sistema Linux.
Ecco qualche esempio di codice awk che usa sprintf() e printf(). Come potete
vedere, ogni cosa è praticamente identica al C.
Codice 1.1: Esempi di codice awk con sprintf() e printf() |
x=1
b="foo"
printf("%s ha preso %d al compito scorso.\n","Gianni",9)
myout=("%s-%d",b,x)
print myout
|
Questo codice stamperà:
Codice 1.2: Output del codice sopra |
Gianni ha preso 9 al compito scorso.
foo-1
|
Funzioni per le stringhe
Awk ha una pletora di funzioni per le stringhe, e questo è una buona cosa. In
awk,avete veramente bisogno di funzioni che lavorano sulle stringhe, poiché non
potete trattare le stringhe come array di caratteri come fareste usando altri
linguaggi come C, C++ e Python. Per esempio, eseguendo questo codice:
Codice 1.3: Codice di esempio |
mystring="Cosa fai oggi?"
print mystring[3]
|
riceverete un errore di questo tipo:
Codice 1.4: Errore del codice di esempio |
awk: string.gawk:59: fatal: attempt to use scalar as array
|
Oh, bene. Anche se non sono utili come i tipi sequenza di Python, le funzioni
sulle stringhe di awk funzionano bene. Diamoci un'occhiata:
Prima di tutto, abbiamo la funzione fondamentale length(), che restituisce la
lunghezza di una stringa. Ecco come usarla:
Codice 1.5: Esempio per la funzione length() |
print length(mystring)
|
Questo codice stamperà il valore:
Codice 1.6: Valore stampato |
24
|
Ok, andiamo avanti. La prossima funzione è chiamata index, e restituisce la
posizione in cui occorre una sottostringa all'interno di un'altra stringa,
oppure 0 se la sottostringa non viene trovata. Usando mystring, possiamo
chiamarla così:
Codice 1.7: Esempio sulla funzione index() |
print index(mystring,"you")
|
Awk stampa:
Codice 1.8: Output della funzione |
6
|
Spostiamoci verso due funzioni più facili: tolower() e toupper(). Come potete
immaginare, queste funzioni restituiscono la stringa con i caratteri convertiti
rispettivamente in minuscolo e in maiuscolo. Notate che tolower() e toupper()
restituiscono una nuova stringa senza modificare la stringa originale. Questo
codice:
Codice 1.9: Convertire stringhe in minuscolo e maiuscolo |
print tolower(mystring)
print toupper(mystring)
print mystring
|
....produrrà come output:
Codice 1.10: Output |
cosa fai oggi?
COSA FAI OGGI?
Cosa fai oggi?
|
Fin qui tutto bene, ma esattamente come possiamo selezionare una sottostringa o
anche un singolo carattere all'interno di una stringa? È qui che substr() corre
in nostro aiuto. Ecco come invocarla:
Codice 1.11: Esempio funzione substr() |
mysub=substr(mystring,startpos,maxlen)
|
mystring dovrebbe essere una variabile stringa o una costante stringa da quale
vorreste estrarre una sottostringa. startpos dovrebbe valere la posizione del
carattere di partenza, e maxlen dovrebbe contenere la lunghezza massima della
stringa da estrarre. Notate che ho specificato lunghezza massima: se
length(mystring) è minore di startpos+maxlen, il risultato verrà troncato. Ecco
un esempio:
Codice 1.12: Un altro esempio |
print substr(mystring,6,3)
|
Awk stamperà:
Codice 1.13: Cosa stampa awk |
fai
|
Se programmate regolarmente in un linguaggio che usa gli indici degli array per
accedere parti di una stringa (e chi non lo fa?), annotatevi mentalmente che
substr() è il vostro analogo in awk. Avrete bisogno di usare substr() singoli
caratteri e sottostringhe. Poiché awk è un linguaggio basato sulle stringhe,
la userete spesso.
Adesso spostiamoci verso qualche funzione più polposa, la prima delle quali si
chiama match(). match() somiglia abbastanza a index(), eccetto per il fatto
che, invece di cercare una sottostringa, cerca una espressione regolare. La
funzione match restituisce la posizione iniziale del match (corrispondenza), o
zero se non c'è alcun match. Oltre a questo, match() cambia il valore di due
variabili chiamate RSTART e RLENGTH. RSTART contiene il valore di ritorno
(posizione del primo match) e RLENGTH specifica la sua lunghezza in caratteri (o
-1 se nessun match è stato trovato). Usando RSTART, RLENGTH, substr() e un
piccolo ciclo potete facilmente scorrere i vari match che compaiono nella vostra
stringa. Ecco un esempio di chiamata a match():
Codice 1.14: Esempio di chiamata a match() |
print match(mystring,/fai/), RSTART, RLENGTH
|
Awk stamperà:
Codice 1.15: Output funzione precedente |
6 6 3
|
Sostituzione di stringhe
Ora vedremo un paio di funzioni per la sostituzione di stringhe, sub() e gsub().
Questi personaggi differiscono leggermente dalle funzioni che abbiamo visto
finora nel fatto che in realtà modificano la stringa originale. Ecco un modello
che ci mostra come chiamare sub():
Codice 1.16: sub() function template |
sub(regexpr,replstring,mystring)
|
Chiamando sub(), essa troverà la prima sequenza di caratteri in mystring che
corrisponde all'espressione regolare regexp, e sostituirà tale sequenza con la
stringa replstring. sub() e gsub() hanno gli stessi argomenti; l'unica cosa in
cui sono diverse è che sub() sostituirà la prima corrispondenza di regexp (se
esiste), e gsub() invece effettuerà una sostituzione globale, cambiando tutte le
corrispondenze di regexp nella stringa. Ecco un esempio di chiamata a sub() e
gsub():
Codice 1.17: Sample both sub() and gsub() function call |
sub(/o/,"O",mystring)
print mystring
mystring="How are you doing today?"
gsub(/o/,"O",mystring)
print mystring
|
Abbiamo dovuto riportare mystring al valore originale poiché la prima chimata a
sub() ha direttamente modificato la stringa. Quando eseguito, questo codice
farà produrre ad awk il seguente output:
Codice 1.18: awk output |
COsa fai oggi?
COsa fai Oggi?
|
Sicuramente è possibile usare espressioni regolari più complesse. Lascerò a voi
il compito di provare qualche espressione regolare complicata.
Terminiamo la trattazione delle funzioni che operano sulle stringhe
presentandovi una funzione chiamata split(). Il lavoro svolto da split()
consiste nel "fare a pezzetti" una stringa e sistemare questi pezzetti in un
array con indici interi. Ecco un esempio di chiamata a split():
Codice 1.19: Esempio di chiamata a split() |
numelements=split("Gen,Feb,Mar,Apr,Mag,Giu,Lug,Ago,Set,Ott,Nov,Dic",mymonths,",")
|
Chiamando split(), il primo argomento contiene la stringa costante o la
variabile stringa da spezzettare. Come secondo argomento dovreste specificare il
nome dell'array in cui split() sistemerà i vari pezzi. Come terzo parametro,
specificate il separatore che sarà usato per spezzare le stringhe, Quando
split() termina, restituirà il numero di elementi in cui è stata spezzata la
stringa. split() assegna a ciascuno di essi una posizione nell'array, partendo
da uno, così il codice seguente:
Codice 1.20: Codice di esempio |
print mymonths[1],mymonths[numelements]
|
...stamperà:
Codice 1.21: Output dell'esempio |
Gen Dic
|
Forme speciali delle funzioni per le stringhe
Una nota veloce: chiamando length(), sub(), o gsub(), potete omettere l'ultimo
argomento e awk applicherà la funzione a $0 (tutta la riga corrente). Per
stampare la lunghezza di ciascuna riga del file, usate questo script di awk:
Codice 1.22: Codice che stampa la lunghezza di ogni riga in un file |
{
print length()
}
|
Finanza creativa
Qualche settimana fa ho deciso di scrivere il mio programma per fare l'estratto
conto in awk. Ho deciso che mi piacerebbe avere un semplice file di testo
delimitato da tabulazioni, nel quale poter registrare le recenti entrate e
uscite. L'idea era di dare questi dati ad uno script awk per sommare
algebricamente tutti gli importi e dirmi il saldo. Ecco come ho deciso di
registrare tutte le mie transazioni nel mio "estratto conto ASCII":
Codice 1.23: Estratto conto ASCII per la registrazione delle transazioni |
23 Ago 2000 cibo - - S Trattoria da Gigi 30.25
|
Ogni campo in questo file è separato da una o più tabulazioni. Dopo la data
(campo 1, $1), ci sono due campi chiamati "Categoria di spesa" e "Categoria di
entrate". Quando immetto una spesa come nella linea seguente, metto un
identificativo di quattro lettere nella categoria delle spese e un trattino "-"
(campo nullo) nella categoria delle entrate. Questo significa che questa
registrazione è una spesa per del cibo :) Ecco come appare un deposito:
Codice 1.24: Esempio di deposito |
23 Ago 2000 - entr - N Il Capo 2001.00
|
In questo caso, metto un "-" (nullo) nella categoria spese e metto "entr" in
quella delle entrate. "entr" è il nome che dò alle mie entrate generiche. Usando
gli identificativi di categoria mi permette di generare un resoconto analitico
delle mie entrate e uscite per categoria. Per quel che riguarda gli altri
record, gli altri campi sono abbastanza autoesplicativi. Il quinto campo ("S" o
"N") registra se la transazione è stata trascritta sul mio conto; oltre a questo
c'è una descrizione della transazione, e un importo positivo in euro.
L'algoritmo usato per calcolare il saldo non è troppo difficile. Awk ha bisogno
semplicemente di leggere ciascuna riga, una ad una. Se è presente una categoria
di spesa ma non una di entrate (ovvero se vale "-"), allora questa riga si
riferisce ad un debito. Se, viceversa, c'è una categoria di entrata ma non di
uscita, allora l'importo in euro va a credito. Poi, se c'è sia una categoria di
spesa che di entrata, allora l'importo si riferisce ad un "trasferimento di
categoria"; in questo caso, l'importo in euro verrà sottratto dalla categoria
delle spese e aggiunto alle entrate. Ancora una volta, queste categorie sono
fittizie, ma sono molto utili per tracciare i redditi e le spese, e anche per la
pianificazione.
Il codice
È tempo di vedere il codice. Partiremo dalla prima linea, il blocco BEGIN e una
definizione di funzione:
Codice 1.25: Saldo, prima parte |
#!/usr/bin/awk -f
BEGIN {
FS="\t+"
mesi="Gen Feb Mar Apr Mag Giu Lug Ago Set Ott Nov Dic"
}
function ciframese(mioMese) {
return (index(mesi,mioMese)+3)/4
}
|
Aggiungere la prima riga "#!..." ad un qualunque script awk permette di
eseguirlo dalla shell, a patto che tu abbia eseguito prima "chmod +x mioscript".
Le righe rimanenti definiscono il nostro blocco di BEGIN, che viene eseguito
prima che awk cominci a processare il nostro estratto conto. Impostiamo FS (il
separatore di campo) a "\t+", che dice ad awk che i campi sono separati da una o
più tabulazioni. Oltre a questo definiamo una stringa chiamata mesi, che è usata
dalla funzione ciframese(), che appare di seguito.
Le ultime tre righe vi mostrano come definire le vostre funzioni awk. Il formato
è semplice: inserite "function", poi il nome della funzione e poi i parametri
separati da virgole e racchiusi da parentesi tonde. Dopo questo un blocco "{ }"
contiene il codice che volete che la funzione esegua. Tutte le funzioni possono
accedere le variabili globali (come la nostra variabile mesi). Inoltre, awk
offre un'istruzione "return" che permette alla funzione di ritornare un valore,
e che funziona in maniera analoga al "return" del C, di Python e di altri
linguaggi. Questa particolare funzione converte il nome di un mese dalla sua
abbreviazione di tre lettere nel suo equivalente numerico. Per esempio, questo:
Codice 1.26: Esempio di chiamata a ciframese() |
print ciframese("Mar")
|
....stamperà:
Codice 1.27: Esempio di uscita di ciframese() |
3
|
Ora spostiamoci verso altre funzioni.
Funzioni finanziarie
Ecco altre tre funzioni che realizzano il bilancio per noi. Il nostro blocco di
codice principale, che vedremo presto, processerà ogni riga del file di estratto
conto sequenzialmente, chiamando una di queste funzioni in modo che le
transazioni siano memorizzate nell'array awk in modo appropriato. Ci sono tre
tipi fondamentali di transazioni: credito (funzione appEntrata), debito
(appSpesa), e trasferimento (appTrasf). Noterete che tutte le funzioni accettano
un solo argomento, chiamato mioSaldo. mioSaldo è il nome di un array
bidimensionale, che passeremo come parametro. Fino ad ora, non abbiamo trattato
gli array multidimensionali; ad ogni modo, come potete vedere più sotto, la
sintassi è abbastanza semplice. Basta separare ciascuna dimensione con una
virgola, e siete a posto.
Memorizzeremo le informazioni dentro "mioSaldo" come segue. La prima dimensione
dell'array varia da 0 a 12, specificando il mese, o zero per l'intero anno. La
nostra seconda dimensione è una categoria di quattro lettere, come "cibo" o
"entr"; questa è la categoria che stiamo attualmente trattando. Così, per
trovare il saldo dell'intero anno per la categoria cibo, dovrete andare a
leggere mioSaldo[0,"cibo"]. Per vedere il reddito di giugno, dovrete leggere
mioSaldo[6,"entr"].
Codice 1.28: Trovare informazioni sui redditi |
function appEntrata(mioSaldo) {
mioSaldo[meseCorrente,$3] += importo
mioSaldo[0,$3] += importo
}
function appSpesa(mioSaldo) {
mioSaldo[meseCorrente,$2] -= importo
mioSaldo[0,$2] -= importo
}
function appTrasf(mioSaldo) {
mioSaldo[0,$2] -= importo
mioSaldo[meseCorrente,$2] -= importo
mioSaldo[0,$3] += importo
mioSaldo[meseCorrente,$3] += importo
}
|
Quando appEntrata() od una delle altre funzioni sono chiamate, registriamo la
transazione in due posti: mioSaldo[0,categoria] e mioSaldo[meseCorrente,
categoria], il saldo per la categoria nell'intero anno e per il mese corrente
rispettivamente. Questo ci permetterà di generare facilmente sia un resoconto
annuale che mensile.
Se guardate queste funzioni, noterete che l'array referenziato da mioSaldo viene
passato per riferimento. Oltre a questo, utilizziamo anche diverse variabili
globali: meseCorrente, che mantiene il valore numerico del mese del record
corrente, $2 (la categoria di spesa), $3 (la categoria di entrata), e importo
($7, l'importo in euro). Quando appEntrata() e compagnia sono chiamate, tutte
queste variabili sono state correttamente definite per il record che viene
attualmente processato.
Il blocco principale
Ecco il blocco principale che contiene il codice che analizza ciascuna riga dei
dati in ingresso. Ricordate, poiché abbiamo settato correttamente FS, possiamo
riferirci al primo campo come $1, al secondo come $2 e così via. Quando
appEntrata() e compagnia vengono chiamate, le funzioni possono accedere i valori
correnti di meseCorrente, $2, $3 e importo dall'interno della funzione. Date
un'occhiata al codice e ci vediamo dall'altra parte per una spiegazione.
Codice 1.29: Saldo, terza parte |
{
meseCorrente=ciframese(substr($1,4,3))
importo=$7
#registrare tutte le categorie incontrate
if ( $2 != "-" )
globcat[$2]="yes"
if ( $3 != "-" )
globcat[$3]="yes"
#trattare correttamente la transazione
if ( $2 == "-" ) {
if ( $3 == "-" ) {
print "Errore: i campi entrata e spesa sono entrambi vuoti!"
exit 1
} else {
#si tratta di una entrata
appEntrata(saldo)
if ( $5 == "S" )
appEntrata(saldo2)
}
} else if ( $3 == "-" ) {
#si tratta di una spesa
appSpesa(saldo)
if ( $5 == "S" )
appSpesa(saldo2)
} else {
#si tratta di un trasferimento
appTrasf(saldo)
if ( $5 == "S" )
appTrasf(saldo2)
}
}
|
Nel blocco principale, le prime due righe impostano meseCorrente ad un valore
intero compreso fra 0 e 12, e importo al campo 7 (per rendere il codice più
facile da capire). Poi abbiamo quattro righe interessanti, in cui scriviamo
questi valori in un array chiamato globcat. globcat, o array globale delle
categorie, è usato per registrare tutte le categorie incontrate nel file:
"entr", "vari", "cibo", "util", etc. Per esempio, se $2 == "entr", settiamo
globcat["entr"] a "yes". Inseguito, possiamo scorrere la nostra lista di
categorie con un semplice ciclo "for (x in globcat)".
Nelle prossime venti righe (circa), analizziamo i campi $2 e $3, e registriamo
la transazione in modo appropriato. Se $2=="-" e $3!="-", abbiamo del reddito,
e allora chiamiamo appEntrata(). Se la situazione è rovesciata, chiamiamo
appSpesa(); e se entrambi $2 e $3 contengono delle categorie, chiamiamo
appTrasf(). Ogni volta, passiamo l'array "saldo" a queste funzioni per far sì
che i dati appropriati vi siano registrati.
Noterete anche diverse righe che dicono "if ( $5 == "Y" ), registra la stessa
transazione in saldo2". Cosa stiamo facendo esattamente qui? Vi ricorderete che
$5 contiene o una "S" o una "N", e ricorda se la transazione è stata registrata
sul conto. Siccome registriamo la transazione su saldo2 solo se la transazione è
stata trascritta, saldo2 conterrà il saldo reale del conto, mentre "saldo"
conterrà tutte le transazioni,sia che siano state trascritte o meno. Potete
usare saldo2 per verificare i vostri dati (poiché dovrebbe corrispondere al
vostro saldo corrente per come vi è stato comunicato dalla banca), e usare
"saldo" per assicurarvi di non andare in rosso con il vostro conto (poichè
memorizzerà anche importi che non sono stati ancora contabilizzati).
Generare un resoconto
Dopo che il blocco principale processa ripetutamente ciascun record, abbiamo un
esauriente registro di entrate e uscite, catalogate per categoria e per mese.
Ora quello che ci resta da fare è definire un blocco END che genererà un
resoconto, in questo caso semplice:
Codice 1.30: Generare un resoconto finale |
END {
sal=0
sal2=0
for (x in globcat) {
sal=sal+saldo[0,x]
sal2=sal2+saldo2[0,x]
}
printf("Soldi a disposizione : %10.2f\n", sal)
printf("Il saldo del tuo conto: %10.2f\n", sal2)
}
|
Questo resoconto stampa un riepilogo che somiglia a qualcosa del genere:
Codice 1.31: Resoconto finale |
Soldi a disposizione : 1174.22
Il saldo del tuo conto: 2399.33
|
Nel nostro blocco END, abbiamo usato il costrutto "for (x in globcat)" per
scorrere la lista delle categorie, creando un saldo globale basato su tutte le
transazioni registrate. Abbiamo in pratica creato due bilanci, uno per i soldi
disponibili, e l'altro per il saldo del conto bancario. Per eseguire il
programma e processare le vostre note finanziarie che avete scritto nel file
chiamato estrattoconto.txt, mettete tutto il codice sopra in un
file di testo, fate chmod +x saldo, e poi scrivete "./saldo
estrattoconto.txt". Lo script saldo sommerà tutte le transazioni e stamperà
per voi un saldo di due righe.
Migliorie
Io uso una versione più avanzata di questo programma per gestire le mie finanze
personali e di lavoro. La mia versione (che non ho potuto includere qui per
ragioni di spazio) stampa un resoconto mensile di entrate e uscite, includendo
dei totali annuali, il reddito netto e un sacco di altre cose. Ancora meglio,
produce un output in formato HTML, così lo posso vedere in un browser Web :) Se
trovate questo programma utile, vi incoraggio ad aggiungere queste
caratteristiche allo script. Non avrete bisogno di configurarlo per registrare
altre informazioni aggiuntive; tutte le informazioni di cui avete bisogno sono
già in saldo e saldo2. Migliorate solo il blocco END, e siete a posto.
Spero che questa serie vi sia piaciuta. Per maggiori informazioni su awk,
controllate le risorse elencate qui sotto.
2.
Risorse
|
|
Aggiornato il 31 ottobre 2005 |
Oggetto:
In questa conclusione alla serie su awk, Daniel Robbins presenta le importanti
funzioni di awk che manipolano le stringhe, e poi mostra come scrivere un
programma di estratto-conto/bilancio partendo da zero. Durante questo percorso,
imparerete a scrivere vostre funzioni e ad usare gli array multidimensionali di
awk. Alla fine di questo articolo, avrete ancora più esperienza con awk, che vi
permetterà di scrivere script sempre più potenti.
|
Daniel Robbins
Author
Luca Martini
Traduzione
|
|
Donate to support our development efforts.
|
|
|