Disclaimer :
La versione originale di questo articolo è stata pubblicata da IBM
developerWorks ed è di proprietà di Westtech Information Services. Questo
documento è una versione aggiornata dell'articolo originale, e contiene
numerosi miglioramenti apportati dal Gentoo Linux Documentation team.
Questo documento non è mantenuto attivamente.
|
Bash per esempi, Parte 2
1.
Ulteriori fondamenti di programmazione bash
Accettare argomenti
Iniziamo con un piccolo suggerimento sulla gestione degli argomenti da linea di
comando, per passare poi ai costrutti fondamentali della programmazione bash.
Nel programma di esempio presentato nell'articolo introduttivo,
abbiamo usato la variabile d'ambiente "$1", che si riferiva al primo argomento
dato da linea di comando. Allo stesso modo, potete usare "$2", "$3", ecc. per
riferirvi ai successivi argomenti passati al vostro script. Ecco un esempio:
Codice 1.1: Riferirsi agli argomenti passati allo script |
#!/usr/bin/env bash
echo il nome dello script è $0
echo il primo argomento è $1
echo il secondo argomento è $2
echo il diciassettesimo argomento è $17
echo il numero degli argomenti è $#
|
L'esempio si spiega da solo, a parte due piccoli dettagli. Per prima cosa, "$0"
si espanderà al nome dello script, così come sarà stato invocato dalla linea di
comando, e "$#"si espanderà al numero degli argomenti passati allo script.
Giocate un po' con lo script qui sopra, passando da linea di comando diversi
tipi di argomenti, in modo da capirne il funzionamento.
A volte, è utile riferirsi a tutti gli argomenti da linea di comando
contemporaneamente. Per questo scopo, bash utilizza la variabile "$@", che si
espande a tutti gli argomenti dati da linea di comando, separati da spazi.
Vedremo un esempio del suo uso un po' più avanti in questo articolo, quando
daremo un'occhiata ai loop "for".
Costrutti di programmazione bash
Se avete già programmato in un linguaggio procedurale come C, Pascal, Python, o
Perl, allora avete già familiarità con i costrutti di programmazione standard,
come i periodi "if", i loop "for" e simili. Bash ha le sue versioni della
maggiorparte di questi costrutti standard. Nelle prossime sezioni, introdurrò
diversi costrutti bash e mostrerò le differenze tra questi e quelli di altri
linguaggi di programmazione che già conoscete. Se non avete programmato molto
prima d'ora, non preoccupatevi: includerò abbastanza informazioni ed esempi da
consentirvi di seguire agevolmente il testo.
Amore condizionale
Se avete programmato almeno una volta codice riguardante un file in C, sapete
che ci vuole uno sforzo consistente per vedere se un particolare file è più
recente di un altro. Questo perché C non ha una sintassi interna per compiere un
confronto di questo tipo; bisogna quindi usare due chiamate stat() e due
strutture stat per effettuare il contronto manualmente. Al contrario, bash ha
degli operatori interni per il confronto standard di file, quindi determinare se
"/tmp/myfile è leggibile" è semplice come controllare se
"$myvar è maggiore di 4".
La seguente tabella elenca gli operatori bash di confronto più usati. Troverete
anche un esempio su come usare ogni opzione correttamente. L'esempio è fatto per
essere messo immediatamente dopo "if". Per esempio:
Codice 1.2: Operatore di confronto bash |
if [ -z "$myvar" ]
then
echo "myvar non è definita"
fi
|
Talvolta, ci sono diversi modi per effettuare lo stesso confronto. Per esempio,
i due seguenti frammenti di codice funzionano in modo identico:
Codice 1.3: Due modi di fare un confronto |
if [ "$myvar" -eq 3 ]
then
echo "myvar è uguale a 3"
fi
if [ "$myvar" = "3" ]
then
echo "myvar è uguale a 3"
fi
|
Nei due confronti qui sopra viene fatta esattamente la stessa cosa, ma il primo
utilizza gli operatori di confronto aritmetico, mentre il secondo usa gli
operatori di confronto delle stringhe.
Caveats di confronto delle stringhe
La maggiorparte delle volte, anche se in teoria potete omettere l'uso delle
virgolette ai lati delle stringhe e delle variabili-stringa, farlo non è una
buona idea. Perché? Perché il vostro codice funzionerà alla perfezione, a meno
che una variabile d'ambiente abbia uno spazio o una tabulazione al suo interno,
nel cui caso bash si confonderà. Ecco un esempio di confronto che dà problemi:
Codice 1.4: Esempio di confronto che dà problemi |
if [ $myvar = "foo bar oni" ]
then
echo "yes"
fi
|
Nell'esempio qui sopra, se myvar è uguale a "foo", il codice funzionerà come ci
aspettiamo e non visualizzerà niente. Tuttavia, se myvar fosse uguale a "foo bar
oni", il codice darebbe il seguente errore:
Codice 1.5: Errore quando una variabile contiene spazi |
[: too many arguments
|
In questo caso, gli spazi in "$myvar" (che è uguale a "foo bar oni") finiscono
per confondere bash. Dopo aver espanso "$myvar", bash effettua il seguente
confronto:
Codice 1.6: Confronto finale |
[ foo bar oni = "foo bar oni" ]
|
Visto che la variabile d'ambiente non è stata racchiusa tra virgolette, bash
pensa che abbiate inserito troppi argomenti nelle parentesi quadre. Potete
facilmente eliminare questo problema mettendo gli argomenti-stringa tra
virgolette. Ricordate che prendendo l'abitudine di mettere tutti gli
argomenti-stringa e le variabili d'ambiente tra virgolette, eliminerete molti
errori di programmazione simili. Ecco come avrebbe dovuto essere stato scritto
il confronto "foo bar oni":
Codice 1.7: Modo giusto di scrivere un confronto |
if [ "$myvar" = "foo bar oni" ]
then
echo "yes"
fi
|
Il codice qui sopra funzionerà come ci si aspetta e non darà nessuna sgradita
sorpresa.
Nota:
Se volete che le variabili d'ambiente vengano espanse, dovete metterle tra
virgolette, anziché tra apici singoli. Gli apici singoli disattivano
l'espansione della variabile (come anche della cronologia).
|
Costrutti di loop: "for"
OK, abbiamo trattato i condizionali; ora è tempo di esplorare i costrutti di
loop bash. Inizieremo con il loop standard "for". Ecco un esempio elementare:
Codice 1.8: Esempio elementare |
#!/usr/bin/env bash
for x in uno due tre quattro
do
echo numero $x
done
numero uno
numero due
numerp tre
numero quattro
|
Cosa è accaduto esattamente? La parte "for x" del nostro loop "for" ha definito
una nuova variabile d'ambiente (detta anche variabile di controllo loop)
chiamata "$x", che è stata successivamente impostata ai valori "uno", "due",
"tre", e "quattro". Dopo ogni assegnazione, il corpo del loop (il codice
nell'intervallo "do" ... "done") è stato eseguito una volta. Nel corpo, ci siamo
riferiti alla variabile di controllo loop "$x" usando la sintassi di espansione
di variabili standard, come per ogni altra variabile d'ambiente. Notate inoltre
che i loop "for" accettano sempre un qualche tipo di lista di parole dopo "in".
In questo caso abbiamo specificato quattro parole italiane, ma la lista di
parole può anche riferirsi a file sul disco, o addirittura a wildcards. Guardate
il seguente esempio, che dimostra come usare le wildcards standard della shell:
Codice 1.9: Usare le wildcards standard della shell |
#!/usr/bin/env bash
for myfile in /etc/r*
do
if [ -d "$myfile" ]
then
echo "$myfile (dir)"
else
echo "$myfile"
fi
done
output:
/etc/rc.d (dir)
/etc/resolv.conf
/etc/resolv.conf~
/etc/rpc
|
Il codice qui sopra esamina ogni file in /etc che inizi con una
"r". Per fare questo, bash prende per prima cosa la nostra wildcard /etc/r* e la
espande, sostituendola con le stringhe /etc/rc.d,
/etc/resolv.conf, /etc/resolv.conf~ e
/etc/rpc, prima di eseguire il loop. Una volta dentro al loop,
l'operatore condizionale "-d" viene usato per compiere due azioni diverse, a
seconda che myfile sia una directory oppure no. Se è una directory, viene
aggiunta una " (dir)" alla riga di output.
Possiamo usare anche wildcards multiple, e perfino variabili d'ambiente, nella
lista di parole:
Codice 1.10: Wildcards multiple e variabili d'ambiente |
for x in /etc/r??? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*
do
cp $x /mnt/mydira
done
|
Bash eseguirà l'espansione delle wildcards e delle variabili nei posti giusti, e
probabilmente creerà una lista di parole molto lunga.
Anche se tutti i nostri esempi sull'espansione delle wildcards hanno usato
percorsi assoluti, potete anche usare percorsi relativi, in questo modo:
Codice 1.11: Usare percorsi relativi |
for x in ../* mystuff/*
do
echo $x is a silly file
done
|
Nell'esempio qui sopra, bash esegue l'espansione della wildcard relativamente
alla directory in cui ci si trova, proprio come quando usate percorsi relativi
sulla linea di comando. Smanettate un po' con l'espansione delle wildcards.
Noterete che se usate percorsi assoluti nella vostra wildcard, bash la espanderà
ad una lista di percorsi assoluti. In caso contrario, bash userà percorsi
relativi nella lista di parole che creerà. Se vi riferite semplicemente a files
nella vostra dicrectory di lavoro attuale (ad esempio, se digitate for x in
*), la lista di files risultante non sarà preceduta da alcuna informazione
di percorso. Ricordate che le informazioni di percorso che precedono il nome del
file possono essere eliminate usando l'eseguibile basename, in questo
modo:
Codice 1.12: Eliminare il percorso che precede il nome del file con basename |
for x in /var/log/*
do
echo `basename $x` is a file living in /var/log
done
|
Certo, spesso è comodo eseguire loops che operino sugli argomenti da linea di
comando di uno script. Ecco un esempio di come usare la variabile "$@",
introdotta all'inizio di questo articolo:
Codice 1.13: Esempio uso della variabile $@ |
#!/usr/bin/env bash
for thing in "$@"
do
echo you typed ${thing}.
done
$ allargs hello there you silly
you typed hello.
you typed there.
you typed you.
you typed silly.
|
Aritmetica nella shell
Prima di esaminare un secondo tipo di costrutto di loop, è una buona idea
familiarizzare con le operazioni aritmetiche nella shell. Sì, è vero: potete
eseguire semplici operazioni tra numeri interi usando i costrutti della shell.
Racchiudete semplicemente l'espressione aritmetica tra un "$((" e un "))", e
bash risolverà l'espressione. Ecco alcuni esempi:
Codice 1.14: Fare conti in bash |
$ echo $(( 100 / 3 ))
33
$ myvar="56"
$ echo $(( $myvar + 12 ))
68
$ echo $(( $myvar - $myvar ))
0
$ myvar=$(( $myvar + 1 ))
$ echo $myvar
57
|
Ora che avete acquisito familiarità con le operazioni matematiche, è tempo di
introdurre altri due costrutti di loop bash, "while" e "until".
Altri costrutti di loop: "while" e "until"
Un periodo "while" viene eseguito fino a quando una particolare condizione è
vera, ed ha il seguente formato:
Codice 1.15: Formato del periodo While |
while [ condizione ]
do
periodi
done
|
I periodi "while" sono di solito usati per eseguire il codice al loro interno un
certo numero di volte, 10 nell'esempio seguente:
Codice 1.16: Iterare il periodo 10 volte |
myvar=0
while [ $myvar -ne 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
|
Potete notare l'uso dell'espansione aritmetica per far diventare falsa la
condizione, facendo terminare il loop.
I periodi "until" hanno la funzione inversa dei periodi "while": vengono
ripetuti finché una perticolare condizione è falsa. Ecco un loop "until" che
funziona esattamente come il loop "while" precedente:
Codice 1.17: Esempio di loop Until |
myvar=0
until [ $myvar -eq 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
|
Periodi Case
I periodi "case" sono un altro costrutto condizionale che può essere utile. Ecco
un esempio:
Codice 1.18: Esempio di periodo Case |
case "${x##*.}" in
gz)
gzunpack ${SROOT}/${x}
;;
bz2)
bz2unpack ${SROOT}/${x}
;;
*)
echo "Formato archivio non riconosciuto."
exit
;;
esac
|
Qui sopra, bash per prima cosa espande "${x##*.}". Nel codice, "$x" è il nome di
un file, e "${x##*.}" ha l'effetto di eliminare tutto il testo tranne quello che
segue l'ultimo punto nel nome del file. Poi, bash confronta la stringa
risultante con i valori elencati a sinistra delle ")". In questo caso,
"${x##*.}" viene confrontato con "gz", poi con "bz2" e infine con "*". Se
"${x##*.}" corrisponde ad una qualsiasi di queste stringhe o pattern, vengono
eseguite le linee immediatamente successive alla ")", fino ai ";;", dopodiché
bash continua ad eseguire le linee dopo l'"esac" finale. Se non ci sono pattern
o stringhe corrispondenti, non viene eseguito alcun codice; tuttavia, in questo
particolare esempio, almeno un blocco di codice verrà eseguito, perché il
pattern "*" acchiapperà qualunque cosa non abbia corrisposto a "gz" o a "bz2".
Funzioni e namespace
In bash potete perfino definire funzioni, simili a quelle di altri linguaggi
procedurali come il Pascal e il C. In bash, le funzioni possono perfino
accettare argomenti, usando un sistema molto simile a quello usato dagli script
per accettare argomenti da linea di comando. Diamo un'occhiata ad un esempio di
definizione di funzione, e poi procediamo da lì:
Codice 1.19: Esempio definizione di funzione |
tarview() {
echo -n "Displaying contents of $1 "
if [ ${1##*.} = tar ]
then
echo "(uncompressed tar)"
tar tvf $1
elif [ ${1##*.} = gz ]
then
echo "(gzip-compressed tar)"
tar tzvf $1
elif [ ${1##*.} = bz2 ]
then
echo "(bzip2-compressed tar)"
cat $1 | bzip2 -d | tar tvf -
fi
}
|
Nota:
Un altro caso: il codice qui sopra avrebbe potuto essere scritto usando un
periodo "case". Riuscite a immaginarvi come?
|
Qui sopra, definiamo una funzione chiamata "tarview" che accetta come argomento
un tarball. Quando la funzione viene eseguita, essa identifica che tipo di
tarball è l'argomento (uncompressed, gzip-compressed, o bzip2-compressed),
visualizza un messaggio informativo di una riga, e poi mostra il contenuto del
tarball. Ecco come dovrebbe essere invocata la suddetta funzione (da uno script
o dalla linea di comando):
Codice 1.20: Invocare la suddetta funzione |
$ tarview shorten.tar.gz
Displaying contents of shorten.tar.gz (gzip-compressed tar)
drwxr-xr-x ajr/abbot 0 1999-02-27 16:17 shorten-2.3a/
-rw-r--r-- ajr/abbot 1143 1997-09-04 04:06 shorten-2.3a/Makefile
-rw-r--r-- ajr/abbot 1199 1996-02-04 12:24 shorten-2.3a/INSTALL
-rw-r--r-- ajr/abbot 839 1996-05-29 00:19 shorten-2.3a/LICENSE
....
|
Come potete vedere, ci si può riferire agli argomenti all'interno della
definizione di funzione usando lo stesso meccanismo usato per riferirsi agli
argomenti da linea di comando. In aggiunta,
la macro "$#" sarà espansa per contenere il numero degli argomenti. L'unica cosa
che potrebbe non funzionare completamente come ci si aspetterebbe è la variabile
"$0", che si espanderà o alla stringa "bash" (se eseguite la funzione dalla
shell, interattivamente) oppure al nome dello script da cui viene chiamata la
funzione.
Nota:
Non dimenticate che le funzioni, come quella sopra, possono essere messe nel
vostro ~/.bashrc o ~/.bash_profile, così da averle disponibili ogni volta che
siete in bash.
|
Namespace
Spesso avrete necessità di creare variabili d'ambiente dentro una funzione.
Sebbene ciò sia possibile, c'è una cosa tecnica che dovreste sapere. Nella
maggiorparte dei linguaggi compilati (come il C), quando create una variabile
dentro una funzione, essa è collocata in un namespace locale separato. Quindi,
se definite in C una funzione chiamata myfunction, e all'interno di essa
definite una variabile chiamata "x", ogni variabile globale (esterna alla
funzione) chiamata "x" non sarà influenzata da essa, rendendo impossibili
eventuali effetti indesiderati.
Se questo è vero in C, non lo è in bash. In bash, ogni qualvolta create una
variabile d'ambiente dentro una funzione, essa viene aggiunta al namespace
globale. Questo significa che essa sovrascriverà qualunque omonima variabile
globale al di fuori della funzione, e continuerà ad esistere anche dopo l'uscita
dalla funzione:
Codice 1.21: Gestione delle variabili in bash |
#!/usr/bin/env bash
myvar="ciao"
myfunc() {
myvar="uno due tre"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
|
Quando questo script viene eseguito, esso produce l'output "uno due tre tre",
mostrando come "$myvar" definita nella funzione abbia sovrascritto la variabile
globale "$myvar", e come la variabile di controllo loop "$x" abbia continuato ad
esistere anche dopo l'uscita dalla funzione (e avrebbe anche sovrascritto
qualsiasi "$x" globale, se fosse stata definita).
In questo semplice esempio, il bug è facile da individuare e da correggere,
usando nomi diversi per le variabili. Tuttavia, questo non è il giusto
approccio; il modo migliore per risolvere questo problema è in primo luogo
quello di prevenire la possibilità sovrascrivere le variabili globali, usando il
comando "local". Quando usiamo "local" per creare variabili all'interno di una
funzione, esse verranno tenute nel namespace locale e non sovrascriveranno
alcuna variabile globale. Ecco come implementare il codice sopra in modo che non
venga sovrascritta nessuna variabile globale:
Codice 1.22: Assicurarsi che nessuna variabile globale venga sovrascritta |
#!/usr/bin/env bash
myvar="ciao"
myfunc() {
local x
local myvar="uno due tre"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
|
Questa funzione produrrà l'output "ciao": la variabile globale "$myvar" non
viene sovrascritta, e "$x" non continua ad esistere al di fuori di myfunc. Nella
prima riga della funzione, creiamo x, una variabile locale che viene usata più
tardi, mentre nel secondo esempio (local myvar="one two three") creiamo una
myvar locale e le assegniamo un valore. La prima forma è comoda per mantenere
locali le variabili di controllo loop, dato che non ci è consentito dire "for
local x in $myvar". Questa funzione non sovrascrive nessuna variabile globale, e
vi consiglio caldamente di strutturare tutte le vostre funzioni in questa
maniera. L'unica volta in cui non dovreste usare "local" è quando volete
esplicitamente modificare una variabile globale.
Per finire
Ora che abbiamo trattato le funzionalità bash più essenziali, è tempo di vedere
come sviluppare un'intera applicazione basata su bash. Nella prossima parte,
faremo solo questo. Ci vediamo!
2.
Risorse
Link utili
|