Creare CD - Copertina - Giochi |
Articoli
Vi sarà sicuramente capitato di dover estrarre dei dati da files di testo o di effettuare delle modifiche riga per riga, eventualmente condizionate dalla presenza di certi pattern, spesso considerando i dati di input come composti da parole o campi. Se avete frequentemente esigenze di questo tipo AWK è lo strumento che fa per voi. Naturalmente potreste scrivere dei meravigliosi programmi in C per fare ciò che vi serve ma dovreste perdere delle intere giornate per scrivere del codice ad-hoc che poi probabilmente non usereste più, e di solito ci mettereste di meno a fare il lavoro a mano.
Volendo dare una definizione sintetica di AWK lo si può definire come un filtro generico per files di testo basato su pattern-matching e programmabile con un linguaggio molto simile al C ma di utilizzo molto più semplificato. Il nome AWK deriva dalle iniziali dei suoi tre inventori originali, Aho, Weinberger e Kernighan, tutti personaggi abbastanza famosi nel campo della computer-science.
Di AWK ne esistono diverse versioni e implementazioni. Io farò riferimento alla implementazione GNU, detta gawk, che oltre a rispettare lo standard POSIX relativo ad AWK ha come sempre qualche funzionalità in più rispetto alle altre implementazioni commerciali che potete trovare in giro. Il Free Software è sempre il meglio!
L'idea che sta alla base di AWK è quello di elaborare una riga alla volta dei files di input, eseguendo azioni diverse a seconda che la riga stessa soddisfi certe condizioni o contenga certi patterns. I programmi scritti in AWK sono strutturati un po' diversamente rispetto a quelli in linguaggi tradizionali. Non esistono infatti un'inizio e una fine del programma stesso, e non è necessario dichiarare le variabili e gestire tutta la solita trafila dell'I/O e del parsing dei dati. A tutto questo ci pensa automaticamente AWK. Quello che resta da fare al programmatore è solamente specificare come devono essere elaborati i dati di input a seconda dei vari patterns in essi contenuti. Un programma AWK è quindi costituito solamente da una lista di regole composte da un pattern ed una corrispondente azione da eseguire se il pattern è soddisfatto:
pattern { azione } pattern { azione } ...AWK si incarica automaticamente di aprire e chiudere i files di input, di leggerne il contenuto una riga alla volta e di applicare a ciascuna le regole definite dal programmatore. Inoltre appena letta una riga AWK la separa automaticamente in campi, in base ad un delimitatore definibile, li assegna a delle variabili predefinite ed aggiorna automaticamente altre variabili di utilità generale, come il numero di righe lette e il numero di campi della riga corrente. Si tratta di operazioni più o meno standard che di solito si devono codificare a mano (reinventando ogni volta l'acqua calda) e che invece AWK esegue automaticamente, tutta fatica risparmiata per il programmatore. Per ciascuna riga di input vengono poi esaminate tutte le regole specificate e vengono eseguite le azioni corrispondenti a tutte quelle i cui pattern risultano soddisfatti. In pratica può succedere che per una certa riga di input non sia soddisfatto nessun pattern e quindi non venga eseguita nessuna azione, oppure che ne sia soddisfatto più di uno, nel qual caso vengono eseguite tutte le azioni corrispondenti, nell'ordine in cui sono state specificate le regole.
La potenza di AWK rispetto ad altri strumenti simili (grep, sed, cut, ecc.) è la possibilità di definire delle azioni completamente arbitrarie da effettuare sui dati di input utilizzando un vero linguaggio di programmazione, senza tuttavia essere costretti a dover gestire tutte le complicazioni tipiche dei linguaggi tradizionali. Il linguaggio di AWK è inoltre dotato di un ricco insieme di funzioni di base, fra cui quelle per la gestione di pattern matching ed espressioni regolari, e permette di gestire in modo molto semplice stringhe ed array associativi. È infine possibile definire delle proprie funzioni, come in qualsiasi linguaggio di programmazione che si rispetti, ed e' sempre possibile richiamare un programma esterno quando non si è in grado di fare una cosa direttamente con AWK.
Supponiamo di voler estrarre da un elenco di nomi e numeri di telefono tutte le righe che contengono il nome MARIO. Possiamo fare cosí:
cat elenco.txt | awk '/MARIO/ { print }'oppure anche:
awk '/MARIO/ { print }' file1 file2 file3oppure semplicemente:
cat elenco.txt | awk '/MARIO/'In questi esempi ho eseguito AWK passando direttamente nelle linea di comando il programma da interpretare, costituito da una sola regola che dice di stampare la riga di input quando questa contiene la stringa 'MARIO'. Tutto ciò che non contiene MARIO viene letto ma non stampato in output. Notate che i dati da elaborare possono essere contenuti in files i cui nomi sono specificati nella linea di comando oppure semplicemente passati in standard input come per qualsiasi programma unix. In seguito userò soprattutto questa seconda tecnica per evidenziare il concetto che AWK può essere usato come filtro di dati.
Negli esempi precedenti il pattern è costituito dall'espressione regolare racchiusa fra / / mentre l'azione corrispondente è quella racchiusa fra { }. Notate che il pattern oppure l'azione possono essere omessi. Se non specifico il pattern significa che l'azione deve essere eseguita per qualsiasi riga di input, mentre se non specifico l'azione (come nell'ultimo esempio) significa che voglio semplicemente stampare la riga di input, che è stata assegnata automaticamente alla variabile $0. Negli esempi precedenti il programma AWK è stato specificato direttamente nella linea di comando. Ovviamente nel caso di programmi più lunghi di qualche riga ciò non è molto pratico, in tal caso si salva il programma in un file e si specifica ad AWK il nome del file da eseguire:
echo '/MARIO/ {print $0}' > cerca-mario.awk cat elenco.txt | awk -f cerca-mario.awkÈ possibile specificare più files contenenti codice AWK da eseguire, per esempio utilizzando delle librerie di funzioni standard. I vari files vengono concatenati insieme come se fossero un unico programma:
cat /etc/passwd | awk -f ~/crack.awk -f /usr/share/awk/passwd.awkVediamo ora un esempio più complicato di un programma AWK che mi consente di estrarre il mittente e il soggetto da un messaggio di posta elettronica:
#!/usr/bin/awk -f # Estrae il From e il Subject da una mail. ($1 == "From:") { print(substr($0,7)) } # mittente ($1 == "Subject:") { print(substr($0,10)) } # soggettoQui ho usato il meccanismo standard del #! che mi consente di richiamare lo script direttamente col suo nome una volta che l'ho reso eseguibile. Da questo esempio possiamo pure intuire che il carattere # serve ad iniziare un commento, anche a metà di una riga. Possiamo inoltre vedere come le variabili $1 ... $N vengono automaticamente inizializzate con i campi in cui è stata suddivisa la riga di input. Per esaminare il quinto campo userò pertanto la variabile $5, mentre la variabile $0 contiene l'intera riga di input.
Altre variabili che vengono settate automaticamente da AWK dopo la lettura di ogni riga sono NF, che contiene il numero di campi del record corrente, NR, che contiene il numero totale di record letti, FILENAME, che contiene il nome del file di input corrente, e molte altre ancora.
Una cosa interessante è che se si modifica il valore di un campo viene modificato anche l'intero record di input contenuto in $0, come si vede dal seguente esempio. La stessa cosa succede se si modifica la variabile NF.
#!/usr/bin/awk -f # Come l'esempio precedente ma in italiano ($1 == "From:") { $1 = "Mittente:"; print } ($1 == "Subject:") { $1 = "Argomento:"; print }La suddivisione del record in campi avviene automaticamente utilizzando il valore della variabile FS, che per default (tipica esclamazione usata dai programmatori) è una sequenza qualsiasi di spazi e tab. Se voglio estrarre da /etc/passwd tutti gli utenti con la password bloccata posso utilizzare il seguente comando:
cat /etc/passwd | awk 'BEGIN { FS=":" } ($2 == "*") { print $3,$1 }'In questo esempio ho usato un pattern particolare, BEGIN, che per definizione viene soddisfatto solo prima di leggere la prima di riga di input. In altre parole il pattern BEGIN serve a specificare un'eventuale inizializzazione del programma che deve essere eseguita prima di cominciare a elaborare i dati. In questo esempio ho settato la variabile FS con la stringa ":" in modo che le righe di /etc/passwd vengono suddivise correttamente nei campi corrispondenti. Esiste anche un pattern END che viene eseguito quando AWK ha terminato di elaborare tutti i dati di input, in modo da poter eseguire delle azioni di chiusura, per esempio stampare dei totali calcolati in precedenza.
Oltre alla variabile FS, che descrive il separatore dei campi di input, e' possibile settare la variabile OFS che specifica il separatore con cui vengono stampati i campi in output. Posso pertanto riscrivere il file delle password con le virgole al posto dei due punti semplicemente con:
cat /etc/passwd | awk 'BEGIN { FS=":"; OFS="," } { print }'
Gli esempi che ho illustrato finora vi fanno anche vedere che con pochissime righe di codice è possibile scrivere dei programmi AWK che fanno qualcosa di utile. Provate ora a pensare a quanto codice in C dovreste scrivere per realizzare delle cose simili e capirete perché vi conviene conoscere almeno un po' AWK.
Come ho già anticipato l'azione associata a ciascun pattern può
essere specificata utilizzando un linguaggio di programmazione simile al C. Se
già sapete programmare in C non avrete difficoltà ad usare
AWK. Ci sono però alcune differenze sostanziali rispetto al C da tenere
presenti. La prima è che in AWK esiste un unico tipo di dato, la
stringa ASCII, che viene utilizzato come stringa o come numero a seconda del
contesto. La seconda è che non devo dichiarare le variabili del mio
programma e che queste assumono dei valori di default (la stringa vuota "" o
il numero 0) se vengono referenziate senza essere state inizializzate (vedi
l'esempio del totalizzatore che usa la variabile
sum
senza inizializzarla). Non esistono le strutture o gli array
e soprattutto mancano i puntatori e tutte le complicazioni che questi si
portano dietro. Un'altra cosa molto comoda di AWK è che si possono
concatenare fra loro variabili o costanti semplicemente scrivendoli uno dopo
l'altro. Per esempio:
nome = $1 cognome = $2 nominativo = nome " " cognome print "Nome e Cognome: " nominativoNon ho pertanto le complicazioni tipiche del C come strcat, sprintf, atoi e cose del genere. Tutte le variabili sono stringhe e quindi non servono conversioni, se non quando sono usate nei calcoli aritmetici, ma in tal caso le fa AWK automaticamente. Ci sono ovviamente le istruzioni di controllo classiche del C: if, do, while, for, ecc. La loro sintassi è praticamente uguale. Stranamente non c'è l'istruzione
switch
, ma non se ne sente molto la mancanza visto che si possono
usare i patterns, cosí come manca il goto
("considered harmful").
Le variabili non devono essere precedute da $ come succede in shell, perl ed
altri linguaggi interpretati, con l'eccezione delle variabli $0,$1,ecc.
Le stringhe costanti devono pertanto essere sempre racchiuse fra doppi apici,
a meno che non siano dei numeri.
Una cosa importante da ricordare è che tutti gli
indici in AWK partono da 1 e non da 0 come si usa in C. In generale una
istruzione sta su una sola riga. È però possibile mettere
più istruzioni in una riga separandole con un punto e virgola, oppure
scrivere un' istruzione su più righe utilizzando il carattere backspace
(\) per concatenarle fra di loro.
Il linguaggio di AWK comprende alcune istruzioni molto potenti per leggere e
scrivere da files o da pipes su comandi esterni. Fra queste ricordiamo la
printf
, il cui uso è molto simile a quello del C:
printf("Nome e Cognome: %-24s\tEtà: %2d\n", nominativo, eta)e la
print
che si limita a stampare i valori specificati
concatenandoli con la variabile OFS (output field separator) e terminando il
tutto con un newline:
OFS = "\t" print "Nome e Cognome: " nominativo, "Età: " etaNotare che nell'esempio i valori passati alla print sono due e non quattro perché "Nome e Cognome: " e nominativo sono concatenati in un unico valore, cosi come "Età: " ed eta. I due valori sono stampati in output separati da OFS, precedentemente settato a Tab.
Possiamo ovviamente leggere e scrivere in files esterni:
outfile = "output.txt" print $1,$2 > outfile print $1,$2 >> "/tmp/xxx.log" # notare gli apici attorno al nome getline < "input.txt" # assegna NF, $0 e $1 ... $N getline my_var < "input.txt" # assegna la variabile my_varPossiamo inoltre scrivere in un pipe connesso allo standard input di un programma esterno o leggere l'output di un programma da un pipe:
print $0 | "mail -s test " destinatario print "." | "mail -s test " destinatario close("mail -s test " destinatario) "hostname" | getline hostname close("hostname")Notare che alla fine è necessario chiudere i files e che questi vengono referenziati con il loro pathname o con il comando unix eseguito. Pertanto l'espressione
close("mail -s test " destinatario)
chiude il pipe
che è stato aperto in precedenza con tale comando.
Una cosa molto comoda che non si trova in C e che invece è disponibile in AWK è la possibilità di gestire in modo molto semplice degli array associativi, cioè delle strutture dati in cui posso associare un valore ad una chiave arbitraria e recuperarlo in maniera molto efficiente. Per esempio posso contare quante volte ogni parola compare nei dati di input con il seguente programmino:
#!/usr/bin/awk -f { for (i=1; i<=NF; i++) totale[$i]++ } END { for (parola in totale) print parola, totale[parola] }Due righe di AWK che si buttano giù in meno di un minuto. Provate a riscriverlo in C e controllate quanto codice dovete scrivere e quanto tempo ci avrete messo a farlo funzionare. Nell'esempio ho utilizzato un array associativo
totale
che
contiene in corrispondenza di ciascuna parola letta in input il numero di
volte che la parola è stata trovata. Nell'azione corrispondente al
pattern END ho utilizzato una particolare sintassi dell'istruzione
for
che assegna in sequenza la variabile parola a tutte
le chiavi contenute nell'array e stampa la chiave ed il valore ad essa
associato. Posso anche verificare se una certa chiave è presente in un
array con la sintassi:
if ("chiave" in array) ...Posso infine cancellare un elemento o l'intero array con l'istruzione
delete
:
delete array["pippo"] delete arrayLe differenze sostanziali rispetto agli array tradizionali del C sono che non devo preallocare l'array con una certa dimensione, in AWK tutta la gestione della memoria è automatica, e che al posto degli indici numerici ho delle chiavi di accesso costituite da stringhe. Pertanto non farò più riferimento all'elemento con posizione 123 ma a quello con chiave alfanumerica "123" oppure "pippo". Se devo comunque usare indici numerici lo posso fare lo stesso ma di fatto sto sempre utilizzando chiavi alfanumeriche, per cui l'espresssione
array[123]
è in realtà
equivalente a array["123"]
. Posso anche simulare array
multidimensionali con la notazione array[x,y,z]
.
Abbiamo visto che un programma AWK è costituito da una serie di coppie pattern-azione, in cui l'uno o l'altro possono essere omessi. Un pattern può essere costituito da:
next
che salta direttamente al record successivo.
Per spiegare bene le espressioni regolari ci vorrebbe un'intero articolo, pertanto assumo che già le conosciate almeno a grandi linee e cercherò di farvi vedere come si possono usare con AWK.
Posso usare un'espressione regolare per confrontare l'intera riga di input oppure un singolo campo utilizzando l'operatore ~:
#!/usr/bin/awk -f /^From: / { print "Mittente: " $2 } $1 ~ /Subject:/ { $1 = "Soggetto:"; print }Posso anche usare un'espressione regolare come argomento ad una delle varie funzioni che accettano espressioni regolari come parametri, per esempio:
if (match($1, /Microsoft/) { gsub(/Microsoft/, "Micro$$$oft", $1) }
Il linguaggio di AWK contiene un buon numero di funzioni built-in predefinite, per esempio funzioni numeriche, funzioni per lavorare con stringhe, funzioni di I/O, funzioni per formattare le date, ecc. Fra le più utili possiamo ricordare:
gsub
che
fa la stessa cosa ma per tutte le occorrenze di regexp
Una cosa abbastanza facile da fare con AWK è un filtro che spia tutto quello che viene scritto in un file di log e calcola delle statistiche, o dei totali oppure esegue dei comandi quando trova certi messaggi di errore. Per esempio:
tail -f /var/log/messages \ | awk '($5 ~ "rshd" && $7 ~ "root") { system("mail -s 'ROOT RSH' root") }'In questo modo posso implementare esternamente delle funzionalità che un certo programma non è in grado di fornire, o monitorare il verificarsi di errori o di condizioni critiche ed intraprendere automaticamente delle azioni. Il tutto senza dover modificare il programma che viene monitorato che si limita semplicemente a scrivere il suo file di log.
AWK permette all'utente di definire delle proprie funzioni che possono poi essere utilizzate liberamente all'interno del codice esattamente come quelle predefinite. La sintassi per definire nuove funzioni è la seguente:
function nome(parametri) { ... body ... }Per esempio posso definire il classico fattoriale con:
function fact(n) { if (n <= 1) { return(1) } else { return (n * fact(n-1)) } }e poi utilizzarlo in una regola o in un'altra funzione:
$1 ~ "^[0-9]+$" { m = fact($1) }Non è possibile dichiarare variabili locali all'interno delle funzioni, ma si può aggirare questo limite dichiarando degli argomenti in piu rispetto a quelli che vengono passati. In questo modo quelli che non sono specificati nella chiamata sono inizializzati con stringhe vuote e possono essere usati come variabili locali. Se nella funzione fact avessi bisogno di due variabili locali potrei pertanto scrivere:
function fact(n, var1, var2) { var1 = ... var2 = ... if (n <= 1) { return(1) } else { return (n * fact(n-1)) } }Notate che esiste la convenzione di separare gli argomenti locali dai parametri con degli spazi in più, per migliorare la leggibilità del codice, come nell'esempio precedente.
Esistono vari modi di modi di utilizzare AWK, da solo o in combinazione con altri programmi. È possibile eseguirlo semplicemente dal prompt delle shell passandogli lo script nella linea di comando, se non è troppo lungo:
awk '/pippo/ { print $1 }' input-file.txtoppure memorizzando il programma in un file esterno:
awk -f pippo.awk input-file.txtSe questo file viene reso eseguibile e contiene nella prima riga il path di AWK preceduto da #! è possibile anche eseguirlo direttamente col suo nome:
pippo.awk input-file.txtPosso anche inserire il comando awk e il suo script all'interno di uno script di shell, eventualmente utilizzando la sostituzione della shell all'interno dello script AWK:
#!/bin/bash ... find . -name \*.c | awk ' # programma awk BEGIN { ... } ($1 == "xyz") { ... } ' | sort | uniq ...Il vantaggio di questa tecnica è che lo script di shell e gli script di AWK che gli servono sono contenuti nello stesso file. In questo caso è pero' necessario quotare l'intero script di AWK all'interno dello script di shell. Se all'interno dello script non ci sono apici singoli conviene usare questo carattere, altrimenti si è costretti a usare i doppi apici ma in tal caso bisogna ricordarsi di quotare tutti i dollari ed i backslash dello script lasciando solo quelli che devono essere espansi dalla shell.
AWK viene usato molto di frequente per fare operazioni 'triviali' sui dati di input, cose che spesso potrei fare anche utilizzando cat, grep o sed. Molto spesso si tratta di progammi di una sola riga. Vediamo qualche esempio.
awk '{ print }'
awk 'NF > 0'
awk '{ print NR, $0 }'
awk 'END { print NR }'
df -k | awk '{ print $4 }'
cat /etc/hosts | awk "(\$1 == $ipaddr) { print \$2 }"oppure
cat /etc/hosts | awk -v ip=$ipaddr '($1 == $ip) { print $2 }'
awk -v ncol=3 '{ sum += $ncol } END { print sum }'
awk '{ if ($1 > max) { max = $1 } } END { print max }'
awk '(NR % 2) { print }'
expand | awk '{ if (max %lt; length()) max = length() } END { print max }'
ls -l | awk '/^-/ { tot += int(($5+1023)/1024) } END { print tot*1024 }'
awk -v n=10 'BEGIN { for (i=1; i<=n; i++) print int(101 * rand()); exit }'
Tempo fa sono stato costretto (per vile denaro) a scrivere un programmino in msdos (ORRORE!) per interfacciarmi ad una applicazione unix. Non disponendo nè di msdos nè di un compilatore C per tale S.O. (Schifezza Obbrobriosa), ho deciso di sviluppare il codice in Linux con AWK e poi portare il tutto in dos utilizzando la versione dos di AWK. Ne è venuto fuori un semplice programma interattivo che funziona più o meno cosí:
#!/usr/bin/gawk -f BEGIN { ESCAPE = "^[" inizializzazione() show_menu() prompt() } END { cls() print "Bye." } ($1 == "q" || $1 == "quit") { exit } ($1 == "1") { do_comando1() show_menu() prompt() next } ... { show_menu() message("\007comando sconosciuto") prompt() next } function inizializzazione() { ... } function do_comando1() { ... } function show_menu() { cls(); cursor(2,1); printf("1.\tComando 1") ... } function message(msg) { cursor(25,1); kill_line() printf msg } function prompt() { cursor(24,1); kill_line() printf("comando ==> ") } function cls() { printf(ESCAPE "[2J") printf(ESCAPE "[1;1H") } function cursor(row, col) { printf(ESCAPE "[" row ";" col "H") } function kill_line() { printf(ESCAPE "[K") }In pratica ho sfruttato il linguaggio di AWK per disporre di uno pseudo-C interpretato sotto dos, e utilizzando le sequenze standard di escape ANSI, che per fortuna ci sono anche in dos, ho scritto un semplicissimo programmino che legge i comandi interattivi dell'utente dallo standard input, li esegue e stampa di nuovo il menu e il prompt. Questo è un uso abbastanza improprio di AWK, che ricordo essere stato progettato per filtrare testi, ma mostra come esso sia abbastanza flessibile da poter essere utilizzato anche per realizzare dei semplici programmi interattivi.
Un altro linguaggio molto diffuso che si può usare in alternativa ad AWK è il Perl. In realtà sono due strumenti abbastanza diversi anche se possono essere usati per fare cose simili. Perl e' un generico linguaggio di programmazione con molte funzionalità in più ed una ricca libreria di moduli. AWK è un tool per scrivere filtri dotato di un linguaggio con sintassi simile al C. Il vantaggio di Perl è che è un linguaggio molto più completo di AWK, per cui si possono fare cose che con AWK non sono possibili. Lo svantaggio di Perl è che il suo linguaggio di programmazione è un po' più complicato e meno leggibile, e questo può essere un problema specialmente per i principianti. Il vantaggio di AWK è che il suo linguaggio di programmazione è molto più 'pulito' e semplice da usare di quello di Perl. Inoltre AWK gestisce automaticamente la lettura dei dati e la separazione dei campi.
In linea generale conviene usare AWK quando si devono realizzare filtri di testo e non si ha bisogno delle funzionalità extra offerte da Perl. Conviene invece usare Perl se si devono scrivere programmi che non sono dei filtri, per esempio script di amministrazione di sistema o cgi, oppure cose che necessitano della potenza in più offerta da Perl. Esiste comunque un traduttore che è grado di tradurre in Perl i programmi scritti per AWK.
In conclusione, se conoscete AWK avete a disposizione nella vostra cassetta degli attrezzi uno strumento abbastanza facile da usare ma anche abbastanza potente per risolvere con facilità un gran numero di problemi che si incontrano quotidianamente quando si lavora in unix. Il tempo e la fatica che vi risparmierete utilizzando AWK vale sicuramente il paio di mezze giornate che spenderete a leggervi la pagina di manuale (man gawk) o l'ottimo file di info disponibile in emacs. Se poi conoscete già il C gran parte della fatica è già fatta. Un ulteriore vantaggio di AWK è che è disponibile in tutti i sistemi operativi e perfino in ambiente MSDOS. Potete quindi portare facilmente il vostro codice da un sistema all' altro e riutilizzarlo senza problemi.
Creare CD - Copertina - Giochi | B