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.
Nella migliore tradizione UNIX
Facciamo subito un esempio concreto: vogliamo sapere qual è il MAC address di un'interfaccia di rete. Il comando
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 (
Abbiamo la regex e abbiamo il nostro input, passiamo tutto attraverso
Ci sono diversi usi possibili di questo one-liner:
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
In
Come si fa ad automatizzare questo compito con
La prima novità sono i delimitatori di inizio e fine riga (rispettivamente
La seconda novità è meno eclatante: il simbolo
Se siete tra coloro che utilizzano il
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
La
I flags modificano il comportamento del comando, ad esempio
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:
Adesso decidiamo l'indirizzo: se lasciamo l'indirizzo vuoto
L'ultima cosa da fare è decidere il flag: se vogliamo cambiare tutte le occorrenze che troviamo allora imposteremo il flag
Non male, ma adesso dobbiamo definire il nostro pattern di sostituzione. Ogni volta che
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:
Ad esempio se volessimo stampare solo le righe che non cominciano con un
L'alternativa c'è, è molto potente ed ha alle spalle anni di sviluppo: si tratta del linguaggio di scripting
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
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à nominatogrep
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 diex
(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.
#!/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:- Per indicare un pattern che indichi la riga su cui agire.
- Per indicare un pattern che indichi uno schema di sostituzione.
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