Gentoo Logo

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

Indice:

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



Stampa

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.

Copyright 2001-2014 Gentoo Foundation, Inc. Questions, Comments? Contact us.