Bash scripting - variabili - stringhe

Bash scripting

Sommario

  1. Introduzione
  2. Comandi essenziali
  3. Variabili (stringhe)
  4. Caratteri di escape, apici e virgolette
  5. Espansioni in stringhe quotate
  6. Espansioni non quotabili
  7. Istruzioni composte
  8. Funzioni
  9. File descriptor e redirezioni
  10. Segnali

In bash ogni variabile di default è trattata come una stringa e, benché siano supportati anche interi e array (indicizzati o associativi), questa guida si limita al solo tipo base.

Nomi di variabili

Un nome di variabile ammette soltanto caratteri alfabetici (maiuscoli e minuscoli), l'underscore ('_') e numeri (non in prima posizione). E il suo contenuto si accede con ${nome} oppure con la forma abbreviata $nome.

La forma abbreviata assume che il nome della variabile sia composto da tutti i caratteri validi incontrati. Per esempio la concatenazione "$nome$cognome" è equivalente a "${nome}${cognome}", ma "$nome_$cognome" non lo è a "${nome}_${cognome}" perché nome_ (con underscore finale) sarebbe un nome valido.

Assegnazioni

Non si deve usare il $ davanti alla variabile a cui assegnare. La forma consigliata, salvo necessità particolari, è quella tra virgolette per le stringhe e le concatenazioni di stringhe e variabili, e senza virgolette per una singola variabile:

var="stringa"
var=$var2                   # senza virgolette
var=${var2}                 # equivalente a sopra
var="$var2"                 # come sopra
var="${var1} testo ${var2}" # con virgolette

Altre forme sono possibili, e il loro significato è trattato in seguito:

var=stringa               # assegno una stringa (senza spazi e caratteri speciali)
var=$'stringa con escape' # come sopra, ma con caratteri di escape e senza espansioni
var='stringa senza apici' # niente espansioni, né caratteri di escape
# espansioni...
var=$(comando)            # assegna l'output del comando
var=$(($n * 10))          # assegna il risultato dell'operazione
var=~utente               # assegna la home di utente

Modificatori

Sono comandi interni che possono essere applicati soltanto a un nome di variabile (senza $) o a un'assegnazione, e in quest'ultimo caso hanno effetto sulla variabile dopo l'avvenuta assegnazione.

export
specifica che la variabile farà parte delle variabili d'ambiente (environment) dei comandi esterni eseguiti dallo script:
var="stringa"
export var
# equivalente a:
export var="stringa"
# comando esterno
comando              # può accedere al contenuto di var

È possibile definire una o più variabili nell'ambiente di un comando, senza farle ereditare a quelli successivi, semplicemente scrivendo le assegnazioni prima del nome del comando. Per esempio:

var="stringa" comando # può accedere al contenuto di var
printf %s\\n "$var"   # $var è vuota!
readonly
specifica che la variabile (per convenzione scritta con caratteri maiuscoli) è di sola lettura e dev'essere trattata da quel punto in poi come una costante:
VAR="valore"
readonly VAR

È equivalente a:

readonly VAR="valore"

Per convenzione le costanti sono poste tutte all'inizio dello script, prima anche di eventuali definizioni delle funzioni.

Assegnazione dallo standard input

Con l'istruzione read è possibile assegnare a una o più variabili il contenuto di una riga dello standard input, che senza redirezioni e pipe corrisponde a ciò che viene scritto da tastiera prima di un invio.

Sintassi (base):

read [ -r ] nomevariabile [ ... ]

Il nome delle variabili non va preceduto da $, proprio come nelle assegnazioni normali. Se sono presenti più nomi di variabile, la riga letta si divide in stringhe delimitate dai caratteri contenuti in $IFS (di default sono tre: spazio, tabulazione e invio), ma all'ultima variabile viene sempre assegnato tutto il contenuto rimanente fino a fine riga.

Il carattere \ è speciale e può essere utilizzato per inserire in una variabile un carattere contenuto in $IFS e/o andare su più righe (gli "a capo" saranno rimossi però dalla variabile). Per inserirlo è una volta è quindi necessario digitare \\, in alternativa con l'opzione -r si può trattare normalmente.

Esempio:

printf %s "Scrivi qualcosa e premi invio: "
read -r testo
printf %s\\n "Hai scritto: ${testo}"

Per permettere tramite \ di scrivere più righe, basta rimuovere l'opzione -r:

printf %s "Scrivi qualcosa (se vuoi andare a capo, premi prima "\"; mentre per scrivere "\" nel testo scrivi "\\") e premi invio: "
read testo
printf %s\\n "Hai scritto: ${testo}" # NOTA: tutto su una riga!

Se si vogliono modificare i delimitatori, anziché modificare $IFS, che avrebbe effetti anche riguardo la separazione delle stringhe, è necessario assegnarlo sulla stessa riga dell'istruzione, in modo che sia poi ripristinato al suo valore precedente. Anche così facendo l'istruzione read si fermerà comunque al primo "a capo" trovato.

Per esempio:

printf %s\\n "Scrivi cognome, nome e data di nascita, separati da ',' e premi INVIO."
printf %s "Cognome, Nome, GG/MM/AAAA: "
# $IFS modificata solo per la durata dell'istruzione read
IFS="," read -r cognome nome data  # legge cognome, nome e data, rimuovendo i separatori
                                   # a $nome assegna anche nomi multipli, finché non trova ','
# $IFS ripristinata al suo valore normale

Assegnazione con ciclo

Con l'istruzione for è possibile eseguire un blocco di istruzioni per ogni elemento di una lista di stringhe, assegnando un elemento per volta a una variabile. Nella sintassi POSIX è quindi equivalente al for each di alcuni linguaggi di programmazione.

Sintassi (base):

for nomevariabile [ in ... ]
do
   ...
done

Al solito il nome della variabile non va preceduto da $. Se la parola riservata in e la lista di stringhe sono omesse, allora è equivalente a: in "$@"

Due modi tipici per generare una lista di stringhe, il cui significato sarà trattato successivamente, sono:

  • con la variabile speciale "$@", l'unica che se quotata si espande a una lista di stringhe (i parametri passati allo script o a una funzione);
  • con l'espansione di percorso, per generare una lista di file secondo un dato pattern;

Per esempio:

for file in /percorso/*.txt
do
   if [ -e "$file" ]; then
      # blocco eseguito su ciascun file .txt in /percorso/ tramite "$file"
      ...
   fi
done
  ATTENZIONE
Altre espansioni sono possibili, ma se si utilizza l'espansione di comando per generare una lista di stringhe, bisogna assicurarsi che ogni stringa non contenga spazi oppure caratteri speciali che potrebbero essere espansi nuovamente (* ? [ ]). L'uso combinato con find in particolare, per effettuare ricerche ricorsive e ampliare le funzionalità dell'espansione di percorso, è sconsigliato e quasi sempre sbagliato, a meno che non si sappia a priori che ogni file presente nel percorso scelto soddisfa tali condizioni.


Il ciclo for può essere:

  • interrotto dall'istruzione break;
  • continuato saltando la corrente iterazione del ciclo con continue, che passa al prossimo elemento della lista, se presente, altrimenti esce.

Si ricordi che le istruzioni su più righe possono essere scritte sulla stessa, facendo terminare il comando con il carattere speciale ; come fatto per esempio con if ... ; then. Per i cicli tuttavia scrivere do su una nuova riga ne migliora la leggibilità.

Espansione di variabile

I caratteri utilizzati per la divisione di una stringa in più stringhe sono quelli contenuti nella variabile $IFS (di default appunto: spazio, tabulazione e "a capo"), ed è consigliabile non modificarla, se non eventualmente prima di read, per non alterare il normale funzionamento di uno script.

Con l'unica eccezione dell'assegnazione, quando si accede al contenuto di una variabile senza quotarla, questa può essere trasformata in più di una singola stringa (esplosione) in base agli spazi (e tabulazioni e "a capo") contenuti, e perfino in "niente" se è vuota. "Niente" proprio come se non presente nel codice.

Entrambi i comportamenti non sono intuitivi e costituiscono una comune sorgente di errori. Se si vuole sempre considerare il contenuto della variabile come una singola stringa, è necessario accederla quotata (tra virgolette), ossia con "$variabile" oppure "${variabile}".

Si considerino per esempio i seguenti confronti (usati spesso con if o while):

[ $var = $var2 ]     # SBAGLIATO! (se una delle due è vuota)
[ "$var" = "$var2" ] # corretto
[ -n $var ]          # SBAGLIATO (se var è vuota)
[ -n "$var" ]        # corretto

Ciò è ancora più importante quando si passa la variabile a un comando che agisce su un file indicato dalla variabile, il cui contenuto in presenza di spazi (comuni per i nomi di file degli utenti) potrebbe venir trattato come una lista di file.

Esempio di codice che crea un backup di un file indicato da una variabile (tramite il comando esterno cp):

cp -- "$file" "${file}.bak"
  Suggerimento
L'opzione "--" dopo il comando esterno cp serve per comunicargli che le stringhe che seguono non sono opzioni, nemmeno se iniziassero con il carattere "-". È sempre buona norma utilizzarla come controllo aggiuntivo con comandi che accettano file come argomenti, il cui nome non è noto a priori, come: rm, rmdir, cp, mv, touch, cat, ecc...

L'opzione deve essere supportata dal comando esterno, non è trattata specialmente dalla shell.


D'altra parte accedere una variabile senza quotarla permette di assegnare alla variabile tutte le opzioni da passare a un comando, se sono stringhe senza spazi e caratteri speciali che potrebbero essere espansi nuovamente (* ? [ ]), per poi accederle in una volta sola:

ARGUMENTS="--arg1 --arg2 ..."
...
comando $ARGUMENTS

Si noti che usando "$ARGUMENTS" (quotata) per una variabile contenente la stringa vuota, il comando leggerebbe lo stesso un argomento e potrebbe fallire.

Variabili speciali

$?
contiene il valore di uscita dell'ultimo comando o funzione (0 solo in caso di successo);
$0
contiene il nome usato per lanciare lo script;
$#
contiene il numero di argomenti passati allo script (o a una funzione, all'interno di una funzione);
$1, $2, ...
contengono, se presenti, i parametri passati allo script (o a una funzione);
$@
contiene la lista di tutti i parametri passati allo script corrente o a una funzione. Ogni parametro viene opportunamente quotato, se questa variabile è quotata, e questo ne permette l'utilizzo nei cicli for per processare (ad esempio) una lista di nomi di file che possono contenere anche spazi. L'uso di questa variabile è quindi in genere preferito rispetto a $* che ha la stessa funzione ma, se quotata, non quota i vari parametri ma l'intera stringa;

Esempio:

# in "$@" si può anche omettere (lasciando solo: for file) perché implicito
for file in "$@"
do
    # fare quello che si vuole con "$file"
    ...
done

L'istruzione shift può essere usata per rimuovere i parametri più a sinistra (partendo da $1), spostando tutti gli altri ($1<-$2, $2<-$3, ...), in questo modo si può accedere, come una lista, solo ai successivi.

Per esempio, codice che crea una lista di file in un percorso scelto:

# Sintassi accettata: nomescript destinazione lista_file...
# controllo numero argomenti
if [ $# -lt 2 ]; then
    printf %s\\n "Sono richiesti almeno due argomenti!" >&2
    exit 1
fi
# assegno il primo argomento
destinazine=$1
shift  # ora $* e $@ contengono tutti gli altri parametri ($1<-$2, $2<-$3, ...)
       # ossia la lista di file
# controllo l'esistenza della destinazione
if ! [ -d "$destinazione" ]; then
    printf %s\\n "Directory di destinazione non trovata!" >&2
    exit 2
fi
# eseguo un ciclo su tutti i file della lista
# si ricorda che è implicito: in "$@"
for file
do
    # crea il file
    if ! touch -- "${destinazione}/${file}"; then
        # se fallisce si interrompe e stampa un errore
        printf %s\\n "Impossibile creare il file \"${destinazione}/${file}\"!" >&2
        exit 3
    fi
done
$$
PID del processo corrente;
$!
PID dell'ultimo job in background.

Esempio:

comando &  # lancio un comando in background
pid=$!     # ottengo il PID del comando
...        # eseguo altre operazioni
wait $pid  # attendo la terminazione del comando
status=$?  # catturo il suo exit status