[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]


47. Bash: programmazione

La programmazione con la shell Bash implica la realizzazione di file script. Alcune istruzioni sono particolarmente utili nella realizzazione di questi programmi, anche se non sono necessariamente utilizzabili solo in questa circostanza.

47.1 Caratteristiche di uno script

Nei sistemi Unix esiste una convenzione attraverso la quale si automatizza l'esecuzione dei file script. Prima di tutto, uno script è un normalissimo file di testo contenente una serie di istruzioni che possono essere eseguite attraverso un interprete. Per eseguire uno script occorre quindi avviare il programma interprete e informarlo di quale script questo deve eseguire. Per esempio, il comando

bash pippo

avvia l'eseguibile `bash' come interprete dello script `pippo'. Per evitare questa trafila, si può dichiarare all'inizio del file script il programma che deve occuparsi di interpretarlo. Per questo si usa la sintassi seguente:

#! <nome-del-programma-interprete>

Quindi, si attribuisce a questo file il permesso in esecuzione. Quando si tenta di avviare questo file come se si trattasse di un programma, il sistema avvia in realtà l'interprete.

Perché tutto possa funzionare, è necessario che il programma indicato nella prima riga dello script sia raggiungibile così come è stato indicato, cioè sia provvisto del percorso necessario. Per esempio, nel caso di uno script per la shell Bash (`/bin/bash'), la prima riga sarà la seguente:

#!/bin/bash

Il motivo per il quale si utilizza il simbolo `#' iniziale, è quello di permettere ancora l'utilizzo dello script nel modo normale, come argomento del programma interprete: rappresentando un commento non interferisce con il resto delle istruzioni.

Come appena accennato, il simbolo `#' introduce un commento che termina alla fine della riga, cioè qualcosa che non ha alcun valore per l'interprete; inoltre, le righe vuote e quelle bianche vengono ignorate nello stesso modo.

47.2 Strutture

Per la formulazione di comandi complessi si possono usare le tipiche strutture di controllo e di iterazione dei linguaggi di programmazione più comuni. Queste strutture sono particolarmente indicate per la preparazione di script di shell, ma possono essere usate anche nella riga di comando di una shell interattiva.

È importante ricordare che il punto e virgola singolo (`;') viene utilizzato per indicare un punto di separazione e può essere rimpiazzato da uno o più codici di interruzione di riga.

47.2.1 for

Il comando `for' esegue una scansione di elementi e in corrispondenza di questi esegue una lista di comandi.

for <variabile> [in <valore>...]
do
    <lista-di-comandi>
done

L'elenco di parole che segue `in' viene espanso, generando una lista di elementi. La variabile indicata dopo `for' viene posta, di volta in volta, al valore di ciascun elemento di questa lista, e la lista di comandi che segue `do' viene eseguita ogni volta. Se `in' (e i suoi argomenti) viene omesso, il comando `for' esegue la lista di comandi (`do') una volta per ogni parametro posizionale esistente (`$1', `$1',...). In pratica è come se fosse stato usato: `in $@'.

Il valore restituito da `for' è quello dell'ultimo comando eseguito all'interno della lista `do', oppure zero se nessun comando è stato eseguito.

Esempi

L'esempio seguente mostra uno script che, una volta eseguito, emette in sequenza gli argomenti che gli sono stati forniti.

#!/bin/bash
for i in $*
do
    echo $i
done

L'esempio seguente mostra uno script un po' più complicato che si occupa di archiviare ogni file e directory indicati come argomenti.

#!/bin/bash
ELENCO_DA_ARCHIVIARE=$*
for DA_ARCHIVIARE in $ELENCO_DA_ARCHIVIARE
do
    tar czvf ${DA_ARCHIVIARE}.tgz $DA_ARCHIVIARE
done

47.2.2 select

Il comando `select' permette all'utente di effettuare una scelta inserendo un valore attraverso la tastiera. `select' è stato ereditato dalla shell Korn.

select <variabile> [in <valore>...]
do
    <lista-di-comandi>
done

L'elenco di parole che segue `in' viene espanso, generando una lista di elementi. L'insieme delle parole espanse viene emesso attraverso lo standard error, ognuna preceduta da un numero. Se `in' (e i suoi argomenti) viene omesso, vengono utilizzati i parametri posizionali (`$1', `$2',...). In pratica è come se fosse stato usato `in $@'.

Dopo l'emissione dell'elenco, viene mostrato l'invito contenuto nella variabile `PS3' e viene letta una riga dallo standard input. Se la riga consiste del numero corrispondente a una delle parole mostrate, allora il valore della variabile indicata dopo `select' viene posto a quella parola (cioè quella parola viene assegnata alla variabile). Se la riga è vuota (probabilmente è stato premuto soltanto [Invio]), l'elenco e l'invito vengono emessi nuovamente. Se viene letto il codice corrispondente a EOF ([Ctrl+d]), il comando termina. Qualsiasi altro valore letto fa sì che la variabile sia posta al valore della stringa nulla. La riga letta viene salvata nella variabile `REPLY'. La lista di comandi che segue `do' viene eseguita dopo ciascuna selezione fino a che non viene incontrato un comando `break' o `return'.

Il valore restituito da `select' è quello dell'ultimo comando eseguito all'interno della lista `do', oppure zero se nessun comando è stato eseguito.

Esempi

L'esempio seguente mostra uno script che fa apparire un menu composto dagli argomenti fornitigli; a ogni selezione mostra quello scelto.

#!/bin/bash
select i in $*
do
    echo "hai selezionato $i premendo $REPLY"
    echo ""
    echo "premi Ctrl+c per terminare"
done

47.2.3 case

Il comando `case' permette di eseguire una scelta nell'esecuzione di varie liste di comandi. La scelta viene fatta confrontando una parola (di solito una variabile) con una serie di modelli. Se viene trovata una corrispondenza con uno dei modelli, la lista di comandi relativa viene eseguita.

case <parola> in
    [<modello> [ | <modello>]... ) <lista-di-comandi> ;; ]
    ...    
    [*) <lista-di-comandi> ;; ]
esac

La parola che segue `case' viene espansa e quindi confrontata con ognuno dei modelli, usando le stesse regole dell'espansione di percorso (i nomi dei file). La barra verticale (`|') viene usata per separare i modelli quando questi rappresentano possibilità diverse di un'unica scelta.

Quando viene trovata una corrispondenza, viene eseguita la lista di comandi corrispondente. Dopo il primo confronto riuscito, non ne vengono controllati altri dei successivi. L'ultimo modello può essere `*)', che corrisponde a qualunque valore, e si può usare come alternativa finale in mancanza di altro.

Il valore restituito è zero se nessun modello combacia. Altrimenti, è lo stesso valore restituito dall'ultimo comando eseguito, contenuto all'interno della lista.

Esempi

L'esempio seguente mostra uno script che fa apparire un messaggio diverso a seconda dell'argomento fornitogli.

#!/bin/bash
case $1 in
    -a | -A | --alpha)    echo "alpha"    ;;
    -b)                   echo "bravo"    ;;
    -c)                   echo "charlie"  ;;
    *)			  echo "opzione sconosciuta" ;;
esac

Come si può notare, per selezionare `alpha' si possono utilizzare tre opzioni diverse.

47.2.4 if

Il comando `if' permette di eseguire liste di comandi differenti, in funzione di una o più condizioni, espresse anch'esse in forma di lista di comandi.

if <lista-condizione>
then
    <lista-di-comandi>
[elif <lista-condizione>
then
    <lista-di-comandi>]
...
[else
    <lista-di-comandi>]
fi

Inizialmente viene eseguita la lista che segue `if' che costituisce la condizione. Se il valore restituito da questa lista è zero (cioè Vero), allora viene eseguita la lista seguente `then' e il comando termina. Altrimenti viene eseguita ogni `elif' in sequenza, fino a che ne viene trovata una la cui condizione si verifica. Se nessuna condizione si verifica, viene eseguita la lista che segue `else', sempre che esista.

Il valore restituito è quello dell'ultimo comando eseguito, oppure zero se non ne è stato eseguito alcuno.

Esempi

L'esempio seguente mostra uno script che fa apparire un messaggio di avvertimento se non è stato utilizzato alcun argomento, altrimenti si limita a visualizzarli.

#!/bin/bash
if [ $# = 0 ]
then
    echo "devi fornire almeno un argomento"
else
    echo $*
fi

L'esempio seguente mostra uno script attraverso il quale si tenta di creare una directory e se l'operazione fallisce viene emessa una segnalazione di errore.

#!/bin/bash
if ! mkdir deposito
then
    echo "Non è stato possibile creare la directory \"deposito\""
else
    echo "È stata creata la directory \"deposito\""
fi

47.2.5 while

Il comando `while' permette di eseguire un gruppo di comandi in modo ripetitivo mentre una certa condizione continua a dare il risultato Vero.

while <lista-condizione>
do
    <lista-di-comandi>
done

Il comando `while' esegue ripetitivamente la lista che segue `do' finché la lista che rappresenta la condizione continua a restituire il valore zero (Vero).

Il valore restituito dal comando è lo stesso di quello della lista che segue `do', oppure zero se la condizione non si è mai verificata.

Esempi

#!/bin/bash
RISPOSTA="continua"
while [ $RISPOSTA != "fine" ]
do
    echo "usa la parola fine per terminare"
    read RISPOSTA
done

All'interno dei comandi composti si utilizzano spesso delle condizioni racchiuse tra parentesi quadre. L'uso delle parentesi quadre è una forma abbreviata del comando interno `test'.

47.2.6 until

Il comando `until' permette di eseguire un gruppo di comandi in modo ripetitivo mentre una certa condizione continua a dare il risultato Falso.

until <lista-condizione>
do
    <lista-di-comandi>
done

Il comando `until' è analogo a `while', cambia solo l'interpretazione della lista che rappresenta la condizione nel senso che il risultato di questa viene invertito (negazione logica).

47.2.7 Funzioni

Attraverso le funzioni è possibile dare un nome a un gruppo di liste di comandi, in modo da poterlo richiamare come si fa per un comando interno normale. Sotto questo aspetto, le funzioni vengono impiegate normalmente all'interno di file script.

[function] <nome> () {
    <lista-di-comandi>
}

Le funzioni vengono eseguite nel contesto della shell corrente e quindi non vengono attivati altri processi per la loro interpretazione (ciò al contrario di quanto capita quando viene avviata l'interpretazione di un nuovo script).

La lista di comandi viene eseguita ogni volta che il nome della funzione è utilizzato come comando. Il valore restituito dalla funzione è quello dell'ultimo comando a essere eseguito all'interno di questa.

Quando viene eseguita una funzione, i parametri posizionali contengono gli argomenti di questa funzione e anche `$#' restituisce un valore corrispondente alla situazione. `$0' continua a restituire il valore precedente, di solito il nome dello script.

All'interno della funzione possono essere dichiarate delle variabili locali attraverso il comando interno `local'.

È possibile utilizzare il comando interno `return' per concludere anticipatamente l'esecuzione della funzione. Al termine dell'esecuzione della funzione, i parametri posizionali riprendono il loro contenuto precedente e l'esecuzione dello script riprende dal comando seguente alla chiamata della funzione.

Le funzioni possono essere esportate e rese disponibili a una subshell utilizzando il comando interno `export'.

Esempi

L'esempio seguente mostra uno script che prima dichiara una funzione denominata `messaggio' e subito dopo l'esegue semplicemente nominandola come un comando qualsiasi.

#!/bin/bash
messaggio () {
    echo "ciao,"
    echo "bella giornata vero?"
}

messaggio

Nell'esempio seguente, una funzione si occupa di emettere il riepilogo della sintassi per l'uso di un ipotetico script.

function sintassi () {
    echo "al {-latex | -html | -txt [-letter -a4] } [-check]"
    echo ""
    echo "-latex        esegue la conversione in latex;"
    echo "-html         esegue la conversione in html;"
    echo "-txt          esegue la conversione in testo normale;"
    echo "-check        esegue il controllo sintattico SGML;"
}

Nell'esempio seguente, si utilizza il comando `return' per fare in modo che l'esecuzione della funzione termini in un punto determinato restituendo un valore stabilito. Lo scopo dello script è quello di verificare che esista il file `pippo' nella directory `/var/log/packages/'.

#!/bin/bash
function verifica() {
    if [ -e "/var/log/packages/$1" ]
    then
        return 0
    else
        return 1
    fi
}

if verifica pippo
then
    echo "il pacchetto pippo esiste"
else
    echo "il pacchetto pippo non esiste"
fi	

47.3 Espressioni aritmetiche

La shell consente di risolvere delle espressioni aritmetiche in certe circostanze. Il calcolo avviene su interi senza controllo dell'overflow, anche se la divisione per zero viene intercettata e segnalata come errore. Oltre alle espressioni puramente aritmetiche si possono risolvere espressioni logiche e binarie, anche se l'utilizzo di queste ultime non è indicato. La tabella 47.1 riporta l'elenco degli operatori aritmetici disponibili.

Operatore e operandi Descrizione
+<op> Non ha alcun effetto.
-<op> Inverte il segno dell'operando.
<op1> + <op2> Somma i due operandi.
<op1> - <op2> Sottrae dal primo il secondo operando.
<op1> * <op2> Moltiplica i due operandi.
<op1> / <op2> Divide il primo operando per il secondo.
<op1> % <op2> Modulo: il resto della divisione tra il primo e il secondo operando.
<var> = <valore> Assegna alla variabile il valore alla destra.
<op1> += <op2> `<op1> = <op1> + <op2>'
<op1> -= <op2> `<op1> = <op1> - <op2>'
<op1> *= <op2> `<op1> = <op1> * <op2>'
<op1> /= <op2> `<op1> = <op1> / <op2>'
<op1> %= <op2> `<op1> = <op1> % <op2>'

Tabella 47.1: Operatori aritmetici della shell Bash.

Le variabili di shell possono essere utilizzate come operandi; l'espansione di parametri e variabili avviene prima della risoluzione delle espressioni. Quando una variabile o un parametro vengono utilizzati all'interno di un'espressione, vengono convertiti in interi. Una variabile di shell non ha bisogno di essere convertita.

La forma generale per esprimere un numero è quella seguente:

[<base>#]n

In tal modo si può specificare esplicitamente la base di numerazione (va da un minimo di due a un massimo di 64). Se non viene espressa, si intende base 10. Per le cifre numeriche superiori al numero nove, si utilizzano le lettere minuscole, le lettere maiuscole, il simbolo `_' e infine `@', in questo ordine. Se la base di numerazione è inferiore o uguale a 36, non conta più la differenza tra lettere maiuscole e minuscole dal momento che non esiste la necessità di rappresentare un numero elevato di cifre. Una costante che inizia con uno zero viene interpretata come un numero ottale, mentre se inizia per 0x... o 0X... si considera rappresentare un numero esadecimale.

Gli operatori sono valutati in ordine di precedenza. Le sottoespressioni tra parentesi sono risolte prima.

47.4 Riferimenti particolari alle variabili

L'espansione normale delle variabili è già stata vista nella sezione 45.3.4, ma la shell Bash offre in particolare dei modi alternativi, derivati dalla shell Korn, utili particolarmente per la programmazione.

Le parentesi graffe usate negli schemi sintattici delle sezioni seguenti, fanno parte delle espressioni, come si può osservare dagli esempi.

47.4.1 Segnalazione di errore

${<parametro>:?<parola>}

${<variabile>:?<parola>}

Definisce un messaggio, rappresentato dalla parola, da emettere attraverso lo standard error nel caso il parametro o la variabile non siano stati definiti o siano pari alla stringa nulla.

Esempi

Si suppone che la variabile `Nessuno' non sia stata definita o sia pari alla stringa nulla.

echo "${Nessuno:?Variabile non definita}"[Invio]

bash: Nessuno: Variabile non definita

47.4.2 Valore predefinito

${<parametro>:-<parola>}

${<variabile>:-<parola>}

Definisce un valore predefinito, corrispondente alla parola indicata, nel caso che il parametro o la variabile non siano definiti o siano pari alla stringa nulla.

Esempi

echo "${99:-ciao}"[Invio]

ciao

47.4.3 Rimpiazzo

${<parametro>:+<parola>}

${<variabile>:+<parola>}

Definisce un valore alternativo, corrispondente alla parola indicata, nel caso che il parametro o la variabile siano definiti e siano diversi dalla stringa nulla.

Esempi

Pippo=""[Invio]

echo "${Pippo:+pappa}"[Invio]

Il risultato è una riga vuota.

Pippo="ciao"[Invio]

echo "${Pippo:+pappa}"[Invio]

pappa

47.4.4 Lunghezza del contenuto

Questo tipo di sostituzione riguarda solo la shell Bash.

${#<parametro>}

${#<variabile>}

Corrisponde alla lunghezza in caratteri del valore contenuto all'interno del parametro o della variabile. Se però si tratta del parametro `*' o `@' il valore è pari al numero dei parametri posizionali presenti.

Esempi

Pippo="ciao"[Invio]

echo "${#Pippo}"[Invio]

4

47.4.5 Valore predefinito con assegnamento

${<variabile>:=<parola>}

Assegna alla variabile il valore indicato dalla parola, nel caso che la variabile non sia definita o sia pari alla stringa nulla. In pratica, rispetto alla sintassi `${<variabile>:-<parola>}' si ottiene in più l'assegnamento della variabile.

47.5 Array

Oltre alle variabili scalari normali, si possono utilizzare degli array dinamici a una sola dimensione. Con questo tipo di array non è necessario stabilire la dimensione: basta assegnare un valore in una posizione qualunque e l'array viene creato. Per esempio,

elenco[3]="Quarto elemento"

crea un array contenente solo l'elemento corrispondente all'indice tre, il quale, a sua volta, contiene la frase «Quarto elemento».

Gli array della shell Bash hanno base zero, cioè il primo elemento si raggiunge con l'indice zero (ecco perché nell'esempio, la frase parla di un quarto elemento).

È possibile creare un array anche usando il comando interno `declare' o `local' con l'opzione `-a'.

È possibile assegnare tutti i valori degli elementi di un array in un colpo solo. Si utilizza la notazione seguente:

<array>=(<valore-1> <valore-2> ... <valore-n>)

I valori indicati tra parentesi, a loro volta, possono essere espressi nella forma seguente (le parentesi quadre fanno parte dell'istruzione).

[<indice>]=<stringa> | <stringa>

La sintassi chiarisce che è possibile sia indicare esplicitamente l'indice dell'elemento da assegnare, sia farne a meno e quindi lasciare che sia semplicemente la posizione dei valori a stabilire l'elemento rispettivo che dovrà contenerli.

47.5.1 Espansione con gli array

Per fare riferimento al contenuto di una cella di un array si utilizza la notazione seguente (le parentesi quadre fanno parte dell'istruzione).

${<array>[<indice>]}

Se si legge un array come se fosse una variabile scalare normale, si ottiene il contenuto del primo elemento (zero). Per esempio, `$pippo[0]' è esattamente uguale a `$pippo'.

È possibile espandere gli elementi di un array tutti contemporaneamente. Per questo si utilizza il simbolo `*', oppure `@', al posto dell'indice. Se si utilizza l'asterisco si ottiene una sola parola, mentre utilizzando il simbolo `@' si ottiene una parola per ogni elemento dell'array.

47.5.2 Lettura della dimensione

Per ottenere la dimensione di un array si utilizza una delle due notazioni seguenti (le parentesi quadre fanno parte dell'istruzione).

${#<array>[*]}

${#<array>[@]}

47.5.3 Eliminazione

Come nel caso delle variabili scalari, il comando `unset' permette di eliminare un array. Se però si fa riferimento a un elemento particolare di questo, si elimina solo quello, senza annullare l'intero array.

---------------------------

Appunti Linux 2000.04.12 --- Copyright © 1997-2000 Daniele Giacomini --  daniele @ pluto.linux.it


[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]