previous up

Introduzione allo Shell Scripting

index next

Subsections



3. Strutture di controllo

In questo capitolo verranno introdotti i cicli e le istruzioni di selezione, i mattoni da costruzione di ogni linguaggio di programmazione.


3.1 Cicli

Succede non di rado di dover ripetere un'istruzione nel tempo; questa operazione è fondamentale in ogni progetto di software. La shell Bash, mette a disposizione 3 tipi di ciclo differenti: while, for e until.

3.1.1 while

Il ciclo while consente di eseguire un blocco di istruzioni fino a quando una certa condizione è vera. La verifica della condizione viene effettuata attraverso il comando test, oppure racchiudendo la condizione tra parentesi quadre, [...]. Un esempio renderà il tutto più chiaro.

Esempio 3.1.1

#!/bin/bash
#
# ciclo while e condizioni

EXIT_SUCCESS=0

while [ "$RISPOSTA" != "q" ]
do
      echo "Premi un tasto (per uscire \"q\"):"
      read RISPOSTA
done

# avremmo potuto scrivere equivalentemente:
#
# while test "$RISPOSTA" != "q"
# do
#       echo "Premi un tasto (per uscire \"q\"):"
#       read RISPOSTA
# done

exit $EXIT_SUCCESS

Come funziona:

L'esempio precedente ha introdotto numerose novità. Innanzi tutto abbiamo visto come utilizzare il ciclo while:

while [ condizione ]
do
      istruzioni
done
Usando la lingua parlata, possiamo dire che tale ciclo consiste in ``finché è vera la condizione in parentesi quadre, esegui le istruzioni contenute tra do e done''. Nel caso in esame, la condizione da verificare è \fbox{\texttt{\lq\lq \$RISPOSTA'' != \lq\lq q''}}. Sottolineamo come prima cosa che le virgolette giocano un ruolo fondamentale, soprattutto quelle relative alla variabile $RISPOSTA; se infatti avessimo scritto \fbox{\texttt{\$RISPOSTA !=
\lq\lq q''}}, la shell avrebbe interrotto l'esecuzione dello script ritornando un messaggio (ed anche un codice3.1) d'errore:

[nico@deepcool intro]$ ./script
./script: [: !=: unary operator expected
Ciò accade perché all'atto della prima valutazione della condizione presente nel ciclo, la variabile $RISPOSTA non è definita, ovvero si ha una condizione del tipo

...
while [  != "q" ]
...
e la shell non è in grado di interpretarla correttamente. La variabile $RISPOSTA ha infatti un valore nullo (null - value) e la shell non riesce a valutare la condizione dato che in queste condizioni si aspetterebbe la presenza di un operatore unario (!= non lo è!). Sono fondamentali gli spazi lasciati dopo [ e prima di ], poiché questi consentono alla shell di interpretare correttamente l'uso delle parentesi quadre per valutare una condizione.




Passiamo ora ad analizzare il funzionamento del ciclo. Alla sua prima esecuzione, la condizione \fbox{\texttt{\lq\lq \$RISPOSTA'' != \lq\lq q''}} è vera poiché la la variabile contiene una stringa vuota, dunque vengono eseguite le istruzioni comprese tra do e done. Nel nostro caso viene stampato sullo schermo un messaggio e poi si aspetta per acquisire un input dall'utente (read RISPOSTA). Acquisito l'input, la condizione viene rivalutata e, qualora risulti nuovamente vera, viene ripetuto il blocco di istruzioni. Non appena l'utente avrà premuto il tasto ``q'', lo script uscirà dal ciclo ed eseguirà l'istruzione successiva (exit $EXIT_SUCCESS).




Per ragioni stilistiche, spesso il ciclo while assume una forma lievemente diversa diventando:

while [ condizione ]; do
      istruzioni
done
Tale scrittura è consentita poiché la shell Bash prevede, anche se non lo impone, che ``;'' funga da separatore di istruzioni. Allo stesso modo, sarebbe perfettamente legale la scrittura
while [ condizione ]; do istruzioni; done
Si noti inoltre che l'uscita dal ciclo può verificarsi anche quando uno dei comandi interni ad esso riporta un valore di ritorno differente da zero (condizione di errore).




Nella sezione 2.3 abbiamo avuto a che fare con i parametri posizionali e ci siamo posti il problema di come fare a fornirne allo script un numero arbitrario. La questione può essere risolta facendo ricorso al comando interno shift .




La sintassi del comando è \fbox{\texttt{shift [N]}}, dove N è un naturale; con ciò si forza lo spostamente a sinistra della posizione del parametro posizionale di N posti. Qualora N non fosse passato, si assume che lo spostamento sia di una unità; se invece N è zero o un numero maggiore di $#, allora non viene intrapresa alcuna azione sui parametri. L'operazione non modifica il valore di $0. Vediamo quindi come modificare l'esempio 2.3.1.

Esempio 3.1.2

#!/bin/bash
#
# Utilizzo dei parametri posizionali (2)

EXIT_SUCCESS=0

TOTALE=$#
echo -n "Hai inserito "

while [ -n "$1" ]; do
    echo -n "$1 "
    shift
done

echo "per un totale di $TOTALE parametri"

exit $EXIT_SUCCESS

Come funziona:

Come prima cosa, mostriamo di aver detto la verità:

[nico@deepcool nico]$ ./shift.sh param1 param2 param3 param4
Hai inserito param1 param2 param3 param4 per un totale di 4 parametri
Lo script non conosceva a priori il numero di parametri inseriti, ma si è comportato in maniera corretta elencando uno ad uno i parametri passati e scrivendone il loro numero.




Analizziamo ora quanto accaduto. Alla prima iterazione del ciclo, viene stampato sullo schermo il valore di $1 e successivamente eseguita l'istruzione shift (Lo spostamento per tanto è di una posizione). In questo modo, a $# viene assegnato il valore $# - 1 ed il vecchio $2 diventa $1. Da ciò si capisce il motivo dell'assegnazione \fbox{\texttt{RITORNO=\$\char93 }}, se avessimo utilizzato il parametro $# dopo l'esecuzione del ciclo per ottenere il numero totale di parametri passati allo script, avremmo ottenuto il valore 0, poiché il contenuto di $# sarebbe stato nel frattempo modificato da shift.


3.1.2 for

Il ciclo for della shell Bash è nettamente differente da quello presente in altri linguaggi di programmazione, la sua sintassi è infatti:

for ELEMENTO in LISTA
do
    istruzioni
done
In lingua corrente potrebbe essere reso con ``per ogni ELEMENTO presente in LISTA esegui i comandi compresi tra do e done''. Possiamo già notare una importante caratteristica; il ciclo for consente di definire una variabile, proprio come read e l'assegnazione tramite l'operatore =. Consideriamo il seguente esempio.

Esempio 3.1.3

#!/bin/bash
#
# Esempio d'uso del ciclo for

EXIT_SUCCESS=0

for file in $(ls $PWD); do
    echo $file
done

exit $EXIT_SUCCESS

Come funziona:

All'inizio, il ciclo inizializza una nuova variabile $file al primo elemento della lista $(ls $PWD)3.2 e successivamente stampa sullo schermo il contenuto di tale variabile. Questo processo continua fino a quando non vengono esauriti gli elementi presenti nella lista. La lista di elementi deve essere composta da una serie di parole separate dal separatore (eventualmente più di uno) standard $IFS. È possibile definire un separatore standard personalizzato, in modo da processare qualsiasi tipo di lista.

Esempio 3.1.4

#!/bin/bash
#
# $IFS personalizzato

EXIT_SUCCESS=0
LISTA_1="uno:due:tre:quattro:cinque:sei:sette:otto:nove:dieci"
LISTA_2="1 2 3 4 5 6 7 8 9 10"
OLD_IFS="$IFS"

export IFS=":"

# Primo ciclo
for i in $LISTA_1; do
    echo $i
done

export IFS="$OLD_IFS"

# Secondo ciclo
for j in $LISTA_2; do
    echo $j
done

exit $EXIT_SUCCESS

Come funziona:

L'esempio mostra come sia possibile sfruttare la personalizzazione di $IFS per avere maggiore flessibilità nel processare una lista. Dapprima vengono create le liste $LISTA_1 e $LISTA_2 mentre la variabile $OLD_IFS viene inizializzata al valore di $IFS. Successivamente il separatore standard viene cambiato in ``:'' ed esportato in modo da renderlo disponibile ai processi figli dello script. A questo punto il primo ciclo for processa la prima lista separandone gli elementi in base alla presenza di ``:'' per poi stamparli. All'uscita,$IFS viene esportata riassumendo il suo valore originario, in modo da processare la seconda lista e stamparne gli elementi, come nell'esempio precedente.




Ormai dovrebbe essere chiara la differenza tra i parametri $* e $@. Infatti la prima scrittura fornisce una lista con singolo elemento, mentre la seconda ritorna una lista contenente tutti i parametri passati allo script.

Esempio 3.1.5

#!/bin/bash
#
# Uso di $* e $@

EXIT_SUCCESS=0

echo "Inizia il primo ciclo:"
for i in "$*"; do
    echo $i
done
echo -e "Il primo ciclo è finito\n"

echo "Inizia il secondo ciclo:"
for i in "$@"; do
    echo $i
done
echo "Il secondo ciclo è finito"

exit $EXIT_SUCCESS

Come funziona:

L'output dello script è più eloquente di ogni spiegazione:
[nico@deepcool intro]$ ./script param1 param2 param3 param4 param5 
Inizia il primo ciclo:
param1 param2 param3 param4 param5
Il primo ciclo è finito

Inizia il secondo ciclo:
param1
param2
param3
param4
param5
Il secondo ciclo è finito




Con oppurtuni accorgimenti, possiamo ottenere un ciclo for analogo a quello del C.

Esempio 3.1.6

#!/bin/bash
#
# un for da C!

EXIT_SUCCESS=0

for i in $(seq 1 10); do
    echo $i
done

exit $EXIT_SUCCESS

Come funziona:

Sfruttando l'utility seq (man seq per fugare ogni dubbio relativo al suo utilizzo) siamo riusciti ad ottenere un ciclo for che itera le sue istruzioni alla stessa maniera del for del C; seq 1 10 infatti restituisce una lista contenente i numeri da 1 a 10 (estremi inclusi).


3.1.3 until

Il ciclo until è simile al while,

until [ CONDIZIONE ]
do
      COMANDI
done
l'unica differenza risiede nel fatto che i comandi compresi tra do e done vengono ripetuti fino a quando la condizione che si verifica è falsa. In lingua italiana, il ciclo until suonerebbe come: fino a quando CONDIZIONE non è vera esegui COMANDI. Consideriamo l'Esempio 3.1.1 modificandolo opportunamente.

Esempio 3.1.7

#!/bin/bash
#
# ciclo until e condizioni

EXIT_SUCCESS=0

until [ "$RISPOSTA" = "q" ]; do    
      echo "Premi un tasto (per uscire \"q\"):"
      read RISPOSTA
done

exit $EXIT_SUCCESS

Come funziona:

Il comportamento dello script è del tutto analogo a quello dell'Esempio 2.4.1, in questo caso, però, l'iterazione avviene utilizzando un ciclo until, per questo motivo la condizione da valutare è stata cambiata. Mentre in 2.4.1 questa era \fbox{\texttt{\lq\lq \$RISPOSTA'' != \lq\lq q''}}, nel presente deve essere negata in modo da funzionare correttamente. Si ottiene dunque \fbox{\texttt{\lq\lq \$RISPOSTA'' = \lq\lq q''}}.


3.2 Istruzioni di selezione

Le istruzioni di selezione, come i cicli, servono a controllare l'esecuzione di blocchi di codice.




Abbiamo visto in precedenza che il contenuto di un ciclo viene eseguito a seconda della veridicità della condizione; un comportamento del tutto analogo si applica anche in questo caso, con la sola differenza che non esiste alcuna iterazione del codice interno all'istruzione.




La selezione è utile, ad esempio, tutte quelle volte in cui si deve prendere una decisione a seconda del valore di una variabile o altra condizione.


3.2.1 if

L'istruzione if consente di eseguire un blocco di codice se una condizione è vera. Come è da aspettarsi, la condizione può essere valutata sia con [ ... ], sia con test 3.3. Come da nostra tradizione, consideriamo un semplice esempio per illustrarne il funzionamento:

Esempio 3.2.1

#!/bin/bash
#
# Un semplice esempio d'uso per if

EXIT_SUCCESS=0
EXIT_FAILURE=1

echo "Ti sta piacendo la guida? (si/no)" 

read RISPOSTA

if [ "$RISPOSTA" != "si" -a "$RISPOSTA" != "no" ] ; then
   echo "Rispondi solo con un si o con un no"
   exit $EXIT_FAILURE
fi

if [ "$RISPOSTA" == "no" ] ; then
   echo "Vai in castigo!" 
   exit $EXIT_FAILURE
fi

echo "Tu si che sei un bravo ragazzo!" 

exit $EXIT_SUCCESS

Come funziona:

Procediamo con l'analisi dell'esempio riportato. Come prima cosa, vengono definite due fariabili $EXIT_SUCCESS ed $EXIT_FAILURE che serviranno a specificare il valore di ritorno del nostro script alla shell. Successivamente viene presentata una domanda alla quale occorre rispondere con un si o cono un no. Il primo controlla che $RISPOSTA non sia diversa da ``si'' o ``no''. La condizione tra [ ... ] è infatti un AND logico e risulta verà se e solo se sono vere le due condizioni che la compongono. La condizione è dunque vera solo quando risposta è diversa sia da ``si'', sia da ``no''. In tale evenienza, viene eseguito il codice interno, ovvero viene stampato un messaggio di cortesia (Rispondi solo con un si o con un no) e si termina l'esecuzione ritornando un codice d'errore.




Qualora si fosse risposto correttamente, la shell determina quale sia stata la risposta; il secondo if, infatti, serve a verificare se la risposta è stata un no, in tal caso stampa sullo schermo un cortese invito (!) all'esecutore e ritorna all'ambiente d'esecuzione un codice d'errore. Se, invece, la risposta non è ``no'', allora sarà sicuramente un ``si'' (non c'è altra possibilità), perciò verrà scritto sullo schermo che chi l'ha eseguito è un bravo ragazzo (!) e lo script terminerà ritornando un valore di successo.




Anche qui vediamo di sintetizzare e rendere in lingua italiana l'istruzione.

if [ CONDIZIONE ] ; then
   COMANDI
fi
Pertanto, possiamo ``leggere'' un if come Se è vera CONDIZONE, allora esegui tutti i COMANDI compresi tra then e fi.


3.2.2 if - else

Può accadere di arrivare ad un punto in uno script in cui occorre fare due operazioni differenti a seconda che una certa condizione sia vera o meno. Ciò è simile a quanto fa ogni persona quando si trova in un punto in cui la strada si biforca e deve decidere dove andare. Ad esempio, un automobilista potrebbe fare questo ragionamento ``Se andando a sinistra giungo a destinazione, allora procedo in quella direzione, altrimenti giro a destra''.




Riconsideriamo l'esempio 3.2.1 e proviamo a scriverlo in modo diverso.

Esempio 3.2.2

#!/bin/bash
#
# Un semplice esempio d'uso per if - else

EXIT_SUCCESS=0
EXIT_FAILURE=1

echo "Ti sta piacendo la guida? (si/no)" 

read RISPOSTA

if [ "$RISPOSTA" != "si" -a "$RISPOSTA" != "no" ] ; then
   echo "Rispondi solo con un si o con un no"
   exit $EXIT_FAILURE
fi

if [ "$RISPOSTA" == "no" ] ; then
   echo "Vai in castigo!" 
   exit $EXIT_FAILURE
else
   echo "Tu si che sei un bravo ragazzo!" 
   exit $EXIT_SUCCES
fi

Come funziona:

Come possiamo notare, l'unica cosa ad esser cambiata è il secondo if, che ora contiene l'istruzione else. In questo modo, qualora $RISPOSTA non fosse ``no'', sarebbe alternativamente eseguito il codice compreso tra else e fi. Riassumendo, un costrutto if - else
if [ CONDIZIONE ] ; then
   COMANDI1
else
   COMANDI2
fi
nella lingua di Dante suonerebbe così: Se è vera CONDIZONE, allora esegui COMANDI1 (presenti tra then ed else), altrimenti esegui COMANDI2 (presenti tra else e fi).


3.2.3 if - elif - else

Nella sezione precedente (3.2.2) abbiamo lasciato un automobilista per strada, mentre decideva se girare a sinistra o a destra. Non vogliamo che rimanga fermo lì per sempre, perciò cerchiamo di farlo andare avanti.
Supponiamo che alla fine abbia deciso di procedere a destra e che ora si trovi ad un incrocio; in questo caso non dovrà solo scegliere la strada che lo porterà a destinazione, ma anche quella più breve.
Ragionare in termini di if - else diventà un po' più complicato e si ha bisogno di un nuovo costrutto che semplifichi la vita (forse all'automobilista basterebbe una cartina stradale). Siamo fortunati, la shell Bash è prodiga nei nostri confronti e ci fornisce il costrutto if - elsif - else. Vediamo come utilizzarlo rivisitando l'esempio precedente.

Esempio 3.2.3

#!/bin/bash
#
# Un semplice esempio d'uso per if - elif - else

EXIT_SUCCESS=0
EXIT_FAILURE=1

echo "Ti sta piacendo la guida? (si/no)" 

read RISPOSTA

if [ "$RISPOSTA" == "si" ] ; then
   echo "Tu si che sei un bravo ragazzo!" 
   exit $EXIT_SUCCESS
elif [ "$RISPOSTA" == "no" ] ; then
   echo "Vai in castigo!" 
   exit $EXIT_FAILURE
else
   echo "Rispondi solo con un si o con un no"
   exit $EXIT_FAILURE
fi

Come funziona:

L'esempio è chiaro, si analizza la prima condizione ( \fbox{\texttt{\lq\lq \$RISPOSTA'' == \lq\lq si''}}), se risultasse vera, lo script eseguirebbe i comandi compresi tra then ed elif, altrimenti passerebbe a valutare la condizione seguente ( \fbox{\texttt{\lq\lq \$RISPOSTA'' == \lq\lq no''}}). Nuovamente, se questa fosse vera, verrebbero eseguite le istruzioni presenti tra then ed else, in caso contrario (entrambe le condizioni precedenti false) sarebbero eseguiti i comandi tra else e fi. Facile, vero?


3.2.4 case

Insistiamo con il nostro esempio; lo stiamo presentando in diverse salse, non diventeremo dei cuochi per questo, ma di sicuro impareremo ad usare meglio la shell. Questa volta utilizzeremo il costrutto case, molto utile quando si vuole confrontare una variabile con un possibile insieme di valori ed agire in modo differente a seconda del valore che questa ha assunto.

Esempio 3.2.4

#!/bin/bash
#
# Uso di case

EXIT_SUCCESS=0
EXIT_FAILURE=1

echo "Ti sta piacendo la guida?" 

read RISPOSTA

case $RISPOSTA in
  si|s|yes|y)
    echo "Tu si che sei un bravo ragazzo!" 
    exit $EXIT_SUCCESS
    ;;
  no|n)
    echo "Vai in castigo!" 
    exit $EXIT_FAILURE
    ;;
  *)
    echo "Non ho capito, puoi ripetere?" 
    exit $EXIT_FAILURE
    ;;
esac

Come funziona:

Dopo aver letto l'input dell'utente, $RISPOSTA viene passata al costrutto esac, che la analizza. Ci accorgiamo subito che questa volta lo script è più elastico, infatti non chiede di scrivere un ``si'' o un ``no'', ma lascia ampia libertà all'utente. Se $RISPOSTA è uno tra ``si s yes y'', allora lo script eseguirà il blocco interno fino alla prima occorrenza di ``;;''. Come si può notare, per separare una lista di possibili valori assunti da $RISPOSTA viene usato |, da non confondersi con l'operatore. Qualora i valori elencati nella prima possibilità non corrispondessero con la variabile in esame, si passerebbe ad analizzare la seconda (``no n''). Nuovamente, in caso di esito positvo, verrebbe eseguito il codice interno, sino alla prima occorrenza di ``;;''. Se nessuna delle possibilità corrispondesse con $RISPOSTA, allora verrebbe eseguito il codice compreso tra ``*)'' e l'ultima occorrenza di ``;;'', che rappresenta in un certo senso il l'azione di default, dato che * un pattern3.4 che individua qualunque valore assunto da $RISPOSTA. Il costrutto si conclude con l'istruzione esac.




Dopo aver visto case in azione, cerchiamo di generalizzare ciò che abbiamo imparato. case controlla sequenzialmente una variabile in un elenco di casi forniti dall'utente. Un caso può essere composto da più possibilità, separate dal simbolo |. Ogni caso va concluso con una parentesi tonda di chiusura ``)''. Il primo caso che soddisfa la variabile data provoca l'uscita dalla struttura, pertanto si deve evitare di porre come primo caso un valore molto generico, ma elencare in primo luogo i casi particolari. Occorre soffermarsi ulteriormente sui casi; questi infatti possono anche essere presentati sotto forma di semplici espressioni regolari. Ad esempio, se volessimo controllare una variabile per scoprire se questa contiene una lettera maiuscola o una minuscola oppure un numero, potremmo scrivere:

... 

  [a-z]) echo "Hai premuto una lettera minuscola" ;;
  [A-Z]) echo "Hai premuto una lettera maiuscola" ;;
  [0-9]) echo "Hai premuto un numero" ;;

...
L'espressione [a-z] ([A-Z]) sta per ``tutte le lettere comprese tra a e z (A e Z)'', così pure [0-9] sta per ``tutti i numeri compresi tra 0 e 9''. Il codice precedente ci ha anche mostrato che è legale allineare caso, comandi (eventualmente separati da ``;'') e terminatore (``;;''). Il default rappresenta un caso particolare di espansione, la shell Bash, infatti, interpreta ``*'' come un qualsiasi valore.




Ogni blocco di codice da eseguire, deve essere seguito da ``;;'', che ne indica la fine, mentre la conclusione dell'istruzione è segnata dalla parola chiave esac.


3.3 Un ibrido: select

FIXME: Inserire paragrafo appena possibile

previous up

Introduzione allo Shell Scripting

index next

Domenico Delle Side 2002-09-24