Bash scripting - espansioni quotabili

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

Le espansione attivate da $ avvengono con la stessa priorità, e in una stringa quotata sono le uniche permesse, quindi il risultato di un'espansione non può mai essere espanso un'altra volta. Sono permesse le sole espansioni di variabile, già vista, e di parametro, di comando e aritmetica (intera), che saranno trattate in seguito.

Espansione di parametro (stringa)

È una forma modificata dell'espansione di variabile, che permette di operare sulla stringa contenuta con un modificatore.

L'espansione di parametro è utilizzabile ogni volta che lo è quella di variabile, con un'unica differenza: l'espansione di variabile può essere contenuta in alcune espansioni di parametro, purché non al posto del nome della variabile, mentre non è mai possibile annidare più espansioni di parametro.

Modificatori:

  • ${#var} ritorna il numero di caratteri della stringa contenuta in ${var}. Espande sempre a una singola stringa;
  • ${!var} (non POSIX) ritorna il contenuto della variabile, il cui nome è contenuto in ${var} (accesso indiretto);
  • ${var:-stringa} (valore di default, se nulla) ritorna il contenuto della variabile, se definita e non è nulla, altrimenti espande stringa, che può essere anche un'altra variabile. Non cambia il contenuto di ${var};
  • ${var-stringa} (valore di default, se non definita) ritorna il contenuto della variabile, se definita (anche se nulla), altrimenti espande stringa, che può anche essere un'altra variabile. Non cambia il contenuto di ${var};
  • modificatori di assegnazione e valori alternativi, non trattati perché sconsigliati per la leggibilità del codice e non sono fondamentali. Si noti invece che l'uso dell'espansione di parametro con valore di default è l'unico modo per distinguere una variabile non definita da una nulla;
  • manipolatori di stringa (rimozione, sostituzione, trasformazione in uppercase/lowercase), trattati tra breve.

Esempio:

var2=${var-stringa}   # assegna stringa (perché $var non è definita)
printf %s\\n "$var2"  # stampa: stringa
var=""                # assegna a var una stringa nulla
var2=${var-stringa}   # assegna il contenuto (nullo) di $var (perché è definita)
printf %s\\n "$var2"  # stampa una riga vuota
var2=${var:-stringa}  # assegna stringa (perché $var è nulla)
printf %s\\n "$var2"  # stampa: stringa
rif="var2"            # rif contiene il nome (senza $) di $var2
var=${!rif}           # equivalente a: var=$var2
printf %s\\n "$var"   # stampa: stringa
printf %s\\n ${#var}  # stampa: 7 (la lunghezza di "stringa")

Se si utilizza l'opzione -u (si legga la parte sul debug integrato), allora tutte le variabili devono essere definite prima di poter essere utilizzate, con l'unica eccezione delle espansioni di parametro con valore di default. Per esempio controlla che una variabile non sia nulla, anche se non definita e con script eseguito con l'opzione -u:

if [ -n "${var:-}" ]; then
   ...
fi

Manipolazione delle stringhe

Per manipolare una stringa, è possibile assegnarla a una variabile per poi effettuarne un'espansione di parametro che manipola la stringa, senza modificare il contenuto della variabile. Per esempio:

VAR="stringa-di-esempio"
printf %s\\n "${VAR#stringa-}"

ritorna il contenuto della variable VAR senza il prefisso "stringa-". VAR non viene modificata, salvo una nuova assegnazione:

VAR=${VAR#stringa-}

ora il prefisso "stringa-" è stato eliminato anche dalla variabile VAR.

I modificatori sono molti, ma possono essere facilmente ricordati se si imparano i fondamentali:

  • # sottrae dall'inizio della stringa (minimale);
  • % sottrae dalla fine della stringa (minimale);
  • / (non POSIX) sostituisce una sottostringa con un'altra (solo la prima volta che viene incontrata);
  • ^ (non POSIX) trasforma in maiuscola la prima lettera della stringa (solo il primo carattere);
  • , (non POSIX) trasforma in minuscola la prima lettera della stringa (solo il primo carattere).

Questi operatori sono minimali, questo vuol dire che se si usano le espressioni regolari per indicare la sottostringa (da eliminare, sostituire o trasformare) verrà individuata in caso di ambiguità la sottostringa più piccola (o solo la prima nel caso di sostituzione o trasformazione).

Per ottenere gli operatori massimali basta raddoppiare il simbolo:

  • ## sottrae dall'inizio della stringa (massimale);
  • %% sottrae dalla fine della stringa (massimale);
  • // (non POSIX) sostituisce una sottostringa con un'altra (tutte le volte che viene incontrata);
  • ^^ (non POSIX) trasforma in maiuscola la stringa (tutta);
  • ,, (non POSIX) trasforma in minuscola la stringa (tutta).

Gli operatori massimali cercano di individuare la sottostringa più grande che corrisponde all'espressione regolare, mentre nel caso del modificatore // tutte le sottostringhe vengono sostituite, e nel caso della trasformazione tutta la stringa e non solo il primo carattere.

Si noti che le stringhe interne a un'espansione di parametro possono essere delle variabili, ma non altre espansioni di parametro:

# cambia l'estensione nella variabile file
if [ "$file" != "${file%${estensione}}" ]; then
   file=${file%${estensione}}${nuova_estensione}
fi

Le espressioni regolari supportate sono le stesse permesse nelle espansioni di parametro e con lo stesso significato (ossia: ? * [ ]), e con gli operatori di sottrazione c'è differenza tra quelli minimali e massimali soltanto se il pattern contiene uno o più *, che è l'unico carattere speciale che può sostituire un numero qualsiasi (zero o più) di caratteri.

Esempio: alternativa a basename

Quando in uno script ci si deve riferire al nome dello script stesso, è usuale utilizzare il comando esterno basename. Una possibile alternativa:

usage () {
   printf %s\\n "Usage: ${0##*/}"
}

Esempi: manipolazione delle stringhe

VAR="questa.sarebbe.UNA.stringa.di.esempio"
  
                             # Risultato:
  
printf %s\\n "${VAR#*.}"     # sarebbe.UNA.stringa.di.esempio
printf %s\\n "${VAR##*.}"    # esempio
  
printf %s\\n "${VAR%.*}"     # questa.sarebbe.UNA.stringa.di
printf %s\\n "${VAR%%.*}"    # questa
  
printf %s\\n "${VAR/st/LL}"  # queLLa.sarebbe.UNA.stringa.di.esempio
printf %s\\n "${VAR//st/LL}" # queLLa.sarebbe.UNA.LLringa.di.esempio

printf %s\\n "${VAR^*}"      # Questa.sarebbe.UNA.stringa.di.esempio
printf %s\\n "${VAR^^*}"     # QUESTA.SAREBBE.UNA.STRINGA.DI.ESEMPIO

printf %s\\n "${VAR,*}"      # questa.sarebbe.UNA.stringa.di.esempio (invariata)
printf %s\\n "${VAR,,*}"     # questa.sarebbe.una.stringa.di.esempio

Per una spiegazione dettagliata di tutti i modificatori e anche di altri modi di manipolare le stringhe in Bash (ad esempio expr) vedere:

Espansione di comando

Consiste nel trasformare l'output di un comando qualsiasi (interno della shell, esterno, una funzione e anche forme composte) in argomenti per un altro comando, oppure nel valore da assegnare a una variabile. Si effettua racchiudendo un comando tra $(...):

$(comando)

oppure, meno leggibile e sconsigliata tra `...` (su tastiera con layout italiano: Alt Gr + '):

`comando`

Inoltre la prima forma può essere annidata facilmente, mentre la seconda richiederebbe un livello aggiuntivo di escape.

Il comando è eseguito in una subshell e non può modificare quindi le variabili in nessun caso. Il suo output consiste in zero, una o più stringhe: in base agli spazi presenti nell'output prodotto, e in maniera analoga all'espansione a cui sono soggette le variabili. Per trasformare l'output di un comando in una singola stringa è necessario che l'espansione di comando sia quotata, con l'eccezione dell'assegnazione a una variabile.

Esempi di assegnazione:

# assegna alla variabile $oggi la data in formato YYYY_MM_DD
oggi=$(date '+%F')   # senza virgolette
oggi="$(date '+%F')" # equivalente a sopra (non servono nelle assegnazioni)

# associo a testo la dimensione in byte di $file
testo=$(wc -c -- $file)     # SBAGLIATO! (se la variabile $file contiene spazi o caratteri speciali)
testo=$(wc -c -- "$file")   # le virgolette attorno alla variabile sono necessarie
testo="$(wc -c -- "$file")" # equivalente a sopra
# testo contiene anche altre stringhe, ma sono interessato solo alla prima
bytes=${testo%% *}          # estraggo il primo argomento (espansione di parametro)

Si noti che se il file non esiste, il comando restituirebbe un exit status diverso da zero, ma al solito l'esecuzione dello script non sarebbe interrotta. Andrebbe quindi sempre controllato ogni exit status dei comandi nel seguente modo:

if ! testo=$(wc -c -- "$file"); then
    # eseguita se l'assegnazione fallisce (se il comando nell'espansione fallisce)
    printf %s\\n "ERRORE: file (${file}) non esistente!" >&2
    exit 1
fi
# la variabile testo è definita
bytes=${testo%% *}          # estraggo il primo argomento

Mentre invece date non può fallire in condizioni normali, se non per errori di sintassi di chi ha scritto lo script, per cui non è necessario controllarne l'exit status. Implicitamente si assume che, se si utilizza bash, allora tutti i comandi di sistema siano sufficientemente avanzati da supportare anche opzioni non previste da POSIX o di trovarsi su una distribuzione GNU/Linux. Si noti però che il comando date potrebbe fallire su altri sistemi, a seconda della versione installata, se non ci si limita alle opzioni previste da POSIX per il comando.

Passaggio dell'output dei comandi come argomento:

# stampa stati
printf %s\\n "Login name: $(logname); Name: $(whoami); UID: $(id -ur); EUID: $(id -u); Groups: $(groups)"
printf %s\\n "OS: Debian GNU/Linux $(cat /etc/debian_version) ($(lsb_release -sc))" # uguale a $(lsb_release -sd)
printf %s\\n "Kernel: $(uname) $(uname -r) ($(uname -v))"

Vale la stessa considerazione sugli exit status, ma alcuni comandi non possono fallire, se non in presenza di problemi del sistema operativo stesso, e il loro exit status non è generalmente controllato.

Per decidere se l'exit status di un comando sia da controllare o meno è in genere sufficiente chiedersi se può fallire per azioni che possono (potenzialmente) essere compiute dall'utente, come fornire un percorso di file non valido o inesistente. Altre possibilità sono in genere ignorate, perché se si volesse una compatibilità ancora maggiore probabilmente non si starebbe usando bash in primo luogo.

Output con a capo finali

Si noti che l'espansione di comando, come anche in sh (POSIX), non espande il comando a tutto l'output prodotto, ma omette sempre gli "a capo" finali. Se da una parte è utile nella maggior parte delle situazioni, può talvolta avere effetti collaterali difficili da prevedere. Si consideri per esempio:

printf %s\\n ciao > ./file_prova # scrive ciao e un "a capo" nel file_prova
testo=$(cat ./file_prova)        # associa il contenuto (SENZA "a capo") a $testo
printf %s\\n "Bytes: ${#testo}"  # NON è la dimensione esatta del file!
printf %s\\n "$testo" |          # invia la stringa a cmp (più un "a capo") per un confronto
  cmp - ./file_prova             # nessun errore!!!
printf %s\\n $?                  # infatti stampa 0
  ATTENZIONE
Una variabile non può contenere il carattere ASCII numero 0, quindi per i file binari è sempre sbagliato accedervi in questo modo. È sconsigliabile anche se non si è certi della loro dimensione.

Se si è interessati soltanto alla prima riga di un file di testo, si può utilizzare:

read -r riga < file_prova


Un altro esempio, vogliamo associare il carattere "a capo" a una variabile:

nr="
"                    # funziona, ma occupa più righe e rompe l'indentazione
nr=$'\n'             # funziona (non POSIX) ed è il modo consigliato in bash

# a titolo esemplicativo per l'espansione di comando
nr=$(printf \\n)     # SBAGLIATO, $nr è vuota
nr="$(printf \\n)"   # SBAGLIATO, $nr è sempre vuota

# una possibile soluzione (POSIX)
nr=$(printf \\n%s X) # $nr contiene "a capo" seguito da X
nr=${nr%X}           # $nr contiene "a capo" (la X è rimossa)

# Attenzione che il carattere aggiunto dev'essere nell'output del comando
nr="$(printf \\n)X"  # SBAGLIATO, $nr contiene solo X

Output più complessi con a capo finale

Proviamo ora con un esempio più complesso con il comando readlink, che stampa il percorso dato in forma assoluta, anche seguendo eventuali link simbolici.

Ovviamente è sbagliato:

absolute_path=$(readlink -f -- "$file")  # eventuali "a capo" sarebbero rimossi

Proviamo ad aggiungere un carattere dentro l'espansione dopo l'output del comando. Si può fare con una concatenazione:

absolute_path=$(readlink -f -- "$file" && printf %s X)

Il significato di && si vedrà poi, basti sapere che printf viene eseguito solo se il comando non fallisce, così da non alterarne l'exit status.

Con il delimitatore niente viene perso ma, provando a stampare la variabile:

  • resta la nuova riga aggiunta dal comando, di solito rimossa dall'espansione di comando assieme a tutte quelle finali;
  • resta il delimitatore.

Quindi basta rimuovere gli ultimi due caratteri con un'espansione di parametro. Ricapitolando:

absolute_path=$(readlink -f -- "$file" &&
                printf %s X)
absolute_path=${absolute_path%??}        # ? sostituisce un carattere qualsiasi

Ora è possibile usare "$absolute_path".

Si ricordi che tutti i nomi di file, salvo quelli seguenti qualche convenzione (come quelli di sistema), possono contenere caratteri non usuali nel loro nome, e in particolare il carattere "a capo". Perciò, se non si controlla a priori ogni nome di file, cercando possibili caratteri non validi e interrompendo l'esecuzione con un errore, uno script robusto deve occuparsi anche di quella possibilità.

Possibilità alternativa, con controllo di errori e interruzione immediata:

if ! absolute_path=$(readlink -f -- "$file"); then
    printf %s\\n "ERRORE: percorso non trovato o contenente caratteri non validi!" >&2
    exit 1
fi

Si noti però che se esistessero file multipli, con lo stesso nome a parte la presenza degli "a capo" finali, non verrebbe mai restituito errore con nessuno di loro, ma ogni volta che si specifica uno dei file con "a capo" si finirebbe per usare sempre il file senza "a capo" finali senza accorgersene.

Espansione aritmetica intera

Permette di compiere operazioni aritmetiche tra interi, ritornando sempre una singola stringa contenente l'intero risultante. Può essere quotata, ma non cambia niente.

La sintassi è: $((...))
All'interno delle parentesi è possibile utilizzare:

  • le quattro operazioni: + - * /
  • resto/modulo: %
  • potenza: **
  • variabili da espandere (contenenti valori interi)
  • parentesi per cambiare le priorità degli operatori: ( )

Esempio:

base=9
altezza=5
area=$(($base * $altezza))
printf %s\\n "Area rettangolo: ${area}"       # Stampa 45
printf %s\\n "Area triangolo: $(($area / 2))" # Stampa 22 (RICORDA: solo interi)