Capitolo 2. Iniziare con #!

 

Shell programming is a 1950s juke box . . .

 Larry Wall
Sommario
2.1. Eseguire uno script
2.2. Esercizi preliminari

Nel caso più semplice, uno script non è nient'altro che un file contenente un elenco di comandi di sistema. Come minimo si risparmia lo sforzo di ridigitare quella particolare sequenza di comandi tutte le volte che è necessario.

Esempio 2-1. cleanup: Uno script per cancellare i file di log in /var/log

# Cleanup
# Da eseguire come root, naturalmente.

cd /var/log
cat /dev/null > messages
cat /dev/null > wtmp
echo "Log cancellati."

Come si può vedere, non c'è niente di insolito, solo una serie di comandi che potrebbero essere eseguiti uno ad uno dalla riga di comando di una console o di un xterm. I vantaggi di collocare dei comandi in uno script vanno, però, ben al di là del non doverli reimmettere ogni volta. Lo script, infatti, può essere modificato, personalizzato o generalizzato per un'applicazione particolare.

Esempio 2-2. cleanup: Lo script clean-up migliorato

#!/bin/bash
# Corretta intestazione di uno script Bash.

# Cleanup, versione 2

# Da eseguire come root, naturalmente.
#  Qui va inserito il codice che visualizza un messaggio d'errore e l'uscita
#+ dallo script nel caso l'esecutore non sia root.

DIR_LOG=/var/log
# Meglio usare le variabili che codificare dei valori.
cd $DIR_LOG

cat /dev/null > messages
cat /dev/null > wtmp


echo "Log cancellati."

exit # Metodo corretto per "uscire" da uno script.

Adesso incomincia ad assomigliare ad un vero script. Ma si può andare oltre . . .

Esempio 2-3. cleanup: Una versione avanzata e generalizzata degli script precedenti.

#!/bin/bash
#  Cleanup, versione 3

#  Attenzione:
#  -----------
#  In questo script sono presenti alcune funzionalità che verranno
#+ spiegate più avanti.
#  Quando avrete ultimato la prima metà del libro,
#+ forse non vi apparirà più così misterioso.



DIR_LOG=/var/log
ROOT_UID=0     # Solo gli utenti con $UID 0 hanno i privilegi di root.
LINEE=50       # Numero prestabilito di righe salvate.
E_XCD=66       # Riesco a cambiare directory?
E_NONROOT=67   # Codice di exit non-root.


# Da eseguire come root, naturalmente.
if [ "$UID" -ne "$ROOT_UID" ]
then
  echo "Devi essere root per eseguire questo script."
  exit $E_NONROOT
fi  

if [ -n "$1" ]
# Verifica se è presente un'opzione da riga di comando (non-vuota).
then
  linee=$1
else  
  linee=$LINEE # Valore preimpostato, se non specificato da riga di comando.
fi  


#  Stephane Chazelas suggerisce il codice seguente,
#+ come metodo migliore per la verifica degli argomenti da riga di comando,
#+ ma è ancora un po' prematuro a questo punto del manuale.
#
#    E_ERR_ARG=65   # Argomento non numerico (formato dell'argomento non valido)
#
#    case "$1" in
#    ""      ) linee=50;;
#    *[!0-9]*) echo "Utilizzo: `basename $0` file-da-cancellare"; exit\
# $E_ERR_ARG;;
#    *       ) linee=$1;;
#    esac
#
#* Vedere più avanti al capitolo "Cicli" per la comprensione delle righe 
#+ precedenti.


cd $DIR_LOG

if [ `pwd` != "$DIR_LOG" ]  # o   if [ "$PWD" != "$DIR_LOG" ]
                            # Non siamo in /var/log?
then
  echo "Non riesco a cambiare in $DIR_LOG."
  exit $E_XCD
fi  #  Doppia verifica per vedere se ci troviamo nella directory corretta,
    #+ prima di cancellare il file di log.

# ancora più efficiente:
#
# cd /var/log || {
#   echo "Non riesco a spostarmi nella directory stabilita." >&2
#   exit $E_XCD;
# }




tail -n $linee messages > mesg.temp # Salva l'ultima sezione del file di
                                    # log messages.
mv mesg.temp messages               # Diventa la nuova directory di log.


# cat /dev/null > messages
#* Non più necessario, perché il metodo precedente è più sicuro.

cat /dev/null > wtmp  #  ': > wtmp' e '> wtmp'  hanno lo stesso effetto.
echo "Log cancellati."

exit 0
#  Il valore di ritorno zero da uno script
#+ indica alla shell la corretta esecuzione dello stesso.

Poiché non si voleva cancellare l'intero log di sistema, questa versione dello script mantiene inalterata l'ultima sezione del file di log messages. Si scopriranno continuamente altri modi per rifinire gli script precedenti ed aumentarne l'efficienza.

I caratteri ( #!), all'inizio dello script, informano il sistema che il file contiene una serie di comandi che devono essere passati all'interprete indicato. I caratteri #! in realtà sono un magic number di due byte, [1] vale a dire un identificatore speciale che designa il tipo di file o, in questo caso, uno script di shell eseguibile (eseguite man magic per ulteriori dettagli su questo affascinante argomento). Immediatamente dopo #! compare un percorso. Si tratta del percorso al programma che deve interpretare i comandi contenuti nello script, sia esso una shell, un linguaggio di programmazione o una utility. L'interprete esegue quindi i comandi dello script, partendo dall'inizio (la riga successiva a #!) e ignorando i commenti. [2]

#!/bin/sh
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/usr/awk -f

Ognuna delle precedenti intestazioni di script richiama un differente interprete di comandi, sia esso /bin/sh, la shell (bash in un sistema Linux) o altri. [3] L'utilizzo di #!/bin/sh, la shell Bourne predefinita nella maggior parte delle varie distribuzioni commerciali UNIX, rende lo script portabile su macchine non-Linux, sebbene questo significhi sacrificare alcune funzionalità specifiche di Bash. Lo script sarà, comunque, conforme allo standard POSIX [4] sh.

È importante notare che il percorso specificato dopo "#!" deve essere esatto, altrimenti un messaggio d'errore -- solitamente "Command not found" -- sarà l'unico risultato dell'esecuzione dello script.

#! può essere omesso se lo script è formato solamente da una serie di comandi specifici di sistema e non utilizza direttive interne della shell. Il secondo esempio ha richiesto #! perché la riga di assegnamento di variabile, linee=50, utilizza un costrutto specifico della shell. [5] È da notare ancora che #!/bin/sh invoca l'interprete di shell predefinito, che corrisponde a /bin/bash su una macchina Linux.

Suggerimento

Questo manuale incoraggia l'approccio modulare nella realizzazione di uno script. Si annotino e si raccolgano come "ritagli" i frammenti di codice che potrebbero rivelarsi utili per degli script futuri. Addirittura si potrebbe costruire una libreria piuttosto ampia di routine. Come, ad esempio, la seguente parte introduttiva di uno script che verifica se lo stesso è stato eseguito con il numero corretto di parametri.

E_ERR_ARG=65
parametri_dello_script="-a -h -m -z"
#                       -a = all, -h = help, ecc.

if [ $# -ne $Numero_di_argomenti_attesi ]
then
  echo "Utilizzo: `basename $0` $parametri_dello_script"
  # `basename $0` è il nome dello script.
  exit $E_ERR_ARG
fi

Spesso scriverete uno script che svolge un compito specifico. Il primo script di questo capitolo ne rappresenta un esempio. Successivamente potrebbe sorgere la necessità di generalizzare quello script, in modo che possa svolgere altri compiti simili. Sostituire le costanti letterali ("codificate") con delle variabili rappresenta un passo in tale direzione, così come sostituire blocchi di codice che si ripetono con delle funzioni.

2.1. Eseguire uno script

Dopo aver creato uno script, lo si può eseguire con sh nomescript [6] o, in alternativa, con bash nomescript. Non è raccomandato l'uso di sh <nomescript perché, così facendo, si disabilita la lettura dallo stdin all'interno dello script. È molto più conveniente rendere lo script eseguibile direttamente con chmod.

O con:

chmod 555 nomescript (che dà a tutti gli utenti il permesso di lettura/esecuzione) [7]

o con

chmod +rx nomescript (come il precedente)

chmod u+rx nomescript (che attribuisce solo al proprietario dello script il permesso di lettura/esecuzione)

Una volta reso eseguibile, se ne può verificare la funzionalità con ./nomescript. [8] Se la prima riga inizia con i caratteri "#!" , all'avvio lo script chiamerà, per la propria esecuzione, l'interprete dei comandi specificato.

Come ultimo passo, dopo la verifica e il debugging, probabilmente si vorrà spostare lo script nella directory /usr/local/bin (naturalmente, operazione da eseguire come root) per renderlo disponibile, oltre che per se stessi, anche agli altri utenti, quindi come eseguibile di sistema. In questo modo lo script potrà essere messo in esecuzione semplicemente digitando nomescript [INVIO] da riga di comando.

Note

[1]

Alcune versioni UNIX (quelle basate su BSD 4.2) utilizzano un magic number a quattro byte, che richiede uno spazio dopo il ! -- #! /bin/sh. Tuttavia, in accordo con Sven Mascheck, probabilmente si tratta di un mito.

[2]

La riga con #! dovrà essere la prima cosa che l'interprete dei comandi (sh o bash) incontra. In caso contrario, dal momento che questa riga inizia con #, verrebbe correttamente interpretata come un commento.

Se, infatti, lo script include un'altra riga con #!, bash la interpreterebbe correttamente come un commento, dal momento che il primo #! ha già svolto il suo compito.

#!/bin/bash

echo "Parte 1 dello script."
a=1

#!/bin/bash
# Questo *non* eseguirà un nuovo script.

echo "Parte 2 dello script."
echo $a  # Il valore di $a è rimasto 1.

[3]

Ciò permette degli ingegnosi espedienti.

#!/bin/rm
# Script che si autocancella.

# Niente sembra succedere quando viene eseguito ... solo che il file scompare.

QUALUNQUECOSA=65

echo "Questa riga non verrà mai visualizzata (scommettete!)."

exit $QUALUNQUECOSA  # Niente paura. Lo script non terminerà a questo punto.

Provate anche a far iniziare un file README con #!/bin/more e rendetelo eseguibile. Il risultato sarà la visualizzazione automatica del file di documentazione. (Un here document con l'uso di cat sarebbe probabilmente un'alternativa migliore -- vedi Esempio 18-3).

[4]

Portable Operating System Interface, un tentativo di standardizzare i SO di tipo UNIX. Le specifiche POSIX sono elencate sul sito del Open Group.

[5]

Se Bash è la vostra shell preimpostata, allora #! all'inizio dello script diventano superflui. Tuttavia, se lo script viene lanciato da una shell diversa, come tcsh, #! saranno indispensabili.

[6]

Attenzione: richiamando uno script Bash con sh nomescript si annullano le estensioni specifiche di Bash e, di conseguenza, se ne potrebbe compromettere l'esecuzione.

[7]

Uno script, per essere eseguito, ha bisogno, oltre che del permesso di esecuzione, anche di quello di lettura perché la shell deve essere in grado di leggerlo.

[8]

Perché non invocare semplicemente uno script con nomescript? Se la directory in cui ci si trova ($PWD) è anche quella dove nomescript è collocato, perché il comando non funziona? Il motivo è che, per ragioni di sicurezza, la directory corrente, di default, non viene inclusa nella variabile $PATH dell'utente. È quindi necessario invocare esplicitamente lo script che si trova nella directory corrente con ./nomescript.