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

44. 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.

44.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.

44.1.1 Commenti

Come appena accennato, il simbolo # introduce un commento che termina alla fine della riga, cioè qualcosa che non ha alcun valore per l'interprete.

44.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.

44.2.1 for

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

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

for <variabile> [in <parola>...]
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 il suo argomento) 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

44.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 <parola>...] ; do <lista-di-comandi> ; done

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

L'elenco di parole che segue la 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 il suo argomento) 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 carattere <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 selezionato.

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

44.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> ;; ]... esac

case <parola> in
	[<modello> [ | <modello> ]... ) <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.

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"  ;;
esac

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

44.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

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

44.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

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.

44.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

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).

44.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>... ; }

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

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

Il gruppo di liste viene eseguito 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 nuovo valore di conseguenza. $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 qualsiasi comando.

#!/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 [-letter -a4] | -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;"
        echo "    -a4       genera un documento in formato A4;"
        echo "    -letter   genera un documento in formato Lettera."
}

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	

44.3 Espressioni aritmetiche

La shell consente di risolvere delle espressioni aritmetiche in certe circostanze. Il calcolo avviene su interi di tipo long (secondo il linguaggio C) senza controllo dell'overflow, anche se la divisione per zero viene intercettata e segnalata come errore.

Oltre alle espressioni puramente aritmetiche si possono anche risolvere espressioni logiche e binarie, anche se l'utilizzo di queste ultime non è indicato.

La tabella 44.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 44.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 la base di numerazione in modo esplicito (va da un minimo di 2 a un massimo di 64). Se non viene espressa, si intende base 10.

Per le cifre numeriche superiori al numero 9, 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.

44.4 Riferimenti particolari alle variabili

L'espansione normale delle variabili è già stata vista nella sezione 42.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.

44.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

44.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

44.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

44.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

44.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.

44.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 3, il quale a sua volta, contiene la frase «Quarto elemento».

Gli array della shell Bash hanno base 0, cioè il primo elemento si raggiunge con l'indice 0 (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 il rispettivo elemento che dovrà contenerli.

44.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.

44.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>[@]}

44.5.3 Eliminazione

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

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

Appunti Linux 1999.09.21 --- Copyright © 1997-1999 Daniele Giacomini --  daniele @ pluto.linux.it


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