martedì 26 agosto 2014

L'asteroide che ucciderà questo dinosauro deve ancora arrivare (terza parte)

L'articolo è diviso in tre parti:
Prima Parte
Seconda Parte

Rieccoci qui a parlare di espressioni regolari. Dopo aver visto cosa sono (e da dove derivano) ed aver visto come si leggono e come si possono scrivere è giunta l'ora di informarci su alcuni dei software che ne fanno uso.

grep

Abbiamo già nominato grep nella prima parte, se ve la foste persa (MALE) ecco la definizione presa pari-pari dal primo articolo di questa serie:

grep è uno dei dinosauri di UNIX che si rifiutano di estinguersi. Nasce come modalità di ricerca di ex (General Regular Expression Print) ma è stato poi scorporato ed è diventato un tool fondamentale nelle mani di ogni amministratore di sistema e di chiunque debba ricercare pattern particolari in vaste collezioni di file di testo.

grep dà il meglio di sè all'interno di altri script o di one-liner (singole linee di comando ottenute concatenando con dei pipe vari comandi della shell UNIX). Il suo compito è quello di tagliare via da un flusso di testo le porzioni non rilevanti per poi poterle analizzare meglio con altri strumenti.

Nella migliore tradizione UNIX grep accetta testo dallo standard input, manda del testo in output sullo standard output e i messaggi di errore sullo standard error.

Facciamo subito un esempio concreto: vogliamo sapere qual è il MAC address di un'interfaccia di rete. Il comando ifconfig, sebbene deprecato, fa al caso nostro: se scriviamo /sbin/ifconfig eth0 infatti otteniamo qualcosa di simile a questo:

eth0      Link encap:Ethernet  HWaddr ba:bb:e0:ba:bb:e0
          inet addr:192.168.0.8  Bcast:192.168.0.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:201005 errors:0 dropped:0 overruns:0 frame:0
          TX packets:136434 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:212918027 (203.0 MiB)  TX bytes:18123529 (17.2 MiB)
          Interrupt:21 Memory:dffe0000-e0000000 
 
Ma a noi non interessa TUTTO quel testo, a noi basta il MAC address (che ifconfig chiama HWaddr): come facciamo ad ottenere solo quello?

Per prima cosa osserviamo la struttura di un MAC address e vediamo che è formata da 6 gruppi di cifre esadecimali separate da dei due punti (:). Costruiamoci ora una regex che trovi questa particolare sequenza:

([0-9a-f]{2}:){5}[0-9a-f]{2}
 
Se avete problemi a leggerla significa che non vi siete impegnati nella lettura dell'articolo precedente (MOLTO MALE). Avrei potuto scrivere la regex diversamente, ma questa è la versione più breve che sono riuscito ad escogitare grazie all'uso dei quantificatori.

Abbiamo la regex e abbiamo il nostro input, passiamo tutto attraverso grep e vediamo cosa succede:

$ /sbin/ifconfig eth0 | grep ([0-9a-f]{2}:){5}[0-9a-f]{2}
bash: syntax error near unexpected token `[0-9a-f]{2}:'
$
 
Giustamente bash ci notifica che non sa cosa sia [0-9a-f]{2}:, rimediamo con un po' di quoting:

$ /sbin/ifconfig eth0 | grep '([0-9a-f]{2}:){5}[0-9a-f]{2}'
$
 
Nessun output... Abbiamo sbagliato qualcosa nella regex? Ni: ci siamo dimenticati che grep di default non riconosce i quantificatori, ma a questo si rimedia usando egrep (oppure indicando a grep che vogliamo usare le extended regular expressions tramite il flag -E):

$ /sbin/ifconfig eth0 | egrep '([0-9a-f]{2}:){5}[0-9a-f]{2}'
eth0      Link encap:Ethernet  HWaddr ba:bb:e0:ba:bb:e0
$
 
Meglio, ma non è abbastanza: abbiamo ancora troppo output. Questo perché di default grep ed egrep stampano le righe in cui c'è un riscontro positivo per la regex che gli passiamo. Fortunamente c'è un flag che ci consente di far stampare a grep solamente la parte di testo che corrisponde alla regex, si tratta del flag -o:

$ /sbin/ifconfig eth0 | egrep -o '([0-9a-f]{2}:){5}[0-9a-f]{2}'
ba:bb:e0:ba:bb:e0
$
 
Ottimo! Questo è il risultato che volevamo! Adesso possiamo usare quel one-liner all'interno di altri script bash per ottenere il MAC address di una scheda di rete e salvarlo in una variabile o in un file.

Ci sono diversi usi possibili di questo one-liner:

  • Comporre un elenco di MAC address da inserire nella configurazione del server DHCP per ottenere delle assegnazioni statiche di indirizzi IP.
  • Se si usa un sistema di installazione automatico tramite boot da rete si può notificare al server di installazione che tutto è andato a buon fine e che può rimuovere il nostro MAC address da quelli che devono essere ancora installati.
  • Usando solo egrep e quell'espressione sui log del daemon DHCP si può costruire un database dei MAC Address che si sono connessi alla nostra rete.
Ad esempio eccovi uno script della shell che stampa a video tutti i MAC address delle interfacce di rete presenti nel sistema preceduti dal nome dell'interfaccia stessa:

#!/bin/sh
for IFACE in $(/sbin/ifconfig | egrep -o '^[a-z0-9]+')
    do
        MACADDR=$(/sbin/ifconfig $IFACE | egrep -o '([0-9a-f]{2}:){5}[0-9a-f]{2}')
        echo $IFACE $MACADDR
    done
 
Confido che lo script sia abbastanza breve e abbastanza semplice da poter essere compreso anche da chi non sa scrivere script della shell ma ha già una conoscenza di base di programmazione. Del resto il grosso del lavoro lo fa egrep filtrando adeguatamente l'output di ifconfig: prima ricavando il nome delle singole interfacce e poi estraendo i MAC address.

Bonus: questo script funziona anche su FreeBSD, NetBSD e OpenBSD (non ho un Mac su cui provarlo, ma credo che funzioni anche su Mac OS X).

Alcuni scripter di lunga data mi faranno sicuramente notare che richiamare tutte quelle volte ifconfig è superfluo: come compito per casa potete modificare quello script affinché prenda l'output di ifconfig all'inizio, lo salvi in una variabile e poi lo passi ad egrep tramite echo.

sed

sed è un altro dinosauro di UNIX: il suo nome è l'abbreviazione di stream editor ed è tutt'ora uno dei più potenti tool per il trattamento automatico dei file di testo nei sistemi operativi POSIX.

In sed le espressioni regolari sono usate in due contesti:

  1. Per indicare un pattern che indichi la riga su cui agire.
  2. Per indicare un pattern che indichi uno schema di sostituzione.
Vediamo più in dettaglio cosa intendo: supponiamo che vogliate eliminare da un file tutte le righe vuote (righe che contengono zero o più caratteri di spaziatura). Un'operazione del genere si fa abbastanza rapidamente con un editor di testo tradizionale (come nano, leafpad, gedit, kwrite, eccetera...) a patto che il testo non sia troppo lungo. Rifare l'operazione per una dozzina di file di testo da 10 kB l'uno comincia ad essere una cosa lunga, figuriamoci se i file fossero di più e/o più grandi...

Come si fa ad automatizzare questo compito con sed? La cosa è piuttosto semplice quando si scopre che il comando per cancellare una linea è d e che le linee da cancellare possono essere indicate da una regex racchiusa tra due slash (/). Tutto si riduce al seguente one-liner:

$ sed '/^[\ \t]*$/d' file_da_modificare > file_modificato
 
La regex non è molto difficile, ormai dovreste essere avvezzi alla lettura di quei simboli arcani. Tuttavia ci sono delle novità che non ho incluso nei miei articoli precedenti e che vale la pena di commentare.

La prima novità sono i delimitatori di inizio e fine riga (rispettivamente ^ e $). Questi delimitatori sono stati introdotti da sed e sono stati poi adottati anche da altri programmi che fanno uso delle espressioni regolari. Senza di essi il nostro pattern diventa troppo generico e finisce per individuare tutte le righe del file, così invece indichiamo esattamente tutte e sole le righe che contengono zero o più spazi o zero o più TAB del nostro file.

La seconda novità è meno eclatante: il simbolo \t non indica il carattere t ma il TAB. Assieme a \n che indica l'andare a capo è una delle sequenza di quoting più utilizzate. Analogamente lo spazio si indica con uno slash seguito da... Uno spazio! Ovviamente!

Se siete tra coloro che utilizzano il sed del progetto GNU avete anche un'utile estensione che permette l'editing in-place: tramite il flag -i è possibile indicare a GNU sed di modificare il file direttamente, senza passare per file intermedi. Io però tendo a non farne uso per due ragioni:

1. Potrei aver sbagliato qualcosa nell'impostare la regex per sed e mi ritroverei con un file corrotto ed irrecuperabile. 2. Non fa parte delle specifiche standard e può essere emulato con un successivo uso del comando mv sul file temporaneo.

La vera forza di sed però sta nel suo comando dedicato alla sostituzione. A differenza del comando per cancellare il comando per sostituire ha la seguente struttura:

/indirizzo/s/regex/sostituzione/flags
 
L'indirizzo è opzionale e può essere sia una regex che un numero non racchiuso tra slash. Nel primo caso ogni riga viene confrontata con la regex e se questa è verificata l'azione di sostituzione viene compiuta. Nel secondo caso solo la linea indicata viene coinvolta. Ad onor del vero è possibile indicare due indirizzi separandoli con una virgola (,). Per esempio 1,10 coinvolge le prime 10 righe del file mentre 10,/sed/ coinvolge le righe dalla 10 in poi ma solo quelle che sono comprese fino alla prima riga che contiene la stringa sed (occhio che la regex NON viene applicata alla decima riga che viene inclusa automaticamente tra le righe da trattare e la riga trovata dalla regex sarà processata anch'essa). È anche possibile indicare due regex ed in tal caso la prima regexp indicherà la riga da cui cominciare a processare e la seconda la riga in cui fermarsi.

La s indica il comando di sostituzione ed è seguita da una regex e da un pattern di sostituzione.

I flags modificano il comportamento del comando, ad esempio g indica di effettuare la sostituzione su TUTTI i match all'interno della riga (mentre il default è di fermarsi al primo match) mentre un numero indica che la sostituzione deve essere compiuta solo in quel match (ad esempio solo il secondo match saltando il primo).

Facciamo un esempio e prendiamo il caso descritto nel primo articolo della serie: convertire le date in formato statunitense (MM/GG/AAAA) in quello europeo (GG/MM/AAAA). Per prima cosa costruiamo l'espressione regolare che riconoscerà le date statunitensi:

(0[1-9]|1[0-2]?|[2-9])/(0?[1-9]|[1-2][0-9]|3[0-1])/([0-9]{4})
 
Anche in questo caso non commenterò la regex (vi lascio come compito per casa la verifica della correttezza della medesima). Sappiate però che i gruppi non sono stati scelti a caso, anzi capiremo presto come quella suddivisione sia essenziale per il nostro scopo.

Adesso decidiamo l'indirizzo: se lasciamo l'indirizzo vuoto sed opererà su tutte le righe in input. Se sappiamo che le righe contenenti le date da cambiare hanno una struttura particolare identificabile da un'espressione regolare possiamo usare quell'espressione come indirizzo, altrimenti affidiamoci al default.

L'ultima cosa da fare è decidere il flag: se vogliamo cambiare tutte le occorrenze che troviamo allora imposteremo il flag g, se sappiamo che le date da cambiare occorrono solo una volta per riga possiamo omettere i flag. Supponendo di voler cambiare tutte le occorrenze il nostro comando diventa:

sed 's/(0[1-9]|1[0-2]?|[2-9])\/(0?[1-9]|[1-2][0-9]|3[0-1])\/([0-9]{4})/pattern/g' nomefile
 
Questo comando legge il file indicato da nomefile, trova tutte le occorrenze della regex che gli abbiamo dato in pasto (notate come io abbia dovuto usare il backslash davanti agli slash per indicare a sed che la regex NON finiva lì) e stampa in standard output un testo che contiene la stringa pattern ogni volta che c'è stata un'occorrenza della regex.

Non male, ma adesso dobbiamo definire il nostro pattern di sostituzione. Ogni volta che sed incontra un gruppo crea una sotto-espressione e salva il risultato di quella sotto-espressione in un registro. Esistono 9 registri (numerati da 1 a 9, strano vero?) che possono essere usati nel pattern di sostituzione.

Nella nostra espressione il primo gruppo corrisponde al mese, il secondo al giorno e il terzo all'anno. Componiamo il nostro pattern invertendo i primi due e dovremmo aver finito:

sed 's/(0[1-9]|1[0-2]?|[2-9])\/(0?[1-9]|[1-2][0-9]|3[0-1])\/([0-9]{4})/\2\/\1\/\3/g' nomefile
 
Manca un ultima cosa: dobbiamo dire a sed che si tratta di un'espressione estesa (che fa uso dei quantificatori) tramite il flag di avvio -r:

sed -r 's/(0[1-9]|1[0-2]?|[2-9])\/(0?[1-9]|[1-2][0-9]|3[0-1])\/([0-9]{4})/\2\/\1\/\3/g' nomefile
 
sed può essere usato anche come se fosse grep tramite il flag -n che inibisce la copia dell'input non processato sullo standard output e il comando p che significa print, cioé stampa.

Ad esempio se volessimo stampare solo le righe che non cominciano con un # scriveremmo:

sed -n '/^[^#]/p' nomefile
 
Ovviamente grep ed egrep hanno più opzioni e consentono un controllo più fine sull'output.

Conclusioni

grep e sed consentono ad uno scripter di estendere la capacità di processamento dei file di testo della shell UNIX in modo considerevole grazie alla potenza delle espressioni regolari. Esistono però dei limiti: grep effettua solamente la ricerca (ma è molto veloce e può essere usato per filtrare solamente le parti interessanti dell'input), sed pur essendo Turing-equivalente (leggasi: in teoria ci si può scrivere qualsiasi programma che si può scrivere con un qualsiasi altro linguaggio di programmazione) non è molto comodo da utilizzare. L'utilizzo in script della shell consente di ovviare ad alcuni dei limiti della sintassi di sed ma genera un altro problema: la shell crea una marea di sottoprocessi (uno per ogni comando dato) e questo rallenta inevitabilmente l'esecuzione. Il linguaggio di sed inoltre ha memoria per una sola riga oltre a quella corrente e questo costringe a fare numerosi equilibrismi...

L'alternativa c'è, è molto potente ed ha alle spalle anni di sviluppo: si tratta del linguaggio di scripting perl. Purtroppo il perl è anche uno dei linguaggi più bizzarri e più ricchi di "cose strane" che vi possa capitare di incontrare. Fortunatamente per voi tutti i moderni (e anche alcuni meno moderni) linguaggi di scripting hanno un supporto più o meno complesso per le espressioni regolari: Tcl ce l'ha (ed è tra i più antichi), Python ce l'ha (tramite il modulo built-in re), PHP ce l'ha, Ruby ce l'ha, Javascript ce l'ha, Se ancora non foste convinti Java supporta le espressioni regolari tramite il package java.util.regex, per il C esistono le librerie PCRE che consentono di usare espressioni regolari compatibili con quelle del perl (il nome è infatti l'acronimo di "Perl Compatible Regular Expressions") oppure se intendete scrivere codice solo per sistemi POSIX-compatibili potete usare le regex POSIX (man 3 regex per maggiori info) infine per i fan del C++ oltre alle PCRE potete usare boost::regex delle librerie Boost.

Insomma non avete scuse per non usare le espressioni regolari quando si tratta di cercare degli schemi che si ripetono all'interno di flussi di testo!

Prima Parte
Seconda Parte