Magazine Tecnologia

Linux Bash Scripting for Dummies: Struttura Scripts

Creato il 18 giugno 2011 da M0rf3us @alex_morfeus

Bentornato al nostro consueto appuntamento settimanale sul bash scripting. Quello che oggi ti propongo è una prima infarinatura sugli scripts.
Linux Bash Scripting for Dummies: Struttura Scripts

Come già ci siamo detti l’obiettivo di questa serie di guide è imparare a scrivere delle procedure (scripts) in maniera chiara e funzionale; e per farlo dobbiamo iniziare a capire come questi devono essere strutturati.

Introduzione

Indice Contenuti

Qualsiasi sviluppatore, sia che esso programmi in php che in ruby che in qualsiasi altro linguaggio, ti dirà che ci sono sostanzialmente due modi di scrivere il software che si possono descrivere con

  1. Scrittura fatta a cazzo di cane
  2. Scrittura fatta bene

Entrambi i metodi funzionano, dando per scontato che i software sono scritti senza errori, vale a dire che due software uguali ma scritti con i due metodi sopra illustrati funzioneranno entrambi, ma – c’è un ma – solo il software scritto col secondo metodo potrà essere definito un buon software, perché?

Perché chi usa il primo metodo tende a scrivere il software nella maniera più veloce, senza inserire i cosiddetti “commenti” all’interno del codice – che sono importantissimi – inserendo le funzioni base e senza pensare se una determinata azione può essere eseguita in altro modo, magari con meno linee di codice.

Il secondo metodo invece è migliore perché, anche se necessità di più tempo per scrivere lo stesso software (ma non sempre eh), fa scrivere del codice più funzionale, vale a dire che se ad esempio il codice è ben commentato e ben indentato1 sarà più facile rileggerlo magari dopo mesi che è stato scritto, soprattutto se a rileggerlo è una persona diversa da chi l’ha scritto inizialmente; inoltre, se il codice è anche ben ottimizzato ne risulterà un software più snello che richiede meno tempo e meno risorse per essere eseguito. Ne consegue che più lo script è complesso e più è importante, nonchè necessario, un processo di progettazione precedente alla stesura del codice; la progettazione è lo step che in molti casi determina la qualità del software finale, pensa che a volte il processo di progettazione può richiedere parecchio tempo, molto più di quello necessario per la stesura del codice finale.

Ecco perché è cosi importante saper ottimizare il codice che si scrive.

La struttura degli scripts bash

Dunque uno script bash possiamo dividerlo in tre macro-aree che chiameremo ispirandoci alla terminologia che si usa per l’html delle pagine web: header, corpo e – se necessario – il footer. Se non hai mai scritto in bash sappi che tra poco inizierai a leggere dei temini dei quali, quasi sicuramente, non ne saprai il significato. Però non devi spaventarti perché ovviamente il tutto ti sarà chiarito mano mano che leggerai l’articolo, e qualora cosi non fosse sei invitato a chiedere tutto quello che vuoi nei commenti.

Nello scripting unix l’header, o apertura, degli scripts include come primissima cosa l’interprete che il sistema deve usare per leggere ed eseguire il codice che seguirà. A seguire troveremo – opzionalmente – le istruzioni che servono ad includere eventuali file di configurazione, librerie, altri script, ecc… e poi tutte le variabili che sono necessarie per l’esecuzione dello script.

L’interprete deve essere inserito come prima riga dello script e nel caso di bash si usa:

#!/bin/bash

Se hai visto qualche volta del codice bash noterai una cosa, questa riga inizia con un cancelletto che viene usato per i commenti, però questo codice viene eseguito perché altrimenti come farebbe la shell a capire quale interprete usare? In realtà una differenza c’è, perché i commenti iniziano con un asterisco e basta, noi invece stiamo usando la combinazione di asterisco+punto esclamativo che in bash si chiama shebang.

Come fa la shell a capire che quella è una shebang (chiamata anche hashbang) e non un commento? Perché per essere una hashbang valida, questa combinazione deve apparire per prima, vale a dire che i primi due caratteri del paragrafo che inizia, devono essere appunto “#!” se invece viene piazzato all’interno di un paragrafo in qualsiasi altra posizione allora la shell sa che quello è un commento.

#!/bin/bash <- questa è una hashbang.
echo "#!/bin/bash" <- questo è un messaggio da stampare a schermo.
# Using #!/bin/bash as interpreter <- questo invece è un commento.

NB. Se dopo aver scritto uno script e lanciandolo con “./script.sh” otteniamo un “command not found” significa che molto probabilmente non è stato inserito l’interprete in testa al file.

Dopo l’interprete a volte potremmo trovare una sezione dedicata al cosiddetto sourcing delle dipendenze. Per esempio noi potremmo scrivere uno script che per funzionare ha bisogno di un file di configurazione che a sua volta può contenere delle variabili, altri comandi da eseguire, ecc…

Il sourcing può anche essere fatto al di fuori degli script con l’apposito comando ‘source’, per esempio, potremmo voler definire un alias di comandi all’interno del nostro file .bashrc2 per poter aggiornare tutti i pacchetti installati nella nostra linux box.

Normalmente lanceremmo “sudo pacman -Sy” se stessimo usando Archlinux oppure “sudo apt-get safe-upgrade -y” per debian/ubuntu, ma per fare ancora prima possiamo inserire dentro il nostro .bashrc l’alias

pkgup='pacman -Sy'

oppure

pkgup='apt-get safe-upgrade -y'

di modo da dover solo scrivere “sudo pkgup” per ottenere la stessa azione. Bene, una volta fatta questa modifica per poter usare quell’alias dovremmo lanciare una nuova shell o riloggarci al sistema nel caso stessimo usando un server che non ha quindi un desktop manager come gnome o kde; oppure possiamo renderla definitiva da subito usando il comando source.

:~$ source ~/.bashrc

Nel caso degli script non dobbiamo usare il comando source, basta solo inserire il path al file che ci interessa preceduto da un punto ed uno spazio.

# Includo il file di configurazione
. /path/to/configuration/file

NB. Un file che viene incluso in testa allo script non per forza necessita di essere reso eseguibile (chmod +x).

La prossima cosa che incontriamo nell’header degli scripts sono le definizioni delle variabili, queste sono molto importanti in quanto ci consentono di poter definire a monte pezzi di codice che poi verranno riutilizzati dentro il corpo dello script, per farlo si richiamano appunto queste variabili anteponendo il simbolo del dollaro al nome, la variabile VAR quindi verrà richiamata con $VAR.

E’ molto importante fare l’uso delle variabili perché cosi evitiamo di dover ripetere gli stessi pezzi di codice più volte all’interno dello script (ottimizzazione, ricordi?), è facile intuire quindi che le variabili vengono concepite per lo più durante la fase di progettazione.

Per capire come si usano le variabili andiamo a vedere un pezzo di script che creai un paio di anni fa, quando dovetti mettere in piedi una procedura di backup dei server basata su rsync.

 

#!/bin/bash
 
#Definizione delle variabili
 
PATH_SCRIPT="/usr/local/scripts"
 
RSYNC_CMD=`which rsync`
 
INCLUDE_FILE="$PATH_SCRIPT/path.conf"
 
SERVER_LIST="$PATH_SCRIPT/servers.inc"
 
PATH_BCK="/mnt/backups"

 

Possiamo notare da subito due cose, la prima è che inizio ad usare sin da subito le variabili che definisco, $PATH_SCRIPT infatti l’ho usata anche per definire il path dell’include file3 ed il path della lista dei server da backuppare; la seconda cosa riguarda il comando rsync:

Avrai notato che tutte le variabili sono definite dentro i doppi apici tranne RSYNC_CMD che è definita dentro degli apici inversi, questo perché apici, doppi apici, ed apici inversi in bash hanno determinate funzioni.

Con i doppi apici diciamo alla bash che quello che delimitano deve essere stampato valorizzando eventuali variabili contenute all’interno, analogamente con il singolo apice diciamo a bash che quello che è contenuto all’interno deve essere stampato cosi com’è senza tenere conto di variabili, per esempio ponendo di avere definito una variabile VAR=”3″:

echo "il valore di VAR è $VAR"

ci darà in output “il valore di VAR è 3″ mentre

echo 'il valore di var è $VAR'

ci darà in output “il valore di VAR è $VAR”.

Mentre il comportamento dell’apice inverso (che in linux otteniamo con la combinazione altgr+’ mentre in windows tenendo premuto ALT e digitando sul tastierino numerico 096) è differente, dichiarando una variabile con questo carattere diciamo a bash che il valore della stessa deve essere uguale all’output del comando inserito tra apici inversi.

Nel caso dello script di cui sopra quindi RSYNC_CMD avrà come valore l’output del comando “which rsync” che nella maggior parte dei casi sarà uguale a “/bin/rsync”.

Ma perché ho definito il comando di rsync con una variabile al posto di scrivere “/bin/rsync” ogni volta che mi serviva? Per una questione di usabilità, quando si fa uno script infatti si deve pensare che questo può essere lanciato su qualsiasi architettura, e non tutte le distribuzioni hanno il comando rsync sotto /bin, potrebbe essere lanciato ad esempio su un server sul quale rsync è stato installato da sorgenti dentro il path /usr/local/bin, conviene sempre quindi definire i comandi usati nello script con delle variabili passandogli `which nomecomando` come valore, eccezion fatta per i comandi standard ovviamente, come ad esempio “ls” che troveremo sempre dentro “/bin/ls”.

Il corpo

Il corpo dello script è la parte centrale, quella dove materialmente vengono inserite le funzioni ed i comandi che poi verranno eseguiti quando verrà lanciato lo script.

Ho menzionato per prima le funzioni non a caso, queste infatti, se presenti, devono essere inserite prima del codice che poi esegue materialmente le azioni, richiamandole.

Cosa sono le funzioni? Per un certo senso hanno un ruolo simile a quello delle variabili, sono anch’esse dei pezzi di codice che vengono usate in più parti nello script, e quindi onde evitare di dover riscrivere le stesse linee di codice (che possono essere tante eh) è molto meglio scriverle una sola volta, dentro una funzione, e poi richiamare la stessa ogni qualvolta ci serve.

Facciamo l’esempio più banale e frequente, ti è mai capitato di lanciare un comando in una shell sbagliando i parametri da passare? Avrai notato che ti viene restituito un errore seguito dallo “usage” ossia le istruzioni su come si usa lo script, che è uguale a quando lanci lo stesso comando seguito da “–help”.

Ecco questo è possibile grazie alle funzioni, lo usage viene definito all’interno di una funzione e poi, successivamente, viene definito un controllo sui parametri che vengono passati alla chiamata del comando, se sono sbagliati, o se viene passato solo –help, allora viene richiamata la funzione che sputa fuori il cosiddetto usage.

#!/bin/bash
 
#Funzione usage
 
usage () {
 
       echo "Usage:"
 
       echo "To use this script you have to pass 3 parameters"
 
       echo "-s: To define the source host to scp from"
 
       echo "-d: To define the destination host to scp to"
 
       echo "-p: To define the file or directory to transfer."
 
}

Alla fine lo usage non è altro che una serie di echo come puoi vedere, e questo che ti ho posto come esempio ha solo 5 righe di echo, più lo script è complesso più lo usage deve essere articolato e chiaro, ecco perché è meglio includerlo in una funzione; nel nostro esempio – quando sarà necessario – sarà sufficiente richiamare la funzione con “usage” per utilizzarla.

Per farti un idea di quante funzioni sono già incluse nella bash puoi usare il comando “declare -f” che ti stampa a video tutte le funzioni predefinite della bash, visto l’output molto lungo ti conviene passarlo in pipe a “less” o a “more” altrimenti leggeresti solo le ultime righe

:)

Ci sono un pò di cose da sapere sulle funzioni, queste infatti sono visibili solo all’interno della shell corrente, se le inseriamo in uno script quindi, una volta che questo avra terminato la sua esecuzione la funzione non esisterà più.

Le funzioni hanno inoltre una precedenza maggiore rispetto ai comandi built-in, è molto importante chiamare quindi le funzioni con nomi appropriati, se chiamassimo una funzione con “cat” ad esempio, andremmo a sostituire il normale comando che viene usato per stampare il contenuto di un file a schermo.

Quando un comando viene passato alla shell questa infatti cerca una sua ricorrenza tra:

  1. Aliases
  2. Parole chiavi
  3. Funzioni
  4. Comandi built-ins
  5. Script ed eseguibili presenti nel PATH

La sintassi per dichiarare una funzione è abbastanza semplice come avrai potuto notare

 

1
2
3
4
5
nomefunzione () {
 
             comandi da eseguire
 
}

 

Infine, è possibile dichiarare all’interno delle funzioni delle variabili visibili solo all’interno della funzione stessa, basta settare questa variabile con “local”

1
local VAR="valorevariabile"

Il footer è la parte finale dello script, in realtà non è sempre presente perché gli script semplici spesso una volta finita la lista di comandi da eseguire si concludono, io tendo ad includere nel footer la parte di script che non serve materialmente alla mera esecuzione della procedura, tipo la notifica via mail, uno script potrebbe prevedere una notifica a fine esecuzione in caso di errori, come potrebbe anche non farlo e  svolgere la stessa procedura.

Per fare questo è molto importante il logging quindi, ed abbiamo due metodi per farlo, possiamo lanciare lo script redirezionando l’output degli errori in un file di log da richiamare poi a fine script, nel footer appunto, per inviarlo via mail.

Oppure possiamo definire un tipo di logging “hard-coded” ossia dentro il codice dello script stesso, in modo da dover semplicemente lanciare lo script con gli eventuali parametri, senza dover pensare alla redirezione dell’output.

Il secondo metodo è più complesso ma preferibile se lo script dovrà essere usato da più persone e su più ambienti, implica necessariamente l’inserimento di più controlli preliminari riguardo l’esistenza della cartella destinata ai log ed ai permessi per poterci scrivere dentro, ma siamo sicuri che il metodo è standardizzato ed esente da errori esterni.

Il primo metodo è più semplice perché non implica nessun controllo da parte dello script, dovrà essere chi si occupa di utilizzarlo a pensare di creare la cartella destinata ai logs ed i file che li conterranno, ma è più soggetto ad errore se chi lo usa non ci pensa preventivamente, si vedrà infatti l’intero output stampato a schermo senza divisione tra standard output ed error output, questo se lanciato a mano, perché se inserito ad esempio in crontab non vedrà un bel nulla

:)

I metodi per la notifica di avvenuta esecuzione sono molteplici, io personalmente tendo ad usare il comando “mail” fornito dal pacchetto “mailutils” per la sua semplicità d’uso, la sintassi infatti è la seguente:

1
cat errorfile.log | mail -s "Attenzione ci sono stati errori durante l'esecuzione" email@dominio.it

con questo metodo avremo l’intero file di error log inserito nel corpo della mail, se invece prevediamo l’invio di una mail con il file come allegato allora dobbiamo usare mutt.

Ecco come in genere si strutturano gli scripts bash, naturalmente quelle che ti ho dato fino adesso sono solo delle linee guida, la prossima volta inizieremo a vedere cosa possiamo inserire nel corpo degli script, vedremo cosa sono i cicli e come implementare i controlli tramite “if” e “case”.

Nel frattempo, se vuoi provare a scrivere qualche semplice script di tuo pugno perché non me ne parli un pò e vediamo cosa possiamo tirare fuori? I commenti, come sempre, sono aperti a tutto

:)

  1. L’indentazione è l’inserimento di una serie di spazi vuoti tramite il tasto TAB ad inizio di ogni paragrafo, serve per aiutare la lettura
  2. .bashrc è il file che viene letto ad ogni nostro login, od ogni qualvolta lanciamo una shell, esso contiene tutte le nostre preferenze per l’uso della shell, come variabili d’ambiente, alias di comandi, personalizzazioni del prompt della shell e tanto altro.
  3. rsync consente di passare un file come parametro all’interno del quale ci sono dei percorsi specifici da includere o da escludere dai backup

Potrebbero interessarti anche :

Ritornare alla prima pagina di Logo Paperblog

Possono interessarti anche questi articoli :

Dossier Paperblog

Magazine