Manipolare flussi di testo
In molti esempi sui comandi presentati, si è visto che questi
generano (oppure accettano in input) dei flussi di testo. Si pensi
ad esempio al comando cat
che permette di stampare il
contenuto di un file (o di concatere il contenuto di più file), o
al comando grep
che invece permette di filtrarlo cercando
alcune righe.
Questi esempi non sono un caso isolato. La maggior parte dei comandi sui sistemi Unix è pensato per poter prendere in input del testo semplice. In questo esercizio vedremo alcuni esempi significativi, che permetteranno poi di affrontare altri esercizi più complessi.
Sostituire il testo
Prima di iniziare, ricordiamo che il comando echo
permette di stampare del testo, e che possiamo usare l'output
di un comando come input per un altro attraverso il comando
|
. Ad esempio, se volessimo dare l'output di
echo
in pasto a grep
, useremmo la seguente
sintassi:
$ echo test | grep provache non ritornerebbe nulla, perché la stringa "prova" non viene trovata all'interno della stringa "test".
Consideriamo ora un task relativamente semplice, quello della
sostituzione di caratteri. A questo scopo, possiamo usare il comando
tr
. Questo prende come input due argomenti, un carattere
da cercare, e quello da sostituire a tutte le occorrenze trovate.
L'input viene letto da stdin, e stampato sullo stdout.
/home/f.durastante/dante.txt
, e si produca un file dal
nome dante-xxx.txt
dove tutte le vocali siano state
sostituite da una x
.
tr
può essere usato anche per eliminare dei
caratteri dal flusso di testo, usando il flag -d
.
dante-xxxn.txt
, in cui sono
stati rimossi tutti i caratteri per andare a capo, che si possono indicare
con '\n'
.
Il comando sed
Il comando tr
è molto efficiente, ma è adatto
solo alle operazioni più semplici. Supponiamo di voler riscrivere
la storia raccontata da Dante, e di voler rinominare Beatrice
con il nome di Giovanna. Ottenere questo effetto con
tr
sarebbe impossibile.
Il comando sed
(stream editor) permette di manipolare
un flusso di testo, effettuando varie operazioni, fra cui delle
sostituzioni.
Questo comando utilizza dei sottocomandi; ad esempio, per
sostituire la stringa "test" con la stringa "prova" dovremmo usare
le linea:
$ sed "s/test/prova/"dove
s
indica il comando "sostituzione", mentre /
viene usato per separare gli argomenti del comando. Si sarebbe potuto
utilizzare un qualsiasi altri simbolo, ad esempio il comando
$ sed "s|test|prova|"sarebbe stato completamente equivalente. Si può scegliere di volta in volta il simbolo da usare in modo da evitare che appaia nelle stringhe da sostituire o da rimpiazzare.
sed
per generare un
file dante-2.0.txt
con Beatrice sostituita da
Giovanna. Si osservi che la stringa "Beatrice" appare solo al più
una volta per riga; se così non fosse stato, sed avrebbe sosituito
solo la prima occorrenza. Per sostituirle tutte si sarebbe
aggiungere il flag "g" in fondo al comando, ad esempio
sed "s/test/prova/g"
. Si noti che anche in assenza
della g
lo slash finale va lasciato.
\(
e \)
per evitare che vengano interpretate
da bash), e riusare questi gruppi indicandoli con \1, ..., \9
nella stringa da sostituire.
Consideriamo un esempio. Supponiamo di aver scritto del codice
in un linguaggio di programmazione dove abbiamo usato più volte l'istruzione
a + b + x
, dove a
e b
possono essere numeri qualunque,
e x
è una variabile. Per qualche ragione, vorremmo trasformare queste
istruzioni in b - a + x
. Possiamo matchare questo input con l'espressione
regolare [0-9] + [0-9] + x
. Per preservare i valori di a,b
,
dobbiamo creare dei gruppi accanto a loro, e fare la sostituzione. Ad esempio:
$ echo "2 + 3 + x" | sed "s/\([0-9]\) + \([0-9]\) + x/\2 - \1 + x/" 3 - 2 + x
sed
per trovare tutte le occorrenze
della forma WWWWW Virgilio
, dove WWWWW
rappresenta
una qualunque parola, e se ne scambi l'ordine nel testo, trasformandole
in Virgilio WWWWW
.
Affettare il testo
Consideriamo ora un altro problema comunque: ci viene data una stringa composta da più parti, che vogliamo separare. Ad esempio, ipotizziamo di avere un file contenente delle date nella forma:
19-10-2020 19-04-2023 4-12-1987 21-10-2020 23-01-2021 24-04-2021 28-08-2022Supponiamo di voler estrarre gli la lista degli anni presenti nel file. Un primo passo sarebbe quello di eliminare le parti che non ci interessano, ovver le date e i mesi. A questo scopo, possiamo usare il comando
cut
che, dato un delimitatore, spezzetta ogni riga in base
al numero di occorrenze del delimitatore.
Per capire la sintassi del comando cut
, supponiamo di
voler separare i campi di un file input.txt
usando il
simbolo :
, e selezionando la terza colonna. Potremmo
scrivere:
$ cut -d ':' -f3 < input.txtdove
-d
seleziona il delimitatore, e -fN
l'N-esima
colonna. Osserviamo che si è usato la redirezione di input.txt
come input usando l'operatore <
, ma si sarebbe potuto usare
cat
combinato con |
, o altro.
date.txt
con il testo riportato sopra.
Si utilizzi il comando cut
per estrarre solamente gli
anni.
Riordinare le righe
Sebbene l'output del comando precedente possa essere soddisfacente, alcuni anni vengono ripetuti più volte, perché esistevano varie date all'interno di quell'anno. È abbastanza chiaro che questo comportamento potrebbe essere fastidioso su una lista più lunga. Inoltre, gli anni non sono in ordine cronologico, ma vengono riportati come sono trovati nel file.
Per risolvere questi due problemi ci possiamo affidare a due programmi:
sort
e uniq
. Il loro nome descrive abbastanza
bene il loro comportamento.
sort
e uniq
per riordinare gli
anni ed eliminare i doppioni. Si ordino gli anni a partire dal più grande
al più piccolo (si legga il manuale di sort
per vedere
come fare).
Awk
Sulla maggior parte dei sistemi Unix è a disposizione un programma, di
nome awk
, che permette di manipolare facilmente dati in
forma tabellare.
Si crei un file con il seguente contenuto, e lo si
chiami mailing_list.txt
:
# Nome Email Telefono Età Mario Rossi rossi@tin.it +39 347 1276354 30 Giovanni Verdi verdi@gmail.com +39 348 7162451 46 Luca Tonelli tonelli@dm.unipi.it +39 050 139232 12 Leonardo Fibonacci fibo@outlook.com +39 050 563821 65 Tommaso Ligabue ligabue@live.com +39 348 9985634 56Attenzione: i campi nome, e-mail, telefono sono separati da tabulazioni (TAB), e non da spazi.
Il comando awk
permette di definire un'azione da
effettuare per ogni riga che corrisponde ad una data espressione
regolare. Come vedremo, ci sono anche azioni speciali che si possono
definire perché vengano eseguite prima e/o dopo la lettura del file.
La sintassi base del comando è:
$ awk '/pattern/ { action }'dove
pattern
è l'espressione regolare da cercare, e
action
specifica l'azione da fare. L'utilizzo di
/pattern/
è opzionale: se non specificato, il comando
eseguirà l'azione su tutte le righe. In maniera analoga agli altri
comandi finora considerati, è possibile specificare un file da cui
leggere il testo, oppure non specificare nulla ed in quel caso verrà
letto dallo stdin.
$ awk '{ print $0 }' mailing_list.txtche chiede di stampare ogni riga (
$0
è un riferimento
speciale alla riga che viene considerata in un dato momento). Il comportamento
dovrebbe essere analogo a cat
solo più complicato da scrivere.
Si modifichi poi l'esempio per stampare solo le righe che iniziano
per L
; questo richiederà di scrivere un'espressione regolare.
La caratteristica più utile di awk
è che permette di
riferirsi alla singole colonne del testo. Ad esempio, supponiamo di
voler estrarre solo gli indirizzi e-mail. Potremmo allora utilizzare
il comando { print $3 }
per stamparli. La variabile
$3
si riferisce alla terza colonna. In questo esempio
awk
separa l'input ogni volta che trova uno spazio o
una tabulazione, e dunque considera separatamente nome e cognome.
awk
che permetta di stampare
gli indirizzo e-mail, escludendo però la prima riga che inizia
con un #
(è necessario scrivere un'opportuna espressione
regolare per escluderla).
awk
separi i campi con gli spazi in questo
esempio è un po' fastidioso: cosa succederebbe se avessimo un nome
senza cognome, oppure con un cognome composto? Per fortuna, possiamo
concincere awk
a considerare solo le tabulazioni come
separatore dell'input, usando il flag -F
.
awk
è in grado di riconoscere gli input numerici, ed
effettuare operazioni aritmetiche. Per queste è spesso utile specificare
più regole, ed in particolare due regole speciali che vengono eseguite
prima di iniziare (BEGIN
) e alla fine END
.
Possiamo considerare il seguente esempio per sommare tutte le età contenuto nel file:
$ awk -F '\t' 'BEGIN { x = 0 } { x += $4 } END { print x }' mailing_list.txtIl pattern
BEGIN
viene usato per
inizializzare la variabile x
, mentre END
viene usato per stampare il risultato alla fine.