Introduzione allo Shell Scripting |
In questo capitolo verranno introdotti i cicli e le istruzioni di selezione, i mattoni da costruzione di ogni linguaggio di programmazione.
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.
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.
#!/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
L'esempio precedente ha introdotto numerose novità. Innanzi tutto abbiamo visto come utilizzare il ciclo while:
while [ condizione ] do istruzioni doneUsando 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 è . Sottolineamo come prima cosa che le virgolette giocano un ruolo fondamentale, soprattutto quelle relative alla variabile $RISPOSTA; se infatti avessimo scritto , 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 expectedCiò 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
è 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 doneTale 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; doneSi 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 è
, 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.
#!/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 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 parametriLo 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
, 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.
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 doneIn 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.
#!/bin/bash # # Esempio d'uso del ciclo for EXIT_SUCCESS=0 for file in $(ls $PWD); do echo $file done exit $EXIT_SUCCESS
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.
#!/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
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.
#!/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
[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.
#!/bin/bash # # un for da C! EXIT_SUCCESS=0 for i in $(seq 1 10); do echo $i done exit $EXIT_SUCCESS
Il ciclo until è simile al while,
until [ CONDIZIONE ] do COMANDI donel'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.
#!/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
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.
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:
#!/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
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 fiPertanto, possiamo ``leggere'' un if come Se è vera CONDIZONE, allora esegui tutti i COMANDI compresi tra then e fi.
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.
#!/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
if [ CONDIZIONE ] ; then COMANDI1 else COMANDI2 finella lingua di Dante suonerebbe così: Se è vera CONDIZONE, allora esegui COMANDI1 (presenti tra then ed else), altrimenti esegui COMANDI2 (presenti tra else e fi).
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.
#!/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
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.
#!/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
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.
FIXME: Inserire paragrafo appena possibile
Introduzione allo Shell Scripting |