2. Lancement avec un #!

La programmation shell est un juke box des années 50...

-- Larry Wall

Dans le cas le plus simple, un script n'est rien de plus qu'une liste de commandes système enregistrées dans un fichier. À tout le moins, cela évite l'effort de retaper cette séquence particulière de commandes à chaque fois qu'elle doit être appelée.

Exemple 2.1. cleanup : Un script pour nettoyer les journaux de trace dans /var/log

# cleanup
# À exécuter en tant que root, bien sûr.

cd /var/log
cat /dev/null > messages
cat /dev/null > wtmp
echo "Journaux nettoyés."

Il n'y a rien d'inhabituel ici, seulement un ensemble de commandes qui pourraient tout aussi bien être appelées l'une après l'autre à partir de la ligne de commande sur la console ou dans une émulation xterm. Les avantages de les placer dans un script vont bien au-delà de ne pas avoir à les retaper. Le script devient un outil et peut facilement être modifié ou personnalisé pour une application particulière.

Exemple 2.2. cleanup : Un script de nettoyage amélioré

#!/bin/bash
# En-tête propre d'un script Bash.

# Nettoyage, version 2

# À exécuter en tant que root, bien sûr
# Insérez du code ici pour afficher les messages d'erreur et sortir si
# l'utilisateur n'est pas root.

REP_TRACES=/var/log
# Les variables sont préférées aux valeurs codées en dur.
cd $REP_TRACES

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


echo "Journaux nettoyés."

exit # La bonne méthode pour "sortir" d'un script.

Maintenant, cela commence à ressembler à un vrai script. Mais nous pouvons aller encore plus loin...

Exemple 2.3. cleanup : Une version améliorée et généralisée des scripts précédents

#!/bin/bash
# Nettoyage, version 3.

#  Attention :
#  -----------
#  Ce script utilise un nombre de fonctionnalités qui seront expliquées bien
#+ après.
#  Après avoir terminé la première moitié de ce livre, il ne devrait plus comporter
#+ de mystère.

REP_TRACES=/var/log
UID_ROOT=0     # Seuls les utilisateurs avec un $UID valant 0 ont les droits de root.
LIGNES=50      # Nombre de lignes sauvegardées par défaut.
E_XCD=66       # On ne peut pas changer de répertoire?
E_NONROOT=67   # Code de sortie si non root.


# À exécuter en tant que root, bien sûr.
if [ "$UID" -ne "$UID_ROOT" ]
then
  echo "Vous devez être root pour exécuter ce script."
  exit $E_NONROOT
fi  

if [ -n "$1" ]
# Teste si un argument est présent en ligne de commande (non vide).
then
  lignes=$1
else  
  lignes=$LIGNES # Par défaut, s'il n'est pas spécifié sur la ligne de commande.
fi  


#  Stephane Chazelas suggère ce qui suit,
#+ une meilleure façon de vérifier les arguments en ligne de commande,
#+ mais c'est un peu trop avancé à ce stade du tutoriel.
#
#    E_MAUVAISARGS=65  # Argument non numérique (mauvais format de l'argument)
#
#    case "$1" in
#    ""      ) lignes=50;;
#    *[!0-9]*) echo "Usage: `basename $0` Nbre_de_Ligne_a_Garder"; exit $E_MAUVAISARGS;;
#    *       ) lignes=$1;;
#    esac
#
#* Passer au chapitre "Boucle" pour comprendre tout ceci.


cd $REP_TRACES

if [ `pwd` != "$REP_TRACES" ]  # ou   if [ "$PWD" != "$REP_TRACES" ]
                            # Pas dans /var/log ?
then
  echo "Impossible d'aller dans $REP_TRACES."
  exit $E_XCD
fi  # Double vérification du bon répertoire, pour ne pas poser problème avec le
    # journal de traces.

# bien plus efficace:
#
# cd /var/log || {
#   echo "Impossible d'aller dans le répertoire." >&2
#   exit $E_XCD;
# }




tail -n $lignes messages > mesg.temp # Sauvegarde la dernière section du journal
                                   # de traces.
mv mesg.temp messages              # Devient le nouveau journal de traces.


# cat /dev/null > messages
#* Plus nécessaire, car la méthode ci-dessus est plus sûre.

cat /dev/null > wtmp  #  ': > wtmp' et '> wtmp'  ont le même effet.
echo "Journaux nettoyés."

exit 0
#  Un code de retour zéro du script indique un succès au shell.

Comme vous pouvez ne pas vouloir supprimer toutes les traces système, cette variante du script conserve la dernière section des traces intacte. Vous découvrirez en permanence de nouvelles façons pour affiner des scripts précédemment écrits et améliorer ainsi leur efficacité.

Le sha-bang ( #!) en en-tête de ce fichier indique à votre système que ce fichier est un ensemble de commandes pour l'interpréteur indiqué. Les caractères #! sont codés sur deux octets [5] et correspondent en fait à un nombre magique, un marqueur spécial qui désigne un type de fichier, ou dans ce cas, un script shell exécutable (lancez man magic pour plus de détails sur ce thème fascinant). Tout de suite après le sha-bang se trouve un chemin. C'est le chemin vers le programme qui interprète les commandes de ce script, qu'il soit un shell, un langage de programmation ou un utilitaire. Ensuite, cet interpréteur de commande exécute les commandes du script, en commençant au début (ligne après le #!), en ignorant les commentaires. [6]

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

Chacune des lignes d'en-tête du script ci-dessus appelle un interpréteur de commande différent, qu'il soit /bin/sh, le shell par défaut (bash dans un système Linux) ou autre chose.

[7]

Utiliser #!/bin/sh, par défaut Bourne Shell dans la plupart des variantes commerciales d'UNIX, rend le script portable aux machines non-Linux, malheureusement en faisant le sacrifice des fonctionnalités spécifiques à Bash. Le script se conformera néanmoins au standard sh de POSIX [8]

Notez que le chemin donné à « sha-bang » doit être correct, sinon un message d'erreur -- habituellement « Command not found » -- sera le seul résultat du lancement du script.

#! peut être omis si le script consiste seulement en un ensemble de commandes système génériques, sans utiliser de directives shell interne. Le second exemple, ci-dessus, requiert le #! initial car la ligne d'affectation des variables, lignes=50, utilise une construction spécifique au shell. [9] Notez encore que #!/bin/sh appelle l'interpréteur shell par défaut, qui est /bin/bash sur une machine Linux.

[Astuce]

Astuce

Ce tutoriel encourage une approche modulaire de la construction d'un script. Prenez note et collectionnez des astuces sous forme de « blocs simples » de code pouvant être utiles pour de futurs scripts. À la longue, vous pouvez obtenir une bibliothèque assez étendue de routines bien conçues. Comme exemple, le début du script suivant teste si le script a été appelé avec le bon nombre de paramètres.

E_MAUVAIS_ARGS=65
parametres_scripts="-a -h -m -z"
#                  -a = all, -h = help, etc.

if [ $# -ne $Nombre_arguments_attendus ]
then
echo "Usage: `basename $0` $parametres_scripts"
  # `basename $0` est le nom du fichier contenant le script.
  exit $E_MAUVAIS_ARGS
fi

De nombreuses fois, vous écrirez un script réalisant une tâche particulière. Le premier script de ce chapitre en est un exemple. Plus tard, il pourrait vous arriver de généraliser le script pour faire d'autres tâches similaires. Remplacer les constantes littérales (« codées en dur ») par des variables est une étape dans cette direction, comme le fait de remplacer les blocs de code répétitifs par des fonctions.

2.1. Appeler le script

Après avoir écrit le script, vous pouvez l'appeler avec sh nom_script [10], ou avec bash nom_script (il n'est pas recommandé d'utiliser sh nom_script car cela désactive la lecture de stdin à l'intérieur du script). Il est bien plus aisé de rendre le script directement exécutable avec un chmod.

Soit

chmod 555 nom_script (donne les droits de lecture/exécution à tout le monde) [11]

soit

chmod +rx nom_script (donne les droits de lecture et d'exécution à tout le monde)

chmod u+rx nom_script (donne les droits de lecture et d'exécution seulement à son propriétaire)

Maintenant que vous avez rendu le script exécutable, vous pouvez le tester avec ./nom_script [12]. S'il commence par une ligne « sha-bang », appeler le script appelle le bon interpréteur de commande.

Enfin, après les tests et le débogage final, vous voudrez certainement le déplacer dans /usr/local/bin (en tant que root, bien sûr), pour le rendre utilisable par vous et par tous les autres utilisateurs du système. Le script pourra alors être appelé en tapant simplement nom_script [ENTER] sur la ligne de commande.



[5] Certains systèmes UNIX (ceux basés sur 4.2BSD) prétendent coder ce nombre magique sur quatre octets, réclamant une espace après le !, #! /bin/sh. Néanmoins, d'après Sven Mascheck, c'est probablement un mythe.

[6] La ligne #! d'un script shell est la première chose que l'interpréteur de commande (sh ou bash) voit. Comme cette ligne commence avec un #, il sera correctement interprété en tant que commentaire lorsque l'interpréteur de commandes exécutera finalement le script. La ligne a déjà été utilisé pour appeler l'interpréteur de commandes.

En fait, si le script inclut une ligne #! supplémentaire, alors bash l'interprètera comme un commentaire.

#!/bin/bash

echo "Partie 1 du script."
a=1

#!/bin/bash
# Ceci ne lance *pas* un nouveau script.

echo "Partie 2 du script."
echo $a  # Valeur de $a est toujours 1.

[7] Ceci permet des tours de passe-passe.

#!/bin/rm
# Script se supprimant lui-même.

#  Rien de plus ne semble se produire lorsque vous lancez ceci... si on enlève
#+ le fait que le fichier disparait.

QUOIQUECESOIT=65

echo "Cette ligne ne s'affichera jamais."

exit $QUOIQUECESOIT  # Importe peu. Le script ne se terminera pas ici.
                # Tester un echo $? après la fin du script.
                # Vous obtiendrez 0, au lieu de 65.

De la même manière, essayer de lancer un fichier README avec un #!/bin/more après l'avoir rendu exécutable. Le résultat est un fichier de documentation s'affichant lui-même. (Un document en ligne utilisant cat est certainement une meilleure alternative -- voir Exemple 18.3, « Message multi-lignes en utilisant cat »).

[8] Portable Operating System Interface, an attempt to standardize UNIX-like OSes (NdT : interface de systèmes d'exploitation portables, un essai pour standardiser les UNIX). Les spécifications POSIX sont disponibles sur le site Open Group.

[9] Si Bash est votre shell par défaut, alors #! n'est pas nécessaire. Par contre, si vous lancez un script à partir d'un shell différent, comme tcsh, alors vous aurez besoin de #!.

[10] Attention : appeler un script Bash avec sh nom_script désactive les extensions spécifiques à Bash, et donc le script peut ne pas fonctionner.

[11] Pour pouvoir être lancé, un script a besoin du droit de lecture (read) en plus de celui d'exécution, car le shell a besoin de le lire.

[12] Pourquoi ne pas simplement appeler le script avec nom_script ? Si le répertoire où vous vous trouvez ($PWD) est déjà celui où se trouve nom_script, pourquoi cela ne fonctionne-t'il pas ? Cela échoue parce que, pour des raisons de sécurité, le répertoire courant (./) n'est pas inclus par défaut dans le $PATH de l'utilisateur. Il est donc nécessaire d'appeler le script de façon explicite dans le répertoire courant avec ./nom_script.