<- PW: Gnome - Archivio Generale - Copertina - PW: Gosh -> |
PlutoWare
L'articoloUna introduzione al programma di shell e linguaggio di programmazione Bash, versione 2. Ci concentreremo sugli aspetti che fanno di Bash non solo uno shell di sistema, ma anche un potente linguaggio di programmazione di uso generale. A completamento della esposizione, vedremo due utili programmi per la gestione dei pacchetti RPM e per il download della posta elettronica via protocollo POP-3, naturalmente realizzati completamente in Bash. |
Bash è il programma shell più comunemente utilizzato nel sistema operativo GNU/Linux. Esso discende dal classico sh, ma ne integra le funzionalità e lo estende nella direzione di un vero e proprio linguaggio di programmazione. Con la versione 2, Bash mette a disposizione un set completo di istruzioni per il controllo del flusso di esecuzione, la ricorsione, gli array, le variabili di riferimento, il supporto alla manipolazione di stringhe, alla gestione dei segnali, il controllo dei processi e dei jobs, le tecniche di comunicazione tra processi, i socket Internet. Nato come linguaggio di scripting, cioè come una sorta di collante rispetto a programmi esterni scritti tipicamente nel più efficiente linguaggio C, Bash oggi mette a disposizione uno spettro di funzionalità sufficientemente ampio da poter realizzare anche programmi di una certa complessità.
Questo articolo è rivolto a coloro che hanno già
dimestichezza con i concetti della programmazione e che desiderano
conoscere più da vicino questo potente shell di sistema.
Alcune funzionalità presentate sono disponibili solo nella
versione 2. Ad esempio, la distribuzione Red Hat 7 fornisce questa
shell per default, mentre nella Red Hat 6.2 e precedenti fornivano sia
il vecchio bash
(default) che bash2
.
Il sistema operativo Unix, e tutti gli altri sistemi operativi che ad esso si ispirano, mettono a disposizione del programmatore una serie di strumenti per interfacciare i suoi programmi al resto del sistema: parametri, variabili d'ambiente, file, segnali, processi. Qualsiasi linguaggio di programmazione o sistema di sviluppo che debba vivere dentro a questo sistema deve in qualche modo implementare questi strumenti. Uno dei fili conduttori della mia esposizione sarà proprio quello di passare in rassegna le soluzioni adottate da Bash per integrarsi nell'ambiente Unix.
Tra i linguaggi di programmazione, Bash e gli shell in generale occupano un posto un po' particolare: diversamente da altri linguaggi di programmazione nati magari a tavolino, per il Bash sembra non esistere una definizione precisa e formale della sintassi. Ci sono due motivi per questo: il primo è che il Bash, come molti altri linguaggi di scripting, fa largo uso della sostituzione di stringhe durante il processo di interpretazione, per cui la sintassi del linguaggio assume una certa dipendenza temporale difficile da riprodurre con grafici sintattici o metalinguaggi. Il secondo motivo è di tipo storico: determinate scelte implementative più o meno felici sono legate alla natura del Bash come shell di sistema, e non come linguaggio di programmazione. La motivazione storica sarà il filo conduttore principale nella prima parte dell'articolo, dove vedremo i principi fondamentali di Bash come shell di sistema. Successivamente affronteremo gli aspetti più prettamente legati alla programmazione, e allora proverò ad introdurre un abbozzo di sintassi formale.
Ai tempi dei sistemi ad elaborazione batch, l'utente si presentava nella sala terminale con in mano un pacco di qualche centinaio di schede perforate: una "mazzetta" iniziale conteneva il programma e le istruzioni di esecuzione, e il resto del malloppo conteneva i dati da elaborare. L'utente consegnava il tutto ad un tecnico in camice bianco, il quale inseriva il pacco nel lettore di schede. Non appena la CPU del calcolatore era disponibile, partiva la lettura delle schede, cui seguiva l'elaborazione e la stampa dei risultati su carta in modulo continuo. L'utente poteva passare alcune ore dopo a ritirare l'elaborato.
L'avvento dei terminali seriali e dei sistemi multiutente come Unix, ha permesso di ridisegnare questa procedura rigida introducendo il paradigma della linea di comando. In effetti l'interfaccia terminale a linea di comando riproduce il concetto di scheda perforata: la telescrivente o il videoterminale del computer sono organizzati in righe (corrispondenti alle schede) costituite tipicamente da 80 colonne (esattamente come le schede). L'operatore compone la riga di comando come se si trovasse davanti ad un perforatore di schede, ma con la differenza che, una volta premuto un apposito bottone, la riga di comando va immediatamente al calcolatore, e sullo schermo del videoterminale compare subito l'output risultante.
Pur nella sua semplicità, l'interfaccia a riga di comando richiede comunque una qualche formalizzazione, e richiede anche un programma che la implementi: questo programma viene chiamato shell (guscio). Nel caso del sistema operativo Unix è risultato naturale strutturare la riga di comando indicando prima il nome di un programma e poi, a seguire, gli eventuali suoi parametri. Il nome del programma e i parametri, per essere distinguibili, vanno separati l'uno dall'altro con almeno un carattere di spazio o di tabulazione (HT):
programma parametro1 parametro2 parametro3 |
Questa semplice sintassi è facile da imparare, è veloce
da battere sulla tastiera, ed in generale si è dimostrata molto
pratica. Sorgono tuttavia spontanee alcune domande: come è
possibile includere caratteri di spazio o tabulazione all'interno dei
parametri? I programmi di shell, incluso Bash, risolvono il problema
introducendo il carattere di doppio apice " per delimitare i parametri
dove si vogliono inserire degli spazi.
Sfortunatamente, con questa scelta
il carattere di doppio apice diventa riservato: com'è possibile
inserirlo dentro a un parametro? Per risolvere anche questo problema,
i programmi di shell adottano la classica soluzione della sequenza di
escape: un carattere di doppio apice che sia preceduto dal carattere di
backslash \
conserva il suo significato letterale, e non
viene interpretato come delimitatore.
Sfortunatamente, il carattere
di backslash diventa a sua volta un carattere riservato: come fare ad
introdurre un backslash dentro a un parametro? La risposta è
che bisogna far precedere il backslash da un secondo backslash. A questo
punto il problema si direbbe definitivamente risolto.
Una volta che l'operatore ha composto correttamente il nome del programma e i suoi eventuali parametri, dovrà premere l'apposito tasto "Invio" (o "Return", o "Enter") per inviare la riga allo shell e dare così inizio alla sua esecuzione.
Lo shell separa tra loro il nome del programma e i parametri, individua
il programma in questione nel file system, e lo avvia passandogli i
parametri inseriti. Individuare il programma in questione non è
un problema del tutto banale. Innanzitutto esistono almeno due modi per
scrivere il nome di un programma: si può indicare direttamente
il pathfile assoluto o relativo, oppure si può indicare solo il
suo nome.
Nel caso di un pathfile assoluto o relativo, lo
shell può individuare immediatamente il programma richiesto.
Quando invece il comando dato per lanciare il programma non contiene alcun pathfile (cioè
non contiene almeno uno slash /
) allora lo shell ricerca il
file del programma esaminando in sequenza tutte le directory elencate
nella variabile d'ambiente PATH. Dentro a PATH ci dovrebbe essere
infatti una lista di directory separate da un :
(due punti).
Per ovvi motivi di efficienza, una volta che Bash ha individuato la dir.
dove risiede un certo comando, la ricorda in una sua cache interna,
in modo da velocizzare successive ricerche.
Ogni bravo utente di sistema Unix costruisce nel tempo un certo numero di
propri comandi di utilità, magari sviluppati proprio con Bash,
oppure in C, o altro ancora. Dove mettere questi comandi in modo che
siano direttamente raggiungibili? La soluzione migliore è creare
una directory bin
nella propria home directory, e quindi
inserire questa linea nel file .bash_profile
:
export PATH="$HOME/bin:$PATH" |
In questo modo Bash cercherà i comandi anche nella directory degli eseguibili personali.
Esiste in realtà un'altra categoria di programmi, che sono i comandi
interni dello shell: comandi come echo
e kill
non sono realmente dei programmi esterni, ma vengono eseguiti internamente
da Bash per motivi di efficienza (il primo) e di sicurezza (il secondo).
Come è facile immaginare, è sempre preferibile usare i
comandi interni allo shell che avviare programmi esterni, qualora Bash
offra una conveniente alternativa.
Bash manipola essenzialmente stringhe, e dunque le variabili conterranno stringhe (c'è solo una eccezione per i numeri interi). I nomi di variabile possono essere costituiti da lettere, cifre e underscore, ma il primo carattere deve essere una lettera o underscore. Bash è sensibile alla differenza tra lettere maiuscole e lettere minuscole. Esempi di assegnamenti:
HOST_NAME="localhost.localdomain" PORT=110 messaggio="Ciao, il mio nome e' Giovanni, ma tutti mi chiamano \"Vanni\"." |
Notare che a sinistra e a destra del carattere di = non ci sono spazi, altrimenti lo shell tenterebbe di interpretare il nome della variabile come programma e il carattere di = come primo parametro... in effetti, i nomi dei programmi non possono contenere il carattere di =.
Siccome i nomi di variabile si confondono come normali stringhe, ecco che
sorge la necessità di designare in qualche modo che una certa
stringa è il nome di una variabile: a questo scopo serve il carattere
$
, per cui ogni volta che vorremo il valore delle variabili
definite prima dovremo anteporre questo carattere:
echo $HOST_NAME $PORT $messaggio |
Ne segue che anche $
diventa un carattere riservato, e quando
vorremo inserire questo carattere col suo significato letterale dovremo
farlo precedere dal solito backslash.
L'accesso alle variabili d'ambiente in Bash è estremamente semplice: il loro valore si ottiene esattamente come se fossero variabili normali. Invece, per impostare una variabile d'ambiente in modo che venga ereditata dai sottoprocessi, è necessario esportarla:
export DATI=/tmp/dati.txt |
Il comando env
fornisce l'elenco delle variabili di ambiente
correntemente definite.
La sintassi del Bash può essere a volte piuttosto sibillina per il neofita. Nel tentativo di chiarire la differenza tra comandi, pipeline e liste di comandi, tenterò una descrizione formale di questi oggetti, nella speranza che possa contribuire a chiarire le cose. E' una operazione mai tentata prima, forse è impossibile, ma io ci provo lo stesso!
Un comando si compone di un nome di comando, seguito dagli eventuali parametri, ed eventualmente terminato da un operatore di controllo:
comando ::= nomecomando { parametro } [ ctrl ]
Solo il nome del comando è obbligatorio, mentre gli altri elementi sono
opzionali. I nomi di comando possono essere comandi interni al Bash (come
echo
), funzioni, o programmi esterni (come tar
).
Una pipeline è una sequenza di comandi separati dalla barra verticale. Ogni comando viene eseguito in un nuovo processo, e l'output di ogni comando diventa l'input del successivo. L'output finale va su stdout, a meno che non si usino opportuni operatori di controllo:
pipeline ::= [!] comando { | comando }
Il codice di stato risultante è quello ritornato dall'ultimo comando. Il punto esclamativo iniziale permette di negare il codice di stato, invertendo la condizione di successo / fallimento.
Una lista di comandi è una successione di pipeline unita da
dall'operatore ;
, dall'operatore &
(esecuzione
in background) oppure dagli operatori logici &&
(AND) e ||
(OR):
operatore ::= { ";" | "&" | "&&" | "||" } lista ::= pipeline { operatore pipeline } [ ";" | "&" ]
Un gruppo è una sequenza di comandi, anche su più linee, racchiusi tra parentesi graffe. Tutti i comandi inclusi condividono l'eventuale redirezione dell'I/O:
gruppo ::= "{" lista ";" "}" [ctrl]
Una sottoshell viene generata inserendo una lista di comandi racchiusi tra parentesi tonde, anche disposti su più righe:
sottoshell ::= "(" lista ")" [ctrl]
Le istruzioni semplici comprendono le istruzioni per il controllo di flusso e le chiamate di funzione, che vedremo nei prossimi paragrafi.
In definitiva, una istruzione Bash può assumere una delle seguenti forme:
istruzione ::= lista | gruppo | sottoshell
Terminata questa faticaccia, il prossimo paragrafo sulle istruzioni di controllo dovrebbe essere finalmente più chiaro.
Bash dispone di un set completo di istruzioni per il controllo del flusso, ormai tradizionali in tutti i linguaggi di programmazione. Quella più articolata è l'istruzione condizionale:
if lista then istruzioni elif lista then istruzioni else istruzioni fi |
Naturalmente, non è necessario specificare tutti i rami
alternativi, ma qui ho voluto illustrare la struttura generale
dell'istruzione. Notare che ad ogni comando if
deve
corrispondere il comando di chiusura fi
. Io
trovo molto pratico sfruttare il punto-e-virgola per rendere più
compatto il sorgente, e porto il comando then
nella riga
di sopra:
if lista; then istruzioni elif lista; then istruzioni else istruzioni fi |
Le espressioni logiche si basano sul codice di stato ritornato dalle liste di comandi: se il comando ha successo (cioè ritorna il codice di stato zero) la condizione è vera, altrimenti è falsa. Ricordiamo che la lista può contenere l'operatore di negazione. Ad esempio, un modo corretto per cancellare un file potrebbe essere questo:
if ! rm $FILE_NAME; then echo "Cancellazione del file $FILE_NAME fallita." exit 1 fi |
Il tradizionale comando test
(cfr. man test
)
può essere sostituito con il corrispondente comando interno
di Bash "[
" (parentesi quadra aperta), con notevole miglioramento
della efficienza. Col comando di test sono possibili una serie di
espressioni logiche per il confronto di stringhe e la verifica della
accessibilità dei files. Un bel help test | less
fornisce il quadro completo delle possibilità offerte da questo
comando. Trattandosi di un comando, sebbene dal nome un po' particolare,
lo si può usare sia all'interno della istruzioni if
,
sia da solo. Per eseguire il remove di un file
previa verifica della sua esistenza possiamo scrivere:
if [ -f "$FILE_NAME" ]; then rm $FILE_NAME fi |
ma possiamo anche scrivere in forma compatta:
[ -f "$FILE_NAME" ] && rm $FILE_NAME |
Diversamente da molti linguaggi di programmazione, gli operatori logici
&&
e ||
hanno uguale precedenza. Tuttavia, Bash
esegue il calcolo breve delle espressioni logiche, cioè non esegue
le parti di istruzione logica che non servono ai fini della determinazione
del valore di verità, similmente a quanto fanno altri linguaggi
come il C. Ecco perché nell'istruzione dell'esempio precedente
il remove viene eseguito solo se il test di esistenza del file ha avuto
esito positivo.
Per completare il quadro, vediamo come eseguire il remove di un file solo se esiste, e gestire correttamente l'eventuale condizione di errore. La prima versione:
if [ -f "$FILE_NAME" ]; then if ! rm $FILE_NAME; then echo "Cancellazione del file $FILE_NAME fallita." exit 1 fi fi |
Oppure, in forma compatta:
if [ -f "$FILE_NAME" ] && ! rm $FILE_NAME; then echo "Cancellazione del file $FILE_NAME fallita." fi |
Fatte queste premesse, passiamo in rassegna le altre istruzioni di controllo per i cicli. A questo scopo mi servirò di un semplice esempio: voglio stampare a schermo i numeri da 1 a 10. Ecco le diverse versioni:
# Istruzione "while" con test "[": n=1 while [ $n -le 10 ]; do echo $n n=$(( n + 1 )) done # Istruzione "while" con test "((" (solo Bash 2): n=1 while (( n <= 10 )); do echo $n (( n++ )) done # Istruzione "until" con test "[": n=1 until [ $n -gt 10 ]; do echo $n n=$(( n + 1 )) done # Istruzione "for": for n in 1 2 3 4 5 6 7 8 9 10; do echo $n done # Istruzione "for" resa sintetica col comando "seq": for n in $( seq 1 10 ); do echo $n done # Istruzione "for" in stile linguaggio C (solo Bash 2): for (( n=1; n<=10; n++ )); do echo $n done |
Il controllo dei cicli si completa anche dei classici comandi
continue
e break
per saltare alla prossima
iterazione e per uscire dal ciclo.
Le istruzioni select
e case
sono di uso meno
frequente: ne vedremo degli esempi nel seguito.
Una stringa letterale deve essere racchiusa tra doppi apici e può essere
arbitrariamente lunga. All'interno delle stringhe il carattere $
permette di includere anche delle variabili:
FILE_NAME="/tmp/dati.txt" N=$( wc $FILE_NAME | (read n x; echo $n ) ) echo "Il file $FILE_NAME contiene $N righe." |
In queste poche righe sono concentrate una serie di caratteristiche molto interessanti che ora esaminiamo in dettaglio:
Il costrutto $( lista )
esegue la lista di istruzioni
indicata in un sottoprocesso e ne ritorna l'output. Ma attenzione:
tutto l'output viene riportato su di una sola riga! Se si desidera
una riga specifica bisogna lavorare con i vari head
,
tail
, grep
, sed
e compagnia. Per
determinare il numero di righe nel file $FILE_NAME
ho
usato il classico comando wc
, che ritorna nell'ordine
il numero di righe, il numero di parole, il numero di byte e il nome
del file. Per isolare solo il primo dato, cioè il numero di
righe, sono ricorso ad un trucco che sfrutta le proprietà
del comando interno read
: il primo dato viene assegnato
alla variabile n
, mentre il resto viene assegnato alla
variabile x
. Osserviamo che, siccome la read
viene eseguita in un sottoprocesso dello shell, le variabili così
assegnate non sono visibili all'esterno di questo sottoprocesso. L'unico
modo per recuperare il valore di $n
è di riportarlo
in stdout con un echo
. Dal punto di vista della efficienza,
l'intera istruzione genera tre processi: il wc
, la
sotto-shell dove esegue read
ed echo
, e la
sotto-shell per il costrutto $( lista )
. Non è certo
una soluzione adatta per le alte prestazioni e, quando serve, bisogna
mettere in atto qualche trucco più sofisticato.
Bash prevede una serie di funzionalità per la manipolazione delle stringhe che sono particolarmente utili nelle applicazioni tipiche nelle quali questo linguaggio viene utilizzato. Ad esempio, per estrarre l'estensione di un file:
echo -n "Il file $FILE_NAME e' di tipo " estensione=${FILE_NAME##*.} case $estensione in txt,text) echo "testo" ;; html) echo "HTML" ;; gif,jpg,jpeg) echo "immagine" ;; *) echo "non previsto!"; exit 1 ;; esac |
Qui ho usato l'operatore di stringa
${nomevar##pattern}
applicato alla
stringa $FILE_NAME
per ricavare l'estensione del file.
La documentazione di Bash riporta anche altri operatori similari. Ad
esempio ${FILE_NAME%.*}
darebbe invece il nome base del file,
privo della estensione. Ovviamente questi operatori non sono limitati
a manipolare solo nomi di file, ma si possono sfruttare anche
per eseguire il parsing di stringhe generiche.
Purtroppo Bash non fornisce uno strumento generale come gli operatori di
stringhe basati sulle espressioni regolari, disponibili ad esempio in PERL.
In questi casi il Bash si appoggia ai comandi esterni, come sed
.
Con il Bash 2 è stato introdotto un operatore per estrarre
sottostringhe, una volta noto l'offset del carattere iniziale e il
numero di caratteri da estrarre. Nel prossimo esempio uso questa
funzionalità per assicurare che la stringa $msg
non superi la lunghezza di 40 caratteri; se è più lunga,
viene troncata e il troncamento viene marcato con ellissi:
msg="Questo messaggio e' particolarmente lungo e bisogna accorciarlo a 40." if [ ${#msg} -gt 40 ]; then msg=${msg:0:37}"..." fi echo "$msg" |
che visualizzerà sullo schermo la stringa $msg
accorciata così:
Questo messaggio e' particolarmente l...
Esistono diversi strumenti per la gestione dei processi in Bash: qui ne vedremo solo alcuni.
Spesso è comodo avviare dei processi in background e lasciare che svolgano il loro lavoro mentre noi siamo liberi di continuare la nostra sessione di shell. A questo scopo basta aggiungere il carattere di ampersand alla fine della riga di comando:
programma parametro1 parametro2 & |
La conseguenza, ancora una volta, è che l'ampersand &
diventa un carattere riservato: per inserirlo come letterale di stringa
occorre farlo precedere dal backslash, oppure bisogna racchiuderlo
tra doppi apici.
Bash esegue il forking, e ritorna nella variabile $!
il PID
del sottoprocesso avviato. Bash fornisce anche le istruzioni trap
per la gestione dei segnali (come SIGCHLD
) e wait
per controllare la terminazione dei sottoprocessi, oltre naturalmente
a kill
per inviare segnali a un processo.
Ricordiamo che tutti i sottoprocessi avviati da shell hanno lo shell stesso come processo padre, e da esso ereditano le variabili d'ambiente e tutti i file aperti. Per convenzione, i file numero 0, 1 e 2 vengono interpretati dai processi come standard input, standard output e standard error, e sono associati rispettivamente il primo alla tastiera e gli altri due allo schermo del terminale. Ne segue che se i programmi avviati in background generano un output o richiedono un input, questo potrebbe interferire fastidiosamente con le altre operazioni che stiamo eseguendo in modo interattivo, e quindi di solito i processi lanciati in background vengono avviati previa una adeguata redirezione del loro output.
Il modo più comune per gestire i file nello shell è attraverso gli operatori di redirezione. Mentre i comuni linguaggi di programmazione richiedono esplicite chiamate per l'apertura, la scrittura/lettura e la chiusura dei file, in Bash si può avviare un comando indicando semplicemente i file di interesse:
programma parametri <fileinput >fileoutput &2>fileerror |
Gli operatori <file, >file 2>file
istruiscono lo shell
ad eseguire il programma indicato con impostati questi file come
stdin, stdout e stderr. Naturalmente, non è necessario indicare
la redirezione su tutti i tre file. E' possibile combinare la redirezione
con l'esecuzione in background, così che in pratica possiamo indicare
al programma dove si trovano i dati da elaborare, dove riversare i risultati
e dove inviare gli eventuali errori. Una volta terminato il programma, potremo
andare ad esaminare con comodo questi file.
Sfortunatamente, ancora una volta abbiamo attribuito un significato
speciale ai caratteri >
e <
, per cui
torna utile il backslash quando necessario.
Spesso è comodo riversare stdout e stderr in un unico file: in questo
caso l'operatore di redirezione da usare è &>
.
Le pipe sono uno degli strumenti più potenti che possono essere usati nello shell, e permettono di avviare diversi programmi in modo che l'output del precedente si innesti come input del successivo. Una delle applicazioni più frequenti è con il grep, ad esempio per trovare tutti i file che sono directory:
ls -l | grep ^d |
Il carattere di barra verticale |
comanda allo shell di creare
una pipe senza nome nella quale fluiscono i dati che vanno dal
processo ls verso il processo grep; una volta creata la pipe, lo shell
avvia i due processi impostando lo stdout di ls verso l'input della pipe,
e lo stdin di grep verso l'output della pipe. L'effetto netto è
che l'output del processo ls viene filtrato dal processo grep prima di essere
riversato sullo schermo. In questo caso l'espressione regolare del grep
seleziona le righe che iniziano con la lettera d
, che sono proprio
le directory che volevamo.
Vi sarete accorti che il carattere di barra verticale assume così un significato speciale, e diventa un carattere riservato: per indicare il carattere letterale dovremo farlo precedere dal solito backslash. Come esempio, cercare di prevedere il diverso comportamento dei comandi seguenti:
echo ciao | grep c > f echo ciao \| grep c > f echo ciao | grep c \> f echo ciao \| grep c \> f |
Tutti i processi, una volta terminati, ritornano al padre che li ha avviati lo stato di uscita: si tratta di un numero che codifica la condizione di errore riscontrata. Come scelta naturale, il codice 0 (zero) corrisponde alla assenza di errore e quindi alla terminazione con successo del programma, mentre altri valori indicano che qualcosa è andato storto. Fin qui, tutto normale.
Uno dei ruoli principali di uno shell è quello di collante tra vari programmi. Di conseguenza, lo stato di uscita di un processo rientra in una qualche condizione logica per stabilire come comportarsi poi. In Bash lo stato di uscita dei processi è proprio il valore logico su cui si lavora, per cui 0 codifica il successo, mentre tutti gli altri valori codificano il fallimento del processo. Questa convenzione è opposta a quella adottata in C e nella maggior parte degli altri linguaggi.
Per aprire un nuovo file sul descrittore numero 3 rispettivamente in
scrittura / lettura / lettura e scrittura si può usare il comando
exec
corredato dagli opportuni operatori di redirezione:
exec 3> /tmp/dati exec 3< /tmp/dati exec 3<> /tmp/dati |
A questo punto il descrittore numero 3 si può usare per le operazioni
di redirezione successive, ricordando che l'operatore di redirezione
>
sovrascrive il file, mentre >>
accoda al file esistente. Per esempio:
echo "Elaborazione dati" >> &3 read altezza larghezza profondita <&3 |
Una volta finito, si può chiudere il file:
exec 3<&- |
Talvolta si desidera che tutto l'output del programma, incluso gli eventuali errori, venga rediretto su un file di log. Ci sono diversi modi per ottenere questo risultato: specificare gli operatori di redirezione per ogni comando; inserire tutti i comandi in un gruppo di comandi delimitato da parentesi graffe e specificare gli operatori di redirezione una volta per tutte. Una terza possibilità è quella di ridefinire i descrittori di file 1 e 2 in modo definitivo, come nel seguente esempio:
exec 1>>/var/log/miolog 2>&1 echo "Backup del $( date ):" tar cvf - /home | gzip > /mnt/bkup/last-bk ... |
Le funzioni sono un potente strumento per la strutturazione dei programmi. In Bash le funzioni devono essere definite prima di essere invocate, si invocano come un qualsiasi comando, accettano parametri, supportano la ricorsione, ritornano un codice di stato, possono comunicare con l'esterno attraverso i file (stdin, stdout, stderr, ecc.) e attraverso le variabili globali. Vedremo alcuni esempi di applicazione delle funzioni nei prossimi due paragrafi.
Il primo esempio di programma è un gestore di pacchetti RPM, il
diffuso sistema per la distribuzione di software per varie distribuzioni
di Linux, a partire dalla Red Hat, alla SuSE, Mandrake, ed altre.
Gli strumenti visuali tipicamente disponibili sono piuttosto lenti
sulle macchine non recenti, ed inoltre non sono utilizzabili da linea di
comando. Questo programma realizza invece una semplice interfaccia a menu
verso le funzionalità più comuni di rpm. Per selezionare
una opzione del menu basta premere il tasto corrispondente, non è
necessario premere anche il tasto Invio (o Return): questo risultato
è possibile grazie alla nuova opzione -n
del comando
read
introdotto con Bash 2. Prima, con Bash versione 1, si era
costretti a ricorrere a trucchi sporchi per ottenere lo stesso risultato.
#!/bin/sh while : ; do echo -e "\nmyrpm - Interfaccia a menu per rpm" echo " b) mostra info su di un package installato" echo " c) mostra tutti i packages installati" echo " d) mostra il package cui appartiene un certo file" echo " e) installa un package" echo " j) aggiorna un package" echo " f) disinstalla un package" echo " q) quit" echo -ne "Quale? [ ]\b\b" read -n 1 q echo case $q in b) echo -n "Nome Package: " read pk { echo -e "\nINFO ON $pk:" rpm -qi $pk echo -e "\nFILES ON $pk:" rpm -qls $pk echo -e "\nDEPENDENCIES OF $pk:" rpm -q --requires $pk } | less ;; c) { echo -e "\nTUTTI I PACKAGES INSTALLATI:" rpm -qa | sort -df } | less ;; d) echo -n "File: " read fn echo "Il file $fn appartiene a:" rpm -qf $fn | less ;; e) echo -n "File *.rpm da installare: " read pk rpm --install $pk 2>&1 | less ;; f) echo -n "Package da disinstallare: " read pk rpm --erase $pk 2>&1 | less ;; j) echo -n "File *.rpm di aggiornamento: " read pk rpm -U $pk | less 2>&1 ;; q) exit 0 ;; *) echo "OPZIONE ERRATA, RIPROVA" sleep 4 ;; esac done
Listato 1. Programma per gestire pacchetti RPM ( myrpm
).
Anche tu non ricordi mai tutte le opzioni di un certo comando, e ti tocca consultare spesso la man page per trovare quella giusta? Bene, adesso sai come renderti la vita più semplice costruendo il tuo programma wrapper con interfaccia a menu!
Bash 2 introduce i socket Internet, e permette di realizzare semplici programmi client. Qui svilupperemo un piccolo programma capace di scaricare la posta elettronica e di visualizzarla sullo schermo. Il dialogo con il server avviene usando il protocollo POP-3, documentato nell'RFC 1939. L'articolo Protocolli 1 pubblicato sul PLUTO Journal n. 35 del gennaio 2002 (www.pluto.linux.it/journal/pj0201/protocolli1.html) fornisce una introduzione a questo protocollo.
All'inizio del programma ho raccolto le variabili che caratterizzano
il collegamento: il nome dell'host remoto, la porta TCP, il nome e
la password di login. Ho fatto le prove con il server POP-3 del mio
stesso computer, ecco perché il nome dell'host indicato è
localhost
.
Siccome il dialogo tra il nostro client e il server avviene per linee, ho trovato conveniente approntare tre utili funzioni:
putline()
invia una linea al server; notare che la linea
deve essere terminata dai codici ASCII di controllo CR e LF: poiché
il comando echo
concluderebbe la riga col solo LF, dovremo
aggiungere esplicitamente il CR.
getline()
si pone in attesa di una linea dal server usando
il comando read
; il carattere LF che termina la linea viene
riconosciuto e scartato automaticamente, mentre rimane il carattere CR da
togliere; infine, la linea così ottenuta viene salvata nella
variabile globale $l
; la funzione ritorna 0 se la lettura ha avuto
successo, 1 altrimenti.
check_ok()
ritorna 0 se la riga $l
inizia
con i caratteri +OK
: questa è la convenzione adottata
dal server POP-3 per indicare il successo dell'ultimo comando inviato.
Una volta approntate queste tre funzioncine di base, il resto del programma non deve fare altro che inviare in sequenza i comandi POP-3 necessari, controllare l'esito dell'operazione, e quindi interpretare adeguatamente i risultati ottenuti.
#!/bin/bash -f # # Scarica posta del server POP-3 e la mostra su stdout. # La posta viene lasciata sul server. # Esce con codice 0 se ha successo, 1 altrimenti. # # ATTENZIONE! richiede Bash v. 2 host="localhost" port=110 user="salsi" pass="xyzt" function putline() { echo "C: $@" echo -n "$@"$'\r\n' >&3 } function getline() { IFS_BAK="$IFS" IFS="" read -r l <&3 result=$? IFS="$IFS_BAK" [ $result == 0 ] || return 1 cr=$'\r' l=${l%$cr} echo "S: $l" } function check_ok() { [ ${l:0:3} == "+OK" ] } # Apre socket TCP sul descrittore 3: 3<>/dev/tcp/$host/$port || exit 1 # Scarica il msg di benvenuto: getline && check_ok || exit 1 # Esegui il login: putline "USER $user" getline && check_ok || exit 1 putline "PASS $pass" getline && check_ok || exit 1 # Determina il numero $n di email giacenti: putline "STAT" getline && check_ok || exit 1 n=$( echo "$l" | (read x n x; echo $n) ) # Scarica tutte le $n email: for (( i=1; i<=n; i++ )); do putline "RETR $i" getline && check_ok || exit 1 while getline ; do [ "$l" == "." ] && break done done # Chiudi il collegamento: putline "QUIT" getline && check_ok || exit 1 exit
Listato 2. Programma client per protocollo POP-3 ( mypop
).
Alcune note su questo programmino:
Grazie alle recenti estensioni di Bash 2, l'intero programma è realizzato sfruttando esclusivamente funzionalità interne di Bash: dunque non vengono invocati processi esterni, e la velocità di esecuzione risulta decisamente buona.
Noterai che il programma visualizza la posta ma non la cancella dal server
(cioè non usa il comando DELE del protocollo POP-3): è facile
introdurre anche questa funzionalità aggiungendo queste righe
come ultime istruzioni del ciclo for
:
putline "DELE $i" getline && check_ok || exit 1 |
Un'altra utile funzionalità sarebbe quella di salvare i messaggi
ognuno in un file di testo: 1.txt
per il primo email,
2.txt
per il secondo, e così via, in modo che
siano facilmente consultabili. E' molto facile realizzare questa
funzionalità sfruttando gli operatori di redirezione.
Suggerimento:
for (( i=1; i<=n; i++ )); do ... while getline ; do ... done | sed "s/^S: //" > $i.txt done |
Un'altra estensione facile da realizzare è l'implementazione
del comando APOP per l'invio crittato della password: l'articolo sui
protocolli già citato fornisce le indicazioni necessarie. La
parte più difficile di questa estensione è riuscire ad
estrarre il timestamp dal messaggio di benvenuto del server: a questo
scopo si può lavorare sia con gli operatori di stringa di Bash,
sia con il filtro sed
. Il messaggio di benvenuto del server
è la prima linea $l
ritornata alla prima invocazione
di getline()
. Ecco un esempio:
s=${l#*<} s=${s%>*} timestamp="<$s>" crittata=$( echo -n $timestamp$pass | md5sum | (read s x; echo $s) ) putline "APOP $user $crittata" getline && check_ok || exit 1 |
Altri esempi con applicazioni ai CGI del WEB si trovano nell'articolo
Apache 2 pubblicato su PLUTO Journal n. 36 di aprile 2002 (www.pluto.linux.it/pj0204/apache2.html
).
Ho omesso di descrivere altre importanti funzionalità di Bash, come
il file globbing, gli array, le variabili predefinite, le innumerevoli
opzioni di configurazione, ecc., e inoltre gli argomenti qui esposti sono
stati appena sfiorati, giusto per dare l'idea. Una buona lettura per
gli approfondimenti è la guida di riferimento di Bash, scritta
ad opera dei suoi autori e citata in bibliografia. La man page di Bash
rimane comunque la fonte ultima e definitiva.
Bash non pretende di sostituirsi ai veri linguaggi di programmazione, tuttavia può rivelarsi utile in molte occasioni. La sua sintassi è piuttosto arcana, richiede esperienza e induce facilmente a commettere errori subdoli, per cui non è certo un linguaggio didatticamente valido. Si tratta invece di uno strumento di uso pratico, e come tale si permette qualche espediente "sporco" pur di raggiungere lo scopo. Ricordo inoltre che gran parte delle possibilità disponibili nella programmazione con un linguaggio di shell stanno nei programmi a corredo del sistema operativo, a cominciare dai mitici sed, grep, find, ecc. e in generale quelli della collezione "shell utilities" di GNU.
man bash
).help
, help comando
).www.gnu.org/manual/bash-2.02
.http://ildp.pluto.linux.it/HOWTO/Bash-Prog-Intro-HOWTO.html
(in italiano).http://personal.riverusers.com/~thegrendel
.info shell
).http://digilander.iol.it/salsi/erratacorrige
.
L'autoreUmberto Salsi <umberto-salsi@libero.it> ha scritto il suo primo programma nel 1981: un potente ciclo FOR stampava su schermo i numeri da 1 a 10000. Folgorato da questo successo, da allora non ha più smesso di seviziare computer nel software e nell'hardware, e di queste pratiche ne ha fatto il suo lavoro e il suo hobby. Nel 1992 scopre Internet e il mondo delle reti telematiche. Nel 1996 incontra GNU/Linux, ed è un'altra infatuazione. |
<- PW: Gnome - Archivio Generale - Copertina - PW: Gosh -> |