Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
I corsi di Scienze Matematiche Fisiche e Naturali
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Clemente Galdi » 4.Shell Scripting


Shell e controllo di flusso

Come negli altri linguaggi di programmazione, tutte le shell Unix posseggono costrutti per il controllo di flusso. È necessario tener presente, però, che questi costrutti devono essere in grado di interfacciarsi con i comandi messi a disposizione dal sistema.

  • Si ricorda che ogni comando unix ha sempre un exit value che, per convenzione, vale zero in caso di “successo”, un valore positivo in caso di “errore”.

In generale un flusso di esecuzione può essere suddiviso in più sequenze di comandi, denominate liste.

  • Una pipeline è una sequenza di uno o più comandi separati dal carattere “|”. Lo standard output del comando a sinistra della pipe viene connesso allo standard input del comando a destra della pipe.
  • Il valore di ritorno di una pipeline è pari al valore di ritorno dell’ultimo comando eseguito.
  • Una lista è una sequenza di pipeline delimitate da uno delle seguenti sequenze di caratteri: “;”, “||”, “&&”, “&” o newline.

- A OR B: Nella lista A || B il comando B viene eseguito se e solo se il comando A termina con un exit value diverso da zero.
- A AND B: Nella lista A && B il comando B viene eseguito se e solo se il comando A termina con un exit value uguale zero.
- Nella lista A & il comando A viene eseguito in backgroud.
- I caratteri “;” e newline delimitano la terminazione di una lista e l’inizio eventuale della successiva.

Il costrutto if-then-else

Il primo costrutto per il controllo di flusso è if-then-else.
Le possibili sintassi di questo costrutto: Mostra codice; Mostra codice. È possibile annidare costrutti di questo tipo. La shell consente l'utilizzo di una sintassi abbreviata per l'annidamento di costrutti, riportata nella figura in basso, in cui è possibile annidare un costrutto if all'interno del blocco else utilizzandola keyword elif.
La lista comandi inclusa nel then viene eseguita se e solo se l'exit value della lista comandi di controllo è pari a zero (che per convenzione, rappresenta "successo"), altrimenti viene eseguita la lista comandi nel blocco else.
Attenzione: a differenza dei linguaggi di programmazione classici, l'argomento dell'if NON è un test ma una lista di comandi.
Si noti che la valutazione di espressioni aritmetiche, attraverso uno dei costrutti ((...)) e let, ritorna, in realtà, due valori. Da un lato il valore dell'espressione calcolata. Dall'altra il "successo" o meno della valutazione. Si veda, questo esempio Mostra codice
La shell fornisce un comando test per l'esecuzione di confronti numerici, su stringhe o per verificare proprietà di file.

Il costrutto if-then-else (segue)

La shell fornisce il comando test per consentire la valutazione di una condizione. Restituisce il valore 0 (successo) se condizione è vera, il valore 1 (insuccesso) altrimenti.
La sintassi di test è: test condizione
Le scritture [ condizione ] e test condizione sono equivalenti.

Le condizioni posso essere:
- Confronti tra due valori numerici. Sintassi: N <primitiva> M.
<primitiva> può assumere I valori: -eq (=); -ne (diverso); -gt (N>M); -lt (N=M); -le (N<=M)
- Valutazione di proprietà di file. Sintassi: <primitiva> nomefile
può assumere, ad es.: -e (file esiste) -x (file eseguibile) -d (directory)
- Confronti tra stringhe di caratteri.
Sintassi: N <primitiva> M.
<primitiva> può assumere i valori: = (uguale); != (diverso);
Sintassi: N <primitiva> M.
<primitiva> può assumere i valori: -z (lunghezza zero); -n (lunghezza positiva);
Attenzione: A differenza dell’assegnazione, per il test su stringhe è necessario far precedere e seguire gli operatori “=” e “!=” da uno spazio.

Il costrutto if-then-else

È possibile comporre espressioni logiche utilizzando le primitive -a (AND), -o (OR) e ! (NOT). È possibile utilizzare anche una sintassi C-like utilizzando le primitive && (AND) e || (OR).
Sintassi: expr1 <primitiva> expr2.

Alcuni esempi: Mostra codice

Il costrutto case

In molti casi, l’annidamento di costrutti if-then-else rende il codice velocemente illeggibile.
Ove possibile, sarebbe opportuno utilizzare il seguente costrutto case: Mostra codice.
La shell confronta il valore della variabile stringa con i singoli pattern.
Un pattern, nel caso più semplice, è una stringa di caratteri. Vedremo, durante la lezione n.6 che un pattern può esprimere in realtà un insieme di stringhe.
La shell verifica se la variabile corrisponde al primo pattern. In caso positivo, esegue la lista di comandi associata al pattern. In caso contrario, viene analizzato il secondo pattern, e così via.
Poiché * rappresenta una stringa qualunque, essa può essere utilizzata per rappresentare "tutti gli altri casi".
Si noti che dopo l'esecuzione dei comandi associati al primo pattern corrispondente al valore del parametro l'esecuzione del costrutto termina.
Nell'esempio che segue: Mostra codice, sebbene la stringa "Casa" corrisponda ad entrambi i pattern, lo script visualizza soltanto le stringhe "Prima riga" e "Seconda riga".

Il costrutto for

Elemento fondamentale di ogni linguaggio di programmazione è la presenza dei costrutti di iterazione. La shell bash prevede tre costrutti di iterazione: for, while ed until.
I costrutti iterativi possono essere annidati in modo arbitrario.
Il seguente esempio: Mostra codice mostra una possibile sintassi per il costrutto for. La variabile var assume di volta in volta uno dei valori contenuti nella lista dei valori.
Si noti che la keyword do è posta sulla riga successiva alla lista dei valori. È possibile scrivere do sulla stessa riga della lista dei valori ma è necessario inserire il delimitatore ; prima del do.
La lista dei valori può essere omessa. In tal caso la lista viene implicitamente sostituita da $@, i.e., dalla stringa contenente tutti i parametri passati allo script.
I comandi inclusi nel corpo do/done vengono eseguiti una volta per ogni token presente nella lista.
Come è facile intuire, il valore di uscita del for è l'exit value dell'ultimo comando eseguito.
Nel caso in cui non viene eseguito nessun comando, e.g., nel caso in cui la lista è omessa e non vi sono parametri passati allo script, l'exit value del ciclo for è 0 ("success").

Esempio di script con relativo output: Mostra codice

Il costrutto for (segue)

È sempre necessario tenere conto dei caratteri di separazione dei token all’interno della lista.
Come già accennato, il delimitatore dei token all’interno della lista è definito dalla variabile IFS, i.e., per default, la lista è una sequenza di parole divise da spazi.
D’altro canto è possibile ridefinire questa variabile all’interno dello script.
Si tenga conto che per l’esecuzione di uno script, la shell crea un nuovo processo il cui compito è l’esecuzione dello script. Per questa ragione, la ridefinizione di IFS è locale allo script. In breve:

  • Lo script eredita la variabile IFS della shell padre
  • Può modificarla al suo interno.
  • La modifica della variabile IFS NON influenza la shell padre.

Il seguente script: Mostra codice, riporta un esempio di utilizzo di IFS.
Il primo ciclo for considera la lista come composta da un'unica stringa "a:b:c"
La modifica di IFS consente alla shell di suddividere la stessa stringa in tre token, "a", "b" e "c" che causano tre esecuzioni del comando echo.

Output dello script: Mostra codice

Il costrutto for (segue)

Non è richiesto che la lista dei valori sia una costante. In generale, è possibile utilizzare una qualsiasi variabile o output di comando. Resta ferma la considerazione appena fatta sul valore di IFS per la corretta suddivisione in token.
Di seguito un esempio in cui la lista dei valori consiste nell’output di un comando: Mostra codice
Nell'esempio specifico, la shell:

  • Esegue le espansioni necessarie
  • Esegue il comando ls
  • Suddivide l'output del comando ls in token
  • Esegue I comandi all'interno del costrutto di iterazione per ogni token estratto.

Il costrutto for (segue)

In molti casi è necessario eseguire un ciclo for utilizzando un indice intero. Un esempio classico è l’utilizzo degli array in cui, come abbiamo visto, l’indice deve essere un intero.
Chiaramente, definire una costante contenente la “lista degli indici” su cui eseguire le iterazioni è inopportuno.
La shell prevede diversi modi per definire in modo compatto una lista di valori interi.
In questo esempio: Mostra codice, è riportata una sintassi molto simile alla sintassi del costrutto for nel linguaggio C.
L'espressione expr1 viene valutata all'inizio del ciclo. Quindi viene valutata l'espressione expr2. La lista di comandi inclusa nel blocco do/done viene eseguira se il valore di expr2 è diverso da zero. Quindi viene valutata expr3.
In questo esempio: Mostra codice, si riporta un esempio concreto delle possibilità offerte da questa sintassi.
In questo esempio, expr1 corrisponde all'inizializzazione delle variabili a e b. La condizione di uscita expr2 è ottenuta tramite AND di due condizioni elementari. Infine l'espressione di expr3 incrementa la sola variabile a mentre la variabile b viene modificata all'interno del corpo del costrutto.

Il costrutto for (segue)

È possibile, inoltre utilizzare su sistemi Linux altre due possibili sintassi.
La prima prevede l’utilizzo del comando seq attraverso la sostituzione di comando. Questo comando prende in input uno, due o tre parametri numerici (interi o floating-point).

  • seq x: visualizza tutti gli interi tra 1 ed x. Ad es. seq 3 visualizza gli interi 1 2 3
  • seq x y: visualizza i numeri compresi tra x ed y, incrementando di volta in volta di una unità. Ad es. seq 1.5 4 visualizza: 1.5 2.5 3.5
  • seq x z y: visualizza i numeri partendo compresi tra x ed y, incrementando di volta in volta di una quantità pari a z. Ad. Ed. seq 1.5 .5 3 visualizza: 1.5 2 2.5 3

Esempio: Mostra codice
Attenzione: La bash supporta internamente solo l'aritmetica intera. Per utilizzare numeri reali è necessario utilizzare programmi appositi, e.g. "bc".

L'ultima sintassi utilizza l'espansione delle parentesi graffe attraverso la seguente sintassi: Mostra codice.
In questo esempio, la shell espande {1..10} nella sequenza di interi compresi tra 1 e 10.

Il costrutto for (segue)

La shell consente di interrompere la scansione della lista di valori utilizzando il comando built-in break.
Nel seguente esempio: Mostra codice, il ciclo viene terminato quanto la condizione numero -ge 3 è vera, i.e., la scansione della lista 1 2 3 4 5 viene interrotta al token "4".
L'istruzione successiva al break è la prima istruzione successiva alla keyword done.

Il comando break può ricevere in input un intero. Il valore del parametro deve essere maggiore di zero ed, in caso sia omesso, il suo valore di default è 1.
Il comando break n può essere utilizzato nel caso in cui vi siano più cicli iterativi annidati.
L'esecuzione di un break n implica la terminazione degli n costrutti iterativi annidati più interni.
Se n è maggiore del numero di costrutti iterativi annidati, allora tutti I costrutti vengono interrotti e l'esecuzione riprende dall'istruzione successiva alla keyword done del costrutto più esterno.
Altrimenti l'esecuzione riprende dall'istruzione successiva alla keyword done dell'n-mo ciclo annidato.

Output dello script: Mostra codice

Il costrutto for (segue)

La shell consente di interrompere la lista dei comandi eseguita per uno specifico token utilizzando il comando built-in continue.
Le differenze tra break e continue sono:

  • break implica la terminazione del costrutto e l’esecuzione dell’istruzione successiva alla keyword done.
  • continue implica la mancata esecuzione delle istruzioni fino alla keyword done e l’analisi immediata del token successivo nella lista. In breve, le istruzioni interne al ciclo vengono “saltate” ed il ciclo riprende dal token successivo.
  • Nel seguente esempio: Mostra codice, quando la variabile numero assume valori maggiori o uguali a 3, viene eseguito continue il cui effetto è "saltare" l'istruzione "echo Dopo il test" e passare all'analisi del token successivo.
  • Si noti che, a differenza di break, tutti gli elementi della lista dei valori vengono analizzati dal ciclo.

Output dello script: Mostra codice

Il costrutto while

Il secondo costrutto iterativo è il costrutto while: Mostra codice.
La lista dei comandi inclusa nel blocco do/done viene se e solo se l'exit value di comando è zero
Si ricorda (nuovamente) che a differenza dei linguaggi di programmazione classici, il valore 0 in shell corrisponde a "true".
È possibile utilizzare il comando test condizione, od opzionalmente la sua sintassi [ condizione ], per condizionare l'esecuzione della lista di comandi in base al valore di variabili.
Analogamente al costrutto for:

  • Per posizionare la keyword do sulla stessa riga contenete la condizione è necessario l'utilizzo del delimitatore ";"
  • È possibile utilizzare una sintassi simile al C per specificare la condizione. Un semplice esempio: Mostra codice.
  • La shell definisce il comando built-in true che ritorna immediatamente con exit value pari a zero. È possibile quindi, definire cicli infiniti utilizzando la sintassi while true; do...; done

Il costrutto while (segue)

È possibile utilizzare I comandi break e continue per interrompere l’esecuzione del ciclo o riprendere il ciclo al token successivo.
Attenzione. L’utilizzo di continue in un costrutto di iterazione deve essere molto cauto.
La variabili contenute nella condizione di uscita da un ciclo while (e, come vedremo in until) vengono tipicamente modificate all’interno del blocco do/done.
L’utilizzo di continue prima dell’aggiornamento delle variabili di uscita può portare ad un ciclo infinito: Mostra codice.
La condizione di uscita dal ciclo consiste nel controllo del valore della variabile C che viene aggiornata successivamente alla possibile esecuzione di continue. Quindi, alla prima esecuzione di continue, la variabile di uscita non viene aggiornata e si entra in un ciclo infinito.
Nota: Nel costrutto for, la variabile utilizzata per il controllo di flusso viene (tipicamente) incrementata automaticamente dal costrutto. È possibile, però, definire cicli for in cui la condizione d'uscita dipende da una o più variabili modificate all'interno del blocco do/done. In questi casi medesima attenzione deve essere posta alla problematica appena descritta.

Output dello script: Mostra codice.

Il costrutto until

Il terzo ed ultimo costrutto iterativo è until, la cui sintassi è riportata nel seguente costrutto until: Mostra codice.
La lista dei comandi inclusa nel blocco do/done viene se e solo se l'exit value di comando è diverso da zero.
Può essere inteso come "Esegui la lista di comandi finché comando non viene eseguito con successo".
Analogamente al costrutto while:
È possibile utilizzare il comando test condizione, od opzionalmente la sua sintassi [ condizione ], per condizionare l'esecuzione della lista di comandi in base al valore di variabili.
Per posizionare la keyword do sulla stessa riga contenete la condizione è necessario l'utilizzo del delimitatore ";"
È possibile utilizzare una sintassi simile al C per specificare la condizione. Un semplice esempio: Mostra codice.
Valgono le stesse considerazioni sull'utilizzo di break e continue.

Le funzioni

Come in ogni linguaggio di programmazione, anche la bash prevede la possibilità di definire funzioni utilizzando una delle sintassi indicate nei seguenti esempi: Mostra codice, Mostra codice.
L'invocazione di una funzione avviene semplicemente invocandone il nome. L'invocazione di una funzione è equivalente all'esecuzione di un comando.
La definizione della funzione deve precedere la sua invocazione.
Attenzione a non definire più funzioni con lo stesso nome all'interno dello stesso script. Essendo lo script interpretato dalla shell, la definizione di una nuova funzione con lo stesso nome di una funzione esistente, "semplicemente" rende quest'ultima inaccessibile.
Nota: È ovviamente possibile invocare un numero arbitrario di volte una funzione in uno script.

Le funzioni: Parametri

Le funzioni possono ricevere in input parametri all’atto dell’invocazione e ritornare un exit status.
Per i parametri passati ad una funzione valgono le stesse regole dei parametri passati ad uno script. In particolare:

  • I parametri sono posizionali, i.e., l’i-esimo parametro corrisponde alla variabile $i
  • Il nome della funzione è contenuto nella variabile $0
  • $# contiene il numero di parametri passati
  • $@ contiene la lista dei parametri passati.

Attenzione: I parametri passati allo script non sono visibili all’interno della funzione. Le variabili $1, $2.. si riferiscono ai parametri passati alla funzione, se esistono.

L’exit status di una funzione corrisponde all’exit status dell’ultimo comando eseguito.
È possibile terminare in qualsiasi punto la funzione utilizzando il comando built-in return. Questo comando prende come parametro opzionale un intero compreso tra 1 e 255. Qualora una funzione termina a seguito dell’esecuzione di return n, l’exit status della funzione sarà l’intero n.

Un esempio di passaggio di parametri: Mostra codice.
Output dello script: Mostra codice.

Le funzioni: Variabili

Una variabile è locale quando è visibile solo all’interno di un blocco di codice. Tutte le variabili, se non dichiarate, sono sempre globali.
In taluni casi, però, è utile definire variabili locali a funzioni. In questo caso è necessario dichiarare esplicitamente queste variabili all’interno della funzione utilizzando la keyword local.
Lo script di seguito utilizza due variabili: Mostra codice
La variabile var_globale non è dichiarata e, in quanto tale è globale e visibile dalla funzione. Inoltre le modifiche ad essa apportate nella funzione sono visibili all'interno dello script che invoca fun.
La variabile var_locale, invece, è dichiarata esplicitamente come locale all'interno della funzione. Questo la rende indipendente dalla variabile var_locale utilizzata all'interno dello script. Difatti:

  • Lo script assegna ad entrambe le variabili il valore "GLOBALE".
  • La funzione dichiara la variabile locale var_locale ed assegna ad entrambe le variabili il valore "LOCALE".
  • Al termine dell'esecuzione della funzione, lo script ritrova il valore di var_globale modificato dalla funzione ma il valore di var_locale inalterato.

Output dello script: Mostra codice

Le funzioni: Ricorsione

La shell bash consente l’invocazione ricorsiva delle funzioni. Sebbene questa tecnica consenta di implementare semplicemente alcune funzionalità, il suo utilizzo è fortemente sconsigliato negli script shell a causa dell’enorme overhead per la sua gestione.
L’implementazione di funzioni ricorsive richiede particolare attenzione allo scoping delle variabili.
Di seguito un esempio (sbagliato): Mostra codice, di una funzione ricorsiva per il calcolo del fattoriale.
L'errore in questa funzione sta nel fatto che il valore della variabile globale n viene da un lato modificato all'interno della funzione e, dall'altro utilizzato per il calcolo del valore di ritorno successivamente all'invocazione ricorsiva.
Infatti, la funzione assegna ad n il valore del parametro, esegue una invocazione ricorsiva su n_meno_1. Questa funzione modifica il valore della variabile n, riassegnando il nuovo valore del parametro. Al termine della chiamata ricorsiva, la funzione utilizza il valore (modificato) di n per il calcolo del valore di ritorno.
Una soluzione immediata al problema appena descritto è la dichiarazione della variabile n come locale.

Le funzioni: Ricorsione (segue)

Non è sempre necessario utilizzare variabili locali all’interno delle funzioni ricorsive.
La funzione fact2 è ricavata dalla funzione fact utilizzando I seguenti accorgimenti:

  • La variabile globale n viene utilizzata solo come contenitore di valori.
  • Il valore della variabile globale n non viene utilizzato dopo la chiamata ricorsiva.
  • Il calcolo del valore di ritorno viene effettuato utilizzando il valore ritornato dalla funzione ed il valore del parametro passato alla funzione. Essendo quest’ultimo, per definizione, locale, non risente delle modifiche dovute alla ricorsione.

Una funzione (sbagliata) per il calcolo del fattoriale: Mostra codice.
Una variante corretta della funzione precedente: Mostra codice.

Script interattivi

La shell prevede la possibilità che gli script (come abbiamo ampiamente visto) visualizzino messaggi all’utente. Esiste inoltre la possibilità che uno script richieda input all’utente.
La scelta di rendere uno script interattivo dipende, chiaramente, dal contesto e dal compito cui lo script deve assolvere.
Per la visualizzazione di messaggi è possibile utilizzare il comando built-in echo.

  • Ogni invocazione di echo visualizza una stringa seguita da un newline.
  • È possibile sopprimere il newline specificando l’opzione -n, e.g. echo -n “$a”
  • echo interpreta alcuni caratteri eseguendo operazioni specifiche, ad es. “\a” corrisponde all’emissione di un suono di alert dal terminale.

Script interattivi

Per leggere input da standard input è possibile utilizzare il comando built-in read, la cui sintassi è read [opzioni] var1 var2 var3…

  • La lettura avviene dallo standard input, a meno che questo non sia stato rediretto su file.
  • La shell legge una riga da standard input e suddivide la stringa in token, utilizzando come delimitatore il contenuto della variabile IFS.

Di seguito riportiamo alcune opzioni possibili per la read:

  • -n NUM: La read ritorna dopo aver letto NUM caratteri, senza attendere che la terminazione della riga.
  • -p PROMPT: visualizza la stringa PROMPT senza newline solo nel caso in cui l’input sia atteso da terminale.
  • -t TIMEOUT: Termina il comando read con errore dopo TIMEOUT secondi di inattività.

Un semplice esempio: Mostra codice

I materiali di supporto della lezione

Bash guide for beginners: Capitoli 7, 8 (par 1-2), 9 (par. 1-3,5), 11

Advanced Bash Scripting: Capitoli 10, 23, 33 (par. 1)

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion