Il comando grep ricerca in un file di testo le righe che corrispondono ad un dato pattern. La sintassi del comando è la seguente:
grep [opzioni] pattern [nomefile]
Il pattern è una espressione regolare, nel caso più semplice, il pattern può essere una stringa senza caratteri speciali. Ad es., grep abc pippo.txt
visualizza le righe di pippo.txt
che contengono la stringa “abc
”
Se il parametro nomefile non è specificato, grep
legge da standard input, i.e., è possibile utilizzare grep
congiuntamente alla redirezione.
L’operazione di default eseguita da grep è la visualizzazione delle righe che corrispondono al pattern.
Alcune Opzioni:
Ricerca e visualizza le righe del file /etc/passwd
contenenti la stringa ‘root’
lso:~>grep root /etc/passwd
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin
Ricerca e visualizza le righe del file /etc/passwd
contenti la stringa ‘root’, indicando il numero di riga
lso:~>grep -n root /etc/passwd
1:root:x:0:0:root:/root:/bin/bash
12:operator:x:11:0:operator:/root:/sbin/nologin
Visualizza il numero di righe del file /etc/passwd
contenti la stringa ‘root’
lso:~>grep -c root /etc/passwd
2
Visualizza le righe del file /etc/passwd
che NON contengono la stringa ‘bash’.
Tra queste, ricerca e visualizza le stringhe che NON contentono la stringa ‘nologin’.
lso:~>grep -v bash /etc/passwd |grep -v nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
news:x:9:13:news:/etc/news:
Visualizza il numero di le righe del file /etc/passwd
contenti la stringa ‘account’.
lso:~>grep -c account /etc/passwd
0
L’output del comando precedente è 0 visto che nel file /etc/passwd
esiste una riga contenente la stringa ‘Account’. Difatti, utilizzando l’opzione -i che rende il comando grep case insensitive, l’output del comando cambia.
lso:~>grep -c -i account /etc/passwd
1
Si noti che è possibile ottenere lo stesso effetto componendo comandi diversi attraverso l’operatore shell “|” (pipe).
lso:~>grep -i account /etc/passwd |wc -l
1
Eliminando l’opzione -c possiamo visualizzare la stringa che corrisponde al pattern.
lso:~>grep -i account /etc/passwd
lso:x:501:501:LSO Account:/home/lso:/bin/bash
Una espressione regolare (in breve RE o regexp, acronimi di Regular Expression) è un stringa che descrive un insieme di stringhe.
L’elemento atomico delle espressioni regolari è il carattere.
Quasi tutti i caratteri possono essere visti come espressioni regolari elementari.
Esistono caratteri speciali, detti metacaratteri, che vengono utilizzati per descrivere operazioni tra/sui pattern. Ad esempio:
Partendo da espressioni regolari semplici è possibile ottenere espressioni regolari via via più complesse attraverso la loro composizione.
Le RE trovano ampia applicazione all’interno dei linguaggi di scripting per la ricerca e/o sostituzione di pattern all’interno di stringhe.
Esistono comandi come sed, awk e grep che interpretano le RE a tale scopo.
La shell bash, dalla sua versione 3, ha un interprete built-in per le RE.
Se un testo contiene almeno una delle stringhe descritte dalla RE allora diremo che il testo corrisponde al pattern.
Una RE è un insieme di caratteri contenente una o più occorrenze dei seguenti elementi:
Una RE può essere definita ricorsivamente a partire dalle seguenti regole:
Esistono due tipi di RE, le RE di base e le RE estese. L’unica differenza tra i due tipi di RE sta nel fatto che:
Alcune implementazioni consentono, però, di definire RE di base in cui i caratteri sopra elencati possono assumere il significato di metacarattere se preceduti dal carattere di escape “\”.
Ad esempio: la RE estesa “(a|b){2}
” equivale alla RE di base “\(a\|b\)\{2\}
“.
In questo caso, quindi, le RE di base ed estese sono, “equivalenti” dal punto di vista della espressività.
Attenzione: Alcuni caratteri vengono interpretati in modo diverso dalla shell bash e nella definizione di RE. Ad esempio, il carattere $:
Quando è necessario utilizzare RE all’interno di script shell (o anche direttamente da riga di comando) è sempre opportuno evitare che la shell interpreti i caratteri che definiscono la RE attraverso il quoting utilizzando apici singoli.
Il comando grep può interpretare sia RE estese che RE di base (comportamento di default per backward compatibility).
È possibile indicare al comando grep che la RE da interpretare è estesa utilizzando l’opzione -E o, in alternativa utilizzando il comando egrep.
L’esecuzione del primo comando genera un errore perché la shell interpreta il simbolo “|” come pipe e tenta di eseguire il comando “^r,*37″
lso:~>egrep ^r.*n$|^r.*37 /etc/passwd
-bash: ^r.*37: command not found
Proteggendo la RE tramite il quoting consente alla shell di invocare correttamente il comando egrep con argomento ^r.*n$|^r.*37
lso:~>egrep '^r.*n$|^r.*37' /etc/passwd
rpm:x:37:37::/var/lib/rpm:/bin/bash
rpc:x:32:32:Portmapper RPC user:/:/sbin/nologin
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
Il primo comando grep della coppia che segue non restituisce alcun risultato perché grep interpreta, per default, RE di base. Quindi il carattere “|” NON viene interpretato come operatore OR.
La seconda esecuzione di grep, invece, descrive correttamente la RE anteponendo a “|” il carattere di escape “\” che indica a grep di interpretare “|” come metacarattere.
lso:~>grep '^r.*n$|^r.*37' /etc/passwd
lso:~>grep '^r.*n$\|^r.*37' /etc/passwd
rpm:x:37:37::/var/lib/rpm:/bin/bash
rpc:x:32:32:Portmapper RPC user:/:/sbin/nologin
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
Definiamo di seguito il significato dei singoli operatori utilizzabili nella definizione delle RE. Utilizzeremo il carattere “E” per identificare una RE.
Un operatore di ripetizione consente, a partire da una espressione regolare E, di definire una nuova RE ottenuta replicando E più volte.
È possibile definire insiemi di caratteri da utilizzare per la costruzione di RE. Il modo più semplice è quello di elencare I caratteri dell’insieme, senza separatori, all’interno di parentesi quadre. Ad esempio:
L’utilizzo del carattere “-” consente di definire intervalli di caratteri, senza dover elencare tutti gli elementi dell’intervallo. Ad esempio:
Esistono classi di caratteri predefinite cui è possibile far riferimento utilizzando la scrittura [[:CLASS:]] dove CLASS è una delle seguenti stringhe: “alnum”, “alpha”, “ascii”, “blank”, “cntrl”, “digit”, “graph”, “lower”, “print”, “punct”, “space”, “upper”, “word” or “xdigit”. Ad esempio:
Nel caso degli insiemi di caratteri, il simbolo “^” assume il valore di negazione. Ad es. [^a-c] corrisponde a “tutti I caratteri tranne quelli appartenenti all’intervallo [a-c]“.
Come già accennato, è possibile comporre espressioni regolari mediante:
Se in una RE compaiono più operatori di composizione, vale il seguente ordine di precedenza:
È possibile ridefinire l’ordine di esecuzione degli operatori di composizione utilizzando le parentesi tonde.
lso:~>egrep '5|1:+' /etc/passwd
In questo esempio sono presenti l’operatore di ripetizione “+”, l’operazione di concatenazione delle RE “1″ e “:” e l’operatore “|”.
In base alle regole di precedenza, gli operatori vengono eseguiti in questo ordine:
La RE corrisponde quindi a tutte le stringhe che (a) contengono un “1″ seguito da almeno una occorrenza di “:”, I.e., 1:, 1::, 1::: … OPPURE (b) contengono “5″.
È possibile ridefinire l’ordine di esecuzione degli operatori utilizzando le parentesi ()
lso:~>egrep '(5|1):+' /etc/passwd
In questo caso, le parentesi indicano che l’OR deve essere eseguito prima degli altri operatori, mentre l’ordine degli altri operatori rimane inalterato.
La RE corrisponde quindi a tutte le stringhe che (a) contengono un “1″ seguito da almeno una occorrenza di “:”, I.e., 1:, 1::, 1::: … OPPURE (b) contengono “5″ seguito da almeno una occorrenza di “:”, I.e., 5:, 5::, 5::: …
lso:~>egrep '(501:){2}' file.txt
Ritorna tutte le righe del file file.txt che contengono due occorrenze consecutive della stringa 501:
lso:~>egrep '501:{2}' file.txt
Ritorna tutte le righe del file file.txt che contengono la (sola) stringa 501:: (due occorrenze di “:”)
lso:~>egrep '50(1:){2,}' file.txt
Ritorna tutte le righe del file file.txt che contengono la stringa 50 seguita da almeno due occorrenze consecutive della stringa 1:, I.e., 501:, 501:1:, 501:1:1:….
lso:~>egrep '\<[[:lower:]]' file.txt
Ritorna tutte le righe del file file.txt che contengono una parola che inizia per una lettera minuscola. Per (e)grep una “parola” è una stringa composta dai caratteri appartenenti alla classe alnum e/o dal carattere "_".
lso:~>egrep 'casa' file.txt
lso:~>egrep '\<casa\>' file.txt
La differenza tra questi due comandi sta nel fatto che il primo ritorna le righe che hanno la stringa casa coma sottostringa. Il secondo comando, invece, ritorna del righe del file che contengono a parola casa. Ad esempio il primo comando ritorna una stringa contenete la parola casale che verrebbe scartata dal secondo.
lso:~>egrep '^A(50(1:)+|2k*a){2}B$' /etc/passwd
In questo caso si noti che la presenza di parentesi modifica le priorità di esecuzione dei seguenti operatori:
Ritorna tutte le righe del file /etc/passwd
in cui:
Riassumendo, riportiamo di seguito alcuni esempi di righe che corrispondono alla RE descritta:
A501:1:501:1:B
A501:1:1:1:1:501:1:1:1:1:B
A2kka2kkaB
A2kkkkkkkkkka2kkkkkkkkkkaB
Si noti che, in genarale, la RE definisce una sottostringa di una riga. Nelle RE del tipo ^XYZ$ viene richiesto, invece, che l’intera riga corrisponda alla RE.
awk è una utility per l’interpretazione di uno specifico linguaggio di programmazione.
Il nome deriva dalle iniziali dei suoi progettisti, Alfred V. Aho, Peter J. Weinberger and Brian W. Kernighan, che lo progettarono nel 1977 nei laboratori Bell Labs dell AT&T.
Caratteristica principale di awk è la semplicità con cui è possibile analizzare e processare file di testo. Nello specifico, awk ricerca all’interno di file di testo le righe che corrispondono ad un dato pattern e, su queste righe, esegue una sequenza di istruzioni.
Vedremo che la semplicità di utilizzo deriva anche dal fatto che awk suddivide automaticamente la linea da analizzare in parole, consentendo l’accesso alla singola parola indipendentemente dalle altre.
Alcuni interpreti awk potrebbero mostrare comportamenti difformi dagli esempi riportati. L’interprete che utilizzeremo in questo corso è gawk, l’interprete di default utilizzato dai sistemi Linux.
Il linguaggio interpretato da awk è data-driven, i.e., uno o piu’ pattern descrivono i i dati su cui uno specifico codice verrà eseguito. Sintatticamente un programma awk ha la seguente forma:
BEGIN{ codice di “inizializzazione”; eseguito una sola volta prima dell’inizio dell’analisi dell’input}
pattern1{codice associato al pattern1}
pattern2{codice associato al pattern2}
…
END{ codice di “conclusione”; eseguito una sola volta dopo la terminazione dell’analisi dell’input}
È possibile scrivere script contenenti solo il blocco BEGIN. In questo caso l’interprete non attende input ed esegue una sola volta il codice all’interno del blocco.
Per eseguire un programma awk è possibile utilizzare una delle seguenti possibilità:
L’interprete awk consente di scrivere il programma direttamente su riga su riga di comando utilizzando la seguente sintassi:
awk 'programma' inputfile1 inputfile2...
awk '{print $1}' file1.txt
Nella maggior parte dei casi, è opportuno scrivere il programma in un file di testo ed indicare all’interprete il nome del file.
awk -f nomefile inputfile1 inputfile2...
Come per gli script shell, è possibile rendere uno script awk “eseguibile” utilizzando i seguenti accorgimenti:
#!/usr/bin/awk -f
/usr/bin/awk
, è seguito dall’opzione “-f” che indica all’interprete che il programma è contenuto in un file.Se i nomi dei file di input sono omessi, awk legge l’input da standard input. È possibile, quindi, utilizzare la redirezione, ad esempio, le seguenti invocazioni sono equivalenti
awk 'programma' file.txt
awk 'programma' < file.txt
cat file.txt | awk 'programma'
L’esecuzione di uno script awk può essere schematizzata come segue. L’interprete:
L’esecuzione termina quando non esistono più record da analizzare.
Un pattern è una espressione regolare racchiusa tra due simboli ‘/’.
I pattern vengono, in genere, utilizzati per filtrare i record da processare anteponendo la descrizione della RE al blocco di codice.
In questo caso, l’intero record viene esaminato per verificare se corrisponde o meno al pattern.
Il seguente esempio di script awk: Mostra codice descrive uno script awk che processa I record attraverso due blocchi di codice.
Si noti che:
È possibile verificare pattern anche all’interno del codice. A tal fine, awk mette a disposizione l’operatore booleano ~ (tilde) la cui sintassi è la seguente:
exp ~ /regexp/
L’operatore ritorna “vero” se l’espressione exp corrisponde al pattern regexp.
Esempi di utilizzo dell’operatore ~: Mostra codice
L'esempio riporta tre esempi equivalenti di programmi awk.
Il primo esempio riporta l'esecuzione dell'interprete da riga di comando. Il programma controlla se il record corrisponde al pattern. In assenza del codice associato ad un pattern, l'interprete visualizza semplicemente il record.
script1.awk contiene semplicemente l'indicazione del pattern.
In script2.awk, le istruzioni nell'unico blocco di codice vengono eseguite su tutti I record ma il codice contiene un test per verificare se il record corrisponde al pattern. In questo caso, quindi, il filtro viene eseguito dal codice del programma.
Awk possiede anche un operatore di negazione "!" che può essere utilizzato in congiunzione con l'operatore tilde.
Gli operatori ~ e ! Possono essere utilizzati per la definizione di pattern o nei costrutti if, for, while do che vedremo.
Esempi di utilizzo dell'operatore !: Mostra codice
Come per la shell, l’interprete awk mantiene una serie di variabili built-in a cui il programma può accedere. Di seguito ne riportiamo alcune.
FILENAME: Abbiamo già accennato che l’interprete awk legge l’input dai file indicati all’atto della sua invocazione o da standard input. Più precisamente, l’interprete legge una riga per volta dal file. Se esistono più file di input, l’interprete analizza i file nell’ordine in cui sono presentati sulla riga di comando. Il nome del file che l’interprete sta correntemente utilizzando e’ memorizzato nella variabile built-in FILENAME.
FNR, NR: L’interprete awk mantiene il numero di record processati per il file corrente nella variabile FNR. Inoltre memorizza anche il numero totale di record processati dal processo corrente nella variabile NR. Chiaramente le due variabili possono assumere valori diversi se il numero di file in input allo script è almeno due.
RS: Il carattere utilizzato come separatore tra due record è memorizzato nella variabile RS (Record Separator). Il valore di default per RS è newline (quindi un record corrisponde ad una linea nel file (di testo) in input. L’utente può assegnare alla variabile RS una qualsiasi espressione regolare.
NF: Ogni record letto viene automaticamente suddiviso in campi (fields). Questa variabile built-in contiene il numero di campi nel record corrente.
FS: Il separatore tra i campi è contenuto nella variabile FS (Field Separator), il cui valore di default è lo spazio. Quindi l’interprete suddivide la riga in un numero di campi pari al numero di “parole” presenti sulla riga. È possibile assegnare ad FS una qualsiasi RE.
Per accedere ai valori dei singoli campi è necessario utilizzare il metacarattere $ seguito dall’indice del campo. Ad esempio $3 corrisponde al valore del terzo campo del record corrente. In awk è possibile far riferimento a campi la cui posizione non è costante, ad esempio $(NF-1) indica il penultimo campi del record corrente. Si noti che NF NON è una costante ed NF-1 è, in realtà, la valutazione di una espressione aritmetica.
In awk è possibile modificare il valore dei campi all’interno di un blocco di codice.
Il seguente script: Mostra codice riporta un semplice esempio di utilizzo delle variabili built-in.
Si noti che, a differenza degli script shell, l'accesso al valore delle variabile NON richiede l'utilizzo del metacarattere $, se non nel caso dell'accesso ai campi del record.
La prima riga visualizza le variabile NR e FNR che memorizzano il numero di record processati, rispettivamente, dall'inizio dello script e per il file corrente.
La seconda riga visualizza il nome del file corrente.
La terza riga visualizza il numero di campi nel record corrente. Vengono quindi visualizzati l'intero record, il primo e l'ultimo campo.
Si noti che l'accesso all'ultimo campo è effettuato attraverso un indirizzamento indiretto attraverso il valore della variabile NF.
Come detto le variabili built-in possono essere modificate. Difatti lo script appena visto modifica il valore della variabile $1 (il primo campo del record) per poi visualizzarne il nuovo valore. Chiaramente, in questo esempio, si assume che il primo campo sia numerico.
Attenzione: La modifica di una variabile built-in permane fino all’eventuale aggiornamento della stessa da parte dell’interprete.
Si supponga di eseguire lo script nella figura in alto con input i due file nella figura in basso.
Nel seguente esempio: Mostra codice le variabili FNR e FILENAME vengono modificate dall'utente quando NR<2. Quindi:
Dal terzo record in poi, quindi, le modifiche fatte dall'utente vengono perse.
Si noti che le modifiche fatte alle variabili contenenti i campi vengono perse quando l'interprete legge il record successivo.
I file di input: Mostra codice
Il seguente esempio: Mostra codice mostra le possibilità offerte dalla modifica dei separatori di record e di campi.
Lo script modifica il separatore di record di default assegnando alla variabile RS l'espressione regolare ;|\n. Questa assegnazione indica all'interprete che la fine di un record è identificata dal carattere newline (\n) o dal carattere ";".
Allo stesso tempo, al separatore dei campi FS viene assegnata l'espressione regolare /|+ che indica all'interprete che la fine di un campo è identificata dal carattere "/" o dal carattere "+".
Queste modifiche consentono all'interprete di analizzare il file "File.txt" riportato nella figura in basso, suddividendolo nel seguenti 5 record:
L'output atteso dall'esecuzione dello script è riportato di seguito:Mostra codice
Abbiamo già utilizzato lo statement print per la visualizzazione di messaggi. Questo statement può essere utilizzato per visualizzare qualsiasi tipo di valore ma NON consente di controllare il modo in cui le informazioni vengono visualizzate. Awk fornisce anche uno statement printf che consente di descrivere la formattazione dell’output.
Lo statement print può essere usato nei seguenti modi:
Il separatore di default tra due item visualizzati da print è lo spazio. In realtà è possibile indicare all’interprete la stringa da utilizzare assegnando un valore alla variabile OFS (Output Field Separator).
Allo stesso modo, l’output di ogni comando print viene terminato dal valore della variabile ORS (Output Record Separator), il cui valore di default è il carattere newline. È possibile indicare a print il formato in cui visualizzare i valori numerici assegnando opportunamente la variabile OFMT, ad es., OFMT=”%3d”.
Come è semplice intuire, la sintassi della variabile OFMT è la stessa del parametro “format” utilizzato nel linguaggio C per la funzione print.
Awk fornisce una seconda funzione di visualizzazione, printf, che consente di definire esplicitamente la formattazione dell’output.
La sintassi di printf è la seguente:
printf formato, item1, item2, item3...
Anche in questo caso il formato ha la stessa sintassi del parametro format della funzione printf del linguaggio C.
Come negli script shell, l’interprete awk consente la redirezione dell’output di print e printf.
La sintassi è esattamente la stessa della shell bash. In particolare:
print item1,... > nomefile
L’interprete scrive l’output di print nel file nomefile. Se il file esiste, il suo contenuto viene cancellato, altrimenti il file viene creato. Nomefile può essere una qualsiasi espressione. Prima della redirezione l’interprete converte l’espressione in una stringa che utilizza come nome del file su cui scrivere.
print item1,... >> nomefile
L’interprete accoda l’output di print al contenuto del file nomefile.
print item1,... | comando
È possibile inviare l’output di print ad un comando tramite una pipe.
Il seguente script: Mostra codice, è un esempio di utilizzo degli statement print e printf. L'esempio: Mostra codice, riporta l'output solo per il primo record.
Lo script modifica le variabili ORS ed OFS come segue:
È possibile concatenare stringhe e valori, per giustapposizione. Nell'esempio, la concatenazione della stringa costate "Il record" e del valore della variabile NR è ottenuta utilizzando la sintassi "Il record " NR.
Si noti che lo spazio tra "il record" ed NR può essere omesso, I.e, le scritture '"Il record " NR' e '"Il record "NR' sono equivalenti.
L'invocazione della printf assume la definizione del formato da utilizzare per i parametri tramite la stessa sintassi del linguaggio C.
Si noti, infine, che le variabili ORS ed OFS hanno effetto SOLO sullo statement print e NON su printf.
Questo, in realtà, non è una limitazione visto che è possibile ridefinire, in ogni invocazione di printf, quali siano i separatori tra gli item e il "fine riga".
Il seguente script: Mostra codice, è una variante dello script appena visto che utilizza la redirezione dell'output.
Lo script crea un file per ogni record nel file. Il nome del file è calcolato attraverso l'espressione ("outfile_"NR), i.e., l'ouput del record i-esimo verrà scritto nel file outfile_i
Le successive invocazioni di print e printf redirigono l'output sul file attraverso gli operatori > e >>. In particolare:
Un esempio di input con relativo output generato dallo script: Mostra codice
1. Introduzione ai sistemi Unix
2. Principi di programmazione Shell
3. Esercitazioni su shell scripting - parte prima
5. Esercitazioni su shell scripting - parte seconda
6. Espressioni Regolari ed Introduzione ad AWK
7. Esercitazioni su espressioni regolari ed awk scripting
9. Esercitazioni su awk scripting - parte seconda
10. Programmazione in linguaggio C: Input/Output di basso livello
11. Esercitazioni su I/O di basso livello
12. Interazione con file di sistema e variabili d'ambiente
13. Esercitazioni sulla gestione dei file di sistema e le variabili...
14. System call per la gestione di file e directory
15. Esercitazioni su gestione file e directory
16. La programmazione multi-processo
17. Esercitazioni su programmazione multi-processo
18. I Segnali
20. I Socket
Bash guide for beginners: Capitolo 4
Advanced Bash Scripting: Capitolo 17
GAWK: Effective Awk Programming: Capitoli 1, 2 (par. 1,6), 3 (par. 1-5) 4 (par. 1-6)