Gentoo Logo

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

Indice:

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

Output:
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

output:

$ 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



Stampa

Aggiornato il 9 ottobre 2005

Oggetto: Nel suo articolo introduttivo su bash, Daniel Robbins vi ha guidato in alcuni elementi fondamentali di questo linguaggio di scripting, e vi ha spiegato i motivi per cui è utile usare bash. In questa seconda parte, Daniel riparte dal punto in cui era rimasto, guardando ai costrutti fondamentali di bash, come i periodi condizionali (if-then), il looping, e altro ancora.

Daniel Robbins
Autore

Cristiano Chiucchiolo
Traduzione

Donate to support our development efforts.

Copyright 2001-2014 Gentoo Foundation, Inc. Questions, Comments? Contact us.