Guida avanzata di scripting Bash: Un'approfondita esplorazione dell'arte dello scripting di shell | ||
---|---|---|
Indietro | Avanti |
Turandot: Gli enigmi sono tre, la morte una! Caleph: No, no! Gli enigmi sono tre, una la vita! | |
Puccini |
Non usate, per i nomi delle variabili, parole o caratteri riservati.
case=valore0 # Crea problemi. 23skidoo=valore1 # Ancora problemi. # I nomi di variabili che iniziano con una cifra sono riservati alla shell. # Sostituite con _23skidoo=valore1. I nomi che iniziano con un #+ underscore (trattino di sottolineatura) vanno bene. # Tuttavia . . . usare il solo underscore non funziona. _=25 echo $_ # $_ è la variabile speciale impostata #+ all'ultimo argomento dell'ultimo comando. xyz((!*=valore2 # Provoca seri problemi. # A partire dalla versione 3 di Bash non è più consentito l'uso dei #+ punti nei nomi delle variabili. |
Non usare il trattino o altri caratteri riservati nel nome di una variabile (o in quello di una funzione).
var-1=23 # Usate 'var_1'. una-funzione () # Errore # Usate invece 'una_funzione ()'. # Dalla versione 3 di Bash non è più consentito usare i punti nei nomi #+ delle funzioni. una.funzione () # Errore # Usate invece 'unaFunzione ()'. |
Non usate lo stesso nome per una variabile e per una funzione. Ciò rende lo script difficile da capire.
fa_qualcosa () { echo "Questa funzione fa qualcosa con \"$1\"." } fa_qualcosa=fa_qualcosa fa_qualcosa fa_qualcosa # Tutto questo è consentito, ma crea estrema confusione. |
Non usate impropriamente gli spazi. A differenza di altri linguaggi di programmazione, Bash è piuttosto pignola con gli spazi.
var1 = 23 # corretto 'var1=23'. # Nella riga precedente, Bash cerca di eseguire il comando "var1" # con gli argomenti "=" e "23". let c = $a - $b # corretto 'let c=$a-$b' o 'let "c = $a - $b"'. if [ $a -le 5] # corretto if [ $a -le 5 ]. # if [ "$a" -le 5 ] ancora meglio. # [[ $a -le 5 ]] anche così. |
Fate sempre seguire da un punto e virgola l'ultimo comando presente in un blocco di codice all'interno delle parentesi graffe.
{ ls -l; df; echo "Fatto." } # bash: syntax error: unexpected end of file { ls -l; df; echo "Fatto."; } # ^ ### Il comando finale deve essere seguito #+ dal punto e virgola. |
Non date per scontato che le variabili non inizializzate (variabili a cui non è ancora stato assegnato un valore) valgano "zero". Una variabile non inizializzata ha valore nullo, non zero.
#!/bin/bash echo "var_non_inizializzata = $var_non_inizializzata" # var_non_inizializzata = |
Non confondete = e -eq nelle verifiche. Bisogna ricordarsi che = serve per il confronto tra variabili letterali mentre -eq per quello tra interi.
if [ "$a" = 273 ] # $a è un intero o una stringa? if [ "$a" -eq 273 ] # $a è un intero. # Talvolta è possibile scambiare -eq con = senza alcuna conseguenza. # Tuttavia . . . a=273.0 # Non è un intero. if [ "$a" = 273 ] then echo "Il confronto ha funzionato." else echo "Il confronto non ha funzionato." fi # Il confronto non ha funzionato. # Stessa cosa con a=" 273" e a="0273". # Allo stesso modo, si hanno problemi ad usare "-eq" con valori non interi. if [ "$a" -eq 273.0 ] then echo "a = $a" fi # Si interrompe con un messaggio d'errore. # test.sh: [: 273.0: integer expression expected |
Non usate in modo scorretto gli operatori per il confronto di stringhe.
Esempio 31-1. I confronti numerici e quelli di stringhe non si equivalgono
#!/bin/bash # bad-op.sh: Tentativo di usare il confronto di stringhe con gli interi. echo numero=1 # Il "ciclo while" seguente contiene due errori: #+ uno vistoso, l'altro subdolo. while [ "$numero" < 5 ] # Errato! Dovrebbe essere: while [ "$numero" -lt 5 ] do echo -n "$numero " let "numero += 1" done # La sua esecuzione provoca il messaggio d'errore: #+ bad-op.sh: line 10: 5: No such file or directory # All'interno delle parentesi quadre singole si deve applicare il quoting a"<" #+ e, anche così, è sbagliato usarlo per confrontare gli interi. echo "---------------------" while [ "$numero" \< 5 ] # 1 2 3 4 do # echo -n "$numero " # Questo *sembra funzionare, ma . . . let "numero += 1" #+ in realtà esegue un confronto ASCII, done #+ invece di uno numerico. echo; echo "---------------------" # Questo può provocare dei problemi. Ad esempio: minore=5 maggiore=105 if [ "$maggiore" \< "$minore" ] then echo "$maggiore è minore di $minore" fi # 105 è minore di 5 # Infatti, "105" è veramente minore di "5" #+ in un confronto di stringhe (ordine ASCII). echo exit 0 |
Talvolta è necessario il quoting (apici doppi) per le variabili che si trovano all'interno del costrutto di "verifica" parentesi quadre ([]). Non farne uso può causare un comportamento inaspettato. Vedi Esempio 7-6, Esempio 19-5 e Esempio 9-6.
Comandi inseriti in uno script possono fallire l'esecuzione se il proprietario dello script non ha, per quei comandi, i permessi d'esecuzione. Se un utente non può invocare un comando al prompt di shell, il fatto di inserirlo in uno script non cambia la situazione. Si provi a cambiare gli attributi dei comandi in questione, magari impostando il bit suid (come root, naturalmente).
Tentare di usare il - come operatore di redirezione (che non è) di solito provoca spiacevoli sorprese.
comando1 2> - | comando2 # Il tentativo di redirigere l'output #+ d'errore di comando1 con una pipe... # ...non funziona. comando1 2>& - | comando2 # Altrettanto inutile. Grazie, S.C. |
Usare le funzionalità di Bash versione 2+ può provocare l'uscita dal programma con un messaggio d'errore. Le macchine Linux più datate potrebbero avere, come installazione predefinita, la versione Bash 1.XX.
#!/bin/bash versione_minima=2 # Dal momento che Chet Ramey sta costantemente aggiungendo funzionalità a Bash, # si può impostare $versione_minima a 2.XX, 3.x.x, # o ad altro valore appropriato. E_ERR_VERSIONE=80 if [ "$BASH_VERSION" \< "$versione_minima" ] then echo "Questo script funziona solo con Bash, versione" echo "$versione_minima o superiore." echo "Se ne consiglia caldamente l'aggiornamento." exit $E_ERR_VERSIONE fi ... |
Usare le funzionalità specifiche di Bash in uno script di shell Bourne (#!/bin/sh) su una macchina non Linux può provocare un comportamento inatteso. Un sistema Linux di solito esegue l'alias di sh a bash, ma questo non è necessariamente vero per una generica macchina UNIX.
Usare funzionalità non documentate in Bash può rivelarsi una pratica pericolosa. Nelle versioni precedenti di questo libro erano presenti diversi script che si basavano su una "funzionalità" che, sebbene il valore massimo consentito per exit o return fosse 255, permetteva agli interi negativi di superare tale limite. Purtroppo, con la versione 2.05b e successive, tale scappatoia è scomparsa. Vedi Esempio 23-9.
Uno script con i caratteri di a capo di tipo DOS (\r\n) fallisce l'esecuzione poiché #!/bin/bash\r\n non viene riconosciuto, non è la stessa cosa dell'atteso #!/bin/bash\n. La correzione consiste nel convertire tali caratteri nei corrispondenti UNIX.
#!/bin/bash echo "Si parte" unix2dos $0 # lo script viene trasformato nel formato DOS. chmod 755 $0 # Viene ripristinato il permesso di esecuzione. # Il comando 'unix2dos' elimina i permessi di esecuzione. ./$0 # Si tenta la riesecuzione dello script. # Come file DOS non può più funzionare. echo "Fine" exit 0 |
Uno script di shell che inizia con #!/bin/sh non funziona in modalità di piena compatibilità Bash. Alcune funzioni specifiche di Bash potrebbero non essere abilitate. Gli script che necessitano di un accesso completo a tali estensioni devono iniziare con #!/bin/bash.
Mettere degli spazi davanti alla stringa limite di chiusura di un here document provoca un comportamento inatteso dello script.
Uno script non può esportare (export) le variabili in senso contrario né verso il suo processo genitore, la shell, né verso l'ambiente. Proprio come insegna la biologia, un figlio può ereditare da un genitore, ma non viceversa.
QUELLO_CHE_VUOI=/home/bozo export QUELLO_CHE_VUOI exit 0 |
bash$ echo $QUELLO_CHE_VUOI bash$ |
È certo, al prompt dei comandi, $QUELLO_CHE_VUOI rimane non impostata.
Impostare e manipolare variabili all'interno di una subshell e cercare, successivamente, di usare quelle stesse variabili al di fuori del loro ambito, provocherà una spiacevole sorpresa.
Esempio 31-2. I trabocchetti di una subshell
#!/bin/bash # Le insidie delle variabili di una subshell. variabile_esterna=esterna echo echo "variabile esterna = $variabile_esterna" echo ( # Inizio della subshell echo "variabile esterna nella subshell = $variabile_esterna" variabile_interna=interna # Impostata echo "variabile interna nella subshell = $variabile_interna" variabile_esterna=interna # Il valore risulterà cambiato a livello globale? echo "variabile esterna nella subshell = $variabile_esterna" # Se le avessimo 'esportate' ci sarebbe stata differenza? # export variabile_interna # export variabile_esterna # Provate e vedete. # Fine della subshell ) echo echo "variabile interna al di fuori della subshell = $variabile_interna" # Non impostata. echo "variabile esterna al di fuori della subshell = $variabile_esterna" # Immutata. echo exit 0 # Cosa succede se decommentate le righe 19 e 20? # Trovate qualche diversità? |
Collegare con una pipe l'output di echo a read può produrre risultati inattesi. In un tale scenario, read si comporta come se fosse in esecuzione all'interno di una subshell. Si usi invece il comando set (come in Esempio 14-17).
Esempio 31-3. Concatenare con una pipe l'output di echo a read
#!/bin/bash # badread.sh: # Tentativo di usare 'echo e 'read' #+ per l'assegnazione non interattiva di variabili. a=aaa b=bbb c=ccc echo "uno due tre" | read a b c # Cerca di riassegnare a, b e c. echo echo "a = $a" # a = aaa echo "b = $b" # b = bbb echo "c = $c" # c = ccc # Riassegnazione fallita. # ------------------------------ # Proviamo la seguente alternativa. var=`echo "uno due tre"` set -- $var a=$1; b=$2; c=$3 echo "-------" echo "a = $a" # a = uno echo "b = $b" # b = due echo "c = $c" # c = tre # Riassegnazione riuscita. # ------------------------------ # Notate inoltre che echo con 'read' funziona all'interno di una subshell. # Tuttavia, il valore della variabile cambia *solo* in quell'ambito. a=aaa # Ripartiamo da capo. b=bbb c=ccc echo; echo echo "uno due tre" | ( read a b c; echo "nella subshell: "; echo "a = $a"; echo "b = $b"; echo "c = $c" ) # a = uno # b = due # c = tre echo "----------------------" echo "Fuori dalla subshell: " echo "a = $a" # a = aaa echo "b = $b" # b = bbb echo "c = $c" # c = ccc echo exit 0 |
In effetti, come fa notare Anthony Richardson, usare la pipe con qualsiasi ciclo può provocare un simile problema.
# Problemi nell'uso di una pipe con un ciclo. # Esempio di Anthony Richardson. #+ con appendice di Wilbert Berendsen. trovato=falso find $HOME -type f -atime +30 -size 100k | while true do read f echo "$f supera i 100KB e non è stato usato da più di 30 giorni" echo "Considerate la possibilità di spostarlo in un archivio." trovato=vero # -------------------------------------------- echo "Livello della subshell = $BASH_SUBSHELL" # Livello della subshell = 1 # Si, siete all'interno di una subshell. # -------------------------------------------- done # In questo caso trovato sarà sempre falso perché #+ è stato impostato all'interno di una subshell if [ $trovato = falso ] then echo "Nessun file da archiviare." fi # =====================Ora nel modo corretto:======================== trovato=falso for f in $(find $HOME -type f -atime +30 -size 100k) # Nessuna pipe. do echo "$f supera i 100KB e non è stato usato da più di 30 giorni" echo "Considerate la possibilità di spostarlo in un archivio." trovato=vero done if [ $trovato = falso ] then echo "Nessun file da archiviare." fi # =====================Ed ecco un'altra alternativa================== # Inserite la parte dello script che legge le variabili all'interno del #+ blocco di codice, in modo che condividano la stessa subshell. # Grazie, W.B. find $HOME -type f -atime +30 -size 100k | { trovato=false while read f do echo "$f supera i 100KB e non è stato usato da più di 30 giorni" echo "Considerate la possibilità di spostarlo in un archivio." trovato=true done if ! $trovato then echo "Nessun file da archiviare." fi } |
Un problema simile si verifica quando si cerca di scrivere lo stdout di tail -f collegato con una pipe a grep.
tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log # Nel file "error.log" non ci sarà scritto niente. |
È rischioso, negli script, l'uso di comandi che hanno il bit "suid" impostato, perché questo può compromettere la sicurezza del sistema. [1]
L'uso degli script di shell per la programmazione CGI potrebbe rivelarsi problematica. Le variabili degli script di shell non sono "tipizzate" e questo fatto può causare un comportamento indesiderato per quanto concerne CGI. Inoltre, è difficile proteggere dal "cracking" gli script di shell.
Bash non gestisce correttamente la stringa doppia barra (//).
Gli script Bash, scritti per i sistemi Linux o BSD, possono aver bisogno di correzioni per consentire la loro esecuzione su macchine UNIX commerciali (o Apple OSX). Questi script, infatti, fanno spesso uso di comandi e filtri GNU che hanno funzionalità superiori ai loro corrispettivi generici UNIX. Questo è particolarmente vero per le utility di elaborazione di testo come tr.
Danger is near thee -- Beware, beware, beware, beware. Many brave hearts are asleep in the deep. So beware -- Beware. | |
A.J. Lamb and H.W. Petrie |
[1] | L'impostazione del bit suid dello script stesso non ha alcun effetto. |