Guida avanzata di scripting Bash: Un'approfondita esplorazione dell'arte dello scripting di shell | ||
---|---|---|
Indietro | Capitolo 33. Miscellanea | Avanti |
Avete un problema che volete risolvere scrivendo uno script Bash. Purtroppo, non avete alcun idea da dove partire. Un sistema è quello di precipitarsi a codificare quelle parti dello script che vi risultano più facili e scrivere quelle più difficili in pseudo-codice.
#!/bin/bash CONTA_ARG=1 # Come argomento occorre il nome. E_ERR_ARG=65 if [ numero_di_argomenti_non_uguale_a "$CONTA_ARG" ] # ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ # Non riesco a codificare questo . . . #+ . . . quindi lo scrivo in pseudo-codice. then echo "Utilizzo: name-dello-script nome" # ^^^^^^^^^^^^^^^^^ Altro pseudo-codice. exit $E_ERR_ARG fi . . . exit 0 # Successivamente, sostituiamo lo pseudo-codice con quello effettivo. # La riga 6 diventa: if [ $# -ne "$CONTA_ARG" ] # La riga 12 diventa: echo "Utilizzo: `basename $0` nome" |
Per un esempio di uso di pseudo-codice, vedi l'esercizio Radice quadrata.
Per mantenere una registrazione di quali script sono stati eseguiti durante una particolare sessione, o un determinato numero di sessioni, si aggiungano le righe seguenti a tutti gli script di cui si vuole tener traccia. In questo modo verrà continuamente aggiornato il file di registrazione con i nomi degli script e con l'ora in cui sono stati posti in esecuzione.
# Accodate (>>) le righe seguenti alla fine di ogni script di cui #+ volete tener traccia. whoami>> $SAVE_FILE # Utente che ha invocato lo script. echo $0>> $SAVE_FILE # Nome dello script. date>> $SAVE_FILE # Data e ora. echo>> $SAVE_FILE # Riga bianca di separazione. # Naturalmente, SAVE_FILE deve essere definito ed esportato come #+ variabile d'ambiente in ~/.bashrc #+ (qualcosa come ~/.script-eseguiti) |
L'operatore >> accoda delle righe in un file. Come si può fare, invece, se si desidera anteporre una riga in un file esistente, cioè, inserirla all'inizio?
file=dati.txt titolo="***Questa è la riga del titolo del file di testo dati***" echo $titolo | cat - $file >$file.nuovo # "cat -" concatena lo stdout a $file. # Il risultato finale è #+ la creazione di un nuovo file con $titolo aggiunto all'*inizio*. |
È una variante semplificata dello script di Esempio 18-13 già visto in precedenza. Naturalmente, anche sed è in grado di svolgere questo compito.
Uno script di shell può agire come un comando inserito all'interno di un altro script di shell, in uno script Tcl o wish, o anche in un Makefile. Può essere invocato come un comando esterno di shell in un programma C, per mezzo della funzione system(), es. system("nome_script");.
Impostare una variabile al risultato di uno script sed o awk incorporato aumenta la leggibilità di uno shell wrapper. Vedi Esempio A-1 e Esempio 14-19.
Si raggruppino in uno o più file le funzioni e le definizioni preferite e più utili. Al bisogno, si può "includere" uno o più di questi "file libreria" negli script per mezzo sia del punto (.) che del comando source.
# LIBRERIA PER SCRIPT # ------ ------- ------ # Nota: # Non è presente "#!" # Né "codice eseguibile". # Definizioni di variabili utili UID_ROOT=0 # Root ha $UID 0. E_NONROOT=101 # Errore di utente non root. MAXVALRES=255 # Valore di ritorno massimo (positivo) di una funzione. SUCCESSO=0 INSUCCESSO=-1 # Funzioni Utilizzo () # Messaggio "Utilizzo:". { if [ -z "$1" ] # Nessun argomento passato. then msg=nomefile else msg=$@ fi echo "Utilizzo: `basename $0` "$msg"" } Controlla_root () # Controlla se è root ad eseguire lo script. { # Dall'esempio "ex39.sh". if [ "$UID" -ne "$UID_ROOT" ] then echo "Devi essere root per eseguire questo script." exit $E_NONROOT fi } CreaNomeFileTemp () # Crea un file temporaneo con nome "univoco". { # Dall'esempio "ex51.sh". prefisso=temp suffisso=`eval date +%s` Nomefiletemp=$prefisso.$suffisso } isalpha2 () # Verifica se l'*intera stringa* è formata da #+ caratteri alfabetici. { # Dall'esempio "isalpha.sh". [ $# -eq 1 ] || return $INSUCCESSO case $1 in *[!a-zA-Z]*|"") return $INSUCCESSO;; *) return $SUCCESSO;; esac # Grazie, S.C. } abs () # Valore assoluto. { # Attenzione: Valore di ritorno massimo = 255. E_ERR_ARG=-999999 if [ -z "$1" ] # È necessario passare un argomento. then return $E_ERR_ARG # Ovviamente viene restituito il #+ codice d'errore. fi if [ "$1" -ge 0 ] # Se non negativo, then # valass=$1 # viene preso così com'è. else # Altrimenti, let "valass = (( 0 - $1 ))" # cambia il segno. fi return $valass } in_minuscolo () # Trasforma la/e stringa/he passata/e come argomento/i { #+ in caratteri minuscoli. if [ -z "$1" ] # Se non viene passato alcun argomento, then #+ invia un messaggio d'errore echo "(null)" #+ (messaggio d'errore di puntatore vuoto in stile C) return #+ e uscita dalla funzione. fi echo "$@" | tr A-Z a-z # Modifica di tutti gli argomenti passati ($@). return # Usate la sostituzione di comando per impostare una variabile all'output #+ della funzione. # Per esempio: # vecchiavar="unA seRiE di LEtTerE mAiUscoLe e MInusColE MisCHiaTe" # nuovavar=`in_minuscolo "$vecchiavar"` # echo "$nuovavar" # una serie di lettere maiuscole e minuscole mischiate # # Esercizio: riscrivete la funzione per modificare le lettere minuscole del/gli #+ argomento/i passato/i in lettere maiuscole ... in_maiuscolo() [facile]. } |
Si utilizzino intestazioni di commento particolareggiate per aumentare la chiarezza e la leggibilità degli script.
## Attenzione. rm -rf *.zzy ## Le opzioni "-rf" di "rm" sono molto pericolose, ##+ in modo particolare se usate con i caratteri jolly. #+ Continuazione di riga. # Questa è la riga 1 #+ di un commento posto su più righe, #+ e questa è la riga finale. #* Nota. #o Elemento di un elenco. #> Alternativa. while [ "$var1" != "fine" ] #> while test "$var1" != "fine" |
Un uso particolarmente intelligente dei costrutti if-test è quello per commentare blocchi di codice.
#!/bin/bash BLOCCO_DI_COMMENTO= # Provate a impostare la variabile precedente ad un valore qualsiasi #+ ed otterrete una spiacevole sorpresa. if [ $BLOCCO_DI_COMMENTO ]; then Commento -- ================================================ Questa è una riga di commento. Questa è un'altra riga di commento. Questa è un'altra riga ancora di commento. ================================================ echo "Questo messaggio non verrà visualizzato." I blocchi di commento non generano errori! Wow! fi echo "Niente più commenti, prego." exit 0 |
Si confronti questo esempio con commentare un blocco di codice con gli here document.
L'uso della variabile di exit status $? consente allo script di verificare se un parametro contiene solo delle cifre, così che possa essere trattato come un intero.
#!/bin/bash SUCCESSO=0 E_ERR_INPUT=65 test "$1" -ne 0 -o "$1" -eq 0 2>/dev/null # Un intero è o diverso da 0 o uguale a 0. # 2>/dev/null sopprime il messaggio d'errore. if [ $? -ne "$SUCCESSO" ] then echo "Utilizzo: `basename $0` intero" exit $E_ERR_INPUT fi let "somma = $1 + 25" # Darebbe un errore se $1 non #+ fosse un intero. echo "Somma = $somma" # In questo modo si può verificare qualsiasi variabile, #+ non solo i parametri passati da riga di comando. exit 0 |
L'intervallo 0 - 255, per i valori di ritorno di una funzione, rappresenta una seria limitazione. Anche l'impiego di variabili globali ed altri espedienti spesso si rivela problematico. Un metodo alternativo che permette alla funzione di restituire un valore allo script, è fare in modo che questa scriva il "valore di ritorno" allo stdout (solitamente con echo) per poi assegnarlo a una variabile. In realtà si tratta di una variante della sostituzione di comando.
Esempio 33-15. Uno stratagemma per il valore di ritorno
#!/bin/bash # multiplication.sh moltiplica () # Moltiplica i parametri passati. { # Accetta un numero variabile di argomenti. local prodotto=1 until [ -z "$1" ] # Finché ci sono parametri... do let "prodotto *= $1" shift done echo $prodotto # Lo visualizza allo stdout, } #+ poiché verrà assegnato ad una variabile. molt1=15383; molt2=25211 val1=`moltiplica $molt1 $molt2` echo "$molt1 X $molt2 = $val1" # 387820813 molt1=25; molt2=5; molt3=20 val2=`moltiplica $molt1 $molt2 $molt3` echo "$molt1 X $molt2 X $molt3 = $val2" # 2500 molt1=188; molt2=37; molt3=25; molt4=47 val3=`moltiplica $molt1 $molt2 $molt3 $molt4` echo "$molt1 X $molt2 X $molt3 X $molt4 = $val3" # 8173300 exit 0 |
La stessa tecnica funziona anche per le stringhe alfanumeriche. Questo significa che una funzione può "restituire" un valore non numerico.
car_maiuscolo () # Cambia in maiuscolo il carattere iniziale { #+ di un argomento stringa/he passato. stringa0="$@" # Accetta più argomenti. primocar=${stringa0:0:1} # Primo carattere. stringa1=${stringa0:1} # Parte restante della/e stringa/he. PrimoCar=`echo "$primocar" | tr a-z A-Z` # Cambia in maiuscolo il primo carattere. echo "$PrimoCar$stringa1" # Visualizza allo stdout. } nuovastringa=`car_maiuscolo "ogni frase dovrebbe iniziare \ con una lettera maiuscola."` echo "$nuovastringa" # Ogni frase dovrebbe iniziare con una #+ lettera maiuscola. |
Con questo sistema una funzione può "restituire" più valori.
Esempio 33-16. Uno stratagemma per valori di ritorno multipli
#!/bin/bash # sum-product.sh # Una funzione può "restituire" più di un valore. somma_e_prodotto () # Calcola sia la somma che il prodotto degli #+ argomenti passati. { echo $(( $1 + $2 )) $(( $1 * $2 )) # Visualizza allo stdout ogni valore calcolato, separato da uno spazio. } echo echo "Inserisci il primo numero" read primo echo echo "Inserisci il secondo numero" read secondo echo valres=`somma_e_prodotto $primo $secondo` # Assegna l'output della funzione. somma=`echo "$valres" | awk '{print $1}'` # Assegna il primo campo. prodotto=`echo "$valres" | awk '{print $2}'` # Assegna il secondo campo. echo "$primo + $secondo = $somma" echo "$primo * $secondo = $prodotto" echo exit 0 |
Le prossime, della serie di trucchi del mestiere, sono le tecniche per il passaggio di un array a una funzione e della successiva "restituzione" dell'array allo script.
Passare un array ad una funzione implica dover caricare gli elementi dell'array, separati da spazi, in una variabile per mezzo della sostituzione di comando. Per la restituzione dell'array, come "valore di ritorno" della funzione, si impiega lo stratagemma appena descritto di effettuare un echo dell'array nella funzione e, quindi, tramite la sostituzione di comando e l'operatore ( ... ) riassegnarlo ad un array.
Esempio 33-17. Passaggio e restituzione di array
#!/bin/bash # array-function.sh: Passaggio di un array a una funzione e... # "restituzione" di un array da una funzione Passa_Array () { local array_passato # Variabile locale. array_passato=( `echo "$1"` ) echo "${array_passato[@]}" # Elenca tutti gli elementi del nuovo array #+ dichiarato e impostato all'interno della funzione. } array_originario=( elemento1 elemento2 elemento3 elemento4 elemento5 ) echo echo "array originario = ${array_originario[@]}" # Elenca tutti gli elementi dell'array originario. # Ecco il trucco che consente di passare un array ad una funzione. # ************************************* argomento=`echo ${array_originario[@]}` # ************************************* # Imposta la variabile #+ a tutti gli elementi, separati da spazi, dell'array originario. # # È da notare che cercare di passare semplicemente l'array non funziona. # Ed ecco il trucco che permette di ottenere un array come "valore di ritorno". # ********************************************* array_restituito=( `Passa_Array "$argomento"` ) # ********************************************* # Assegna l'output 'visualizzato' della funzione all'array. echo "array restituito = ${array_restituito[@]}" echo "=============================================================" # Ora, altra prova, un tentativo di accesso all'array (per elencarne #+ gli elementi) dall'esterno della funzione. Passa_Array "$argomento" # La funzione, di per sé, elenca l'array, ma... #+ non è consentito accedere all'array al di fuori della funzione. echo "Array passato (nella funzione) = ${array_passato[@]}" # VALORE NULL perché è una variabile locale alla funzione. echo exit 0 |
Per un dimostrazione più elaborata di passaggio di array a funzioni, vedi Esempio A-10.
Utilizzando il costrutto doppie parentesi è possibile l'impiego della sintassi in stile C per impostare ed incrementare le variabili, e per i cicli for e while. Vedi Esempio 10-12 e Esempio 10-17.
Impostare path e umask all'inizio di uno script lo rende
maggiormente "portabile" -- più probabilità che
possa essere eseguito su una macchina "forestiera" il cui utente potrebbe
aver combinato dei pasticci con $PATH
e umask.
#!/bin/bash PATH=/bin:/usr/bin:/usr/local/bin ; export PATH umask 022 # I file creati dallo script avranno i permessi impostati a 755. # Grazie a Ian D. Allen per il suggerimento. |
Un'utile tecnica di scripting è quella di fornire ripetitivamente l'output di un filtro (con una pipe) allo stesso filtro, ma con una serie diversa di argomenti e/o di opzioni. tr e grep sono particolarmente adatti a questo scopo.
Dall'esempio "wstrings.sh". wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \ tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '` |
Esempio 33-18. Divertirsi con gli anagrammi
#!/bin/bash # agram.sh: Giocare con gli anagrammi. # Trova gli anagrammi di... LETTERE=etaoinshrdlu FILTRO='.......' # Numero minimo di lettere. # 1234567 anagram "$LETTERE" | # Trova tutti gli anagrammi delle lettere fornite... grep "$FILTRO" | # Di almeno 7 lettere, grep '^is' | # che iniziano con 'is' grep -v 's$' | # nessun plurale (in inglese, ovviamente [N.d.T.]) grep -v 'ed$' # nessun participio passato di verbi (come sopra) # E' possibile aggiungere molte altre combinazioni di condizioni e filtri. # Usa l'utility "anagram" che fa parte del pacchetto #+ dizionario "yawl" dell'autore di questo documento. # http://ibiblio.org/pub/Linux/libs/yawl-0.3.tar.gz # http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz exit 0 # Fine del codice. bash$ sh agram.sh islander isolate isolead isotheral # Esercizi: # -------- # Modificate lo script in modo che il valore di LETTERE venga fornito come #+ parametro da riga di comando. # Anche i filtri alle righe 11 - 13 devono essere sostituiti da parametri #+ (come con $FILTRO), così che possano essere passati come argomenti #+ a una funzione. # Per un approccio agli anagrammi leggermente diverso, #+ vedi lo script agram2.sh. |
Vedi anche Esempio 27-3, Esempio 15-22 e Esempio A-9.
Si usino gli "here document anonimi" per commentare blocchi di codice ed evitare di dover commentare ogni singola riga con un #. Vedi Esempio 18-11.
Eseguire uno script su una macchina sulla quale non è installato il comando su cui lo script si basa, è pericoloso. Si usi whatis per evitare potenziali problemi.
CMD=comando1 # Scelta primaria. PianoB=comando2 # Comando di ripiego. verifica_comando=$(whatis "$CMD" | grep 'nothing appropriate') #* # Se 'comando1' non viene trovato sul sistema , 'whatis' restituisce #+ "comando1: nothing appropriate." # # Un'alternativa più sicura sarebbe: # verifica_comando=$(whereis "$CMD" | grep \/) # Ma allora il senso della verifica seguente andrebbe invertito, #+ dal momento che la variabile $verifica_comando è impostata solo se #+ $CMD è presente sul sistema. # (Grazie, bojster.) if [[ -z "$verifica_comando" ]] # Verifica se il comando è presente. then $CMD opzione1 opzione2 # Esegue comando1 con le opzioni. else # Altrimenti, $PianoB #+ esegue comando2. fi #* Ma anche "niente di appropriato". # Verificatelo per la vostra distribuzione [N.d.T.] |
Una verifica if-grep potrebbe non dare i risultati attesi in caso di errore, quando il testo viene visualizzato allo stderr invece che allo stdout.
if ls -l file_inesistente | grep -q 'No such file or directory' then echo "Il file \"file_inesistente\" non esiste." fi |
Il problema può essere risolto con la redirezione dello stderr allo stdout.
if ls -l file_inesistente 2>&1 | grep -q 'No such file or directory' # ^^^^ then echo "Il file \"file_inesistente\" non esiste." fi # Grazie a Chris Martin per la precisazione. |
Il comando run-parts è utile per eseguire una serie di comandi in sequenza, in particolare abbinato a cron o at.
Sarebbe bello poter invocare i widget X-Windows da uno script di shell. Si dà il caso che esistano diversi pacchetti che hanno la pretesa di far questo, in particolare Xscript, Xmenu e widtools. Sembra, però, che i primi due non siano più mantenuti. Fortunatamente è ancora possibile ottenere widtools qui.
Il pacchetto widtools (widget tools) richiede l'installazione della libreria XForms. In aggiunta, il Makefile necessita di alcune sistemazioni prima che il pacchetto possa essere compilato su un tipico sistema Linux. Infine, tre dei sei widget non funzionano (segmentation fault). |
La famiglia di strumenti dialog offre un metodo per richiamare i widget di "dialogo" da uno script di shell. L'utility originale dialog funziona in una console di testo, mentre i suoi successori gdialog, Xdialog e kdialog usano serie di widget basate su X-Windows.
Esempio 33-19. Widget invocati da uno script di shell
#!/bin/bash # dialog.sh: Uso dei widgets 'gdialog'. # Per l'esecuzione dello script è indispensabile aver installato 'gdialog'. # Versione 1.1 (corretta il 05/04/05) # Lo script è stato ispirato dal seguente articolo. # "Scripting for X Productivity," di Marco Fioretti, # LINUX JOURNAL, Numero 113, Settembre 2003, pp. 86-9. # Grazie a tutti quelli di LJ. # Errore di input nel box di dialogo. E_INPUT=65 # Dimensioni dei widgets di visualizzazione e di input. ALTEZZA=50 LARGHEZZA=60 # Nome del file di output (composto con il nome dello script). OUTFILE=$0.output # Visualizza questo script in un widget di testo. gdialog --title "Visualizzazione: $0" --textbox $0 $ALTEZZA $LARGHEZZA # Ora, proviamo a salvare l'input in un file. echo -n "VARIABILE=" > $OUTFILE gdialog --title "Input Utente" --inputbox "Prego, inserisci un dato:" \ $ALTEZZA $LARGHEZZA 2>> $OUTFILE if [ "$?" -eq 0 ] # È buona pratica controllare l'exit status. then echo "Eseguito \"box di dialogo\" senza errori." else echo "Errore(i) nell'esecuzione di \"box di dialogo\"." # Oppure avete cliccato su "Cancel" invece che su "OK". rm $OUTFILE exit $E_INPUT fi # Ora, recuperiamo e visualizziamo la variabile. . $OUTFILE # 'Include' il file salvato. echo "La variabile inserita nel \"box di input\" è: "$VARIABILE"" rm $OUTFILE # Cancellazione del file temporaneo. # Alcune applicazioni potrebbero aver ancora bisogno #+ di questo file. exit $? |
Per altri metodi di scripting con l'impiego di widget, si provino Tk o wish (derivati Tcl), PerlTk (Perl con estensioni Tk), tksh (ksh con estensioni Tk), XForms4Perl (Perl con estensioni XForms), Gtk-Perl (Perl con estensioni Gtk) o PyQt (Python con estensioni Qt).
Per effettuare revisioni multiple di uno script complesso, si usi il pacchetto rcs Revision Control System.
Tra le sue
funzionalità vi è anche quella di aggiornare automaticamente
l'ID dell'intestazione. Il comando co di
rcs effettua una sostituzione di parametro di
alcune parole chiave riservate, ad esempio rimpiazza
#$Id$
di uno script con qualcosa come:
#$Id: hello-world.sh,v 1.1 2004/10/16 02:43:05 bozo Exp $ |