2. Le lancement avec #!

La programmation shell est comme 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

# Nettoyage
# À exécuter en tant que root, évidemment.

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 un terminal. Mais les avantages de les placer dans un script vont bien au-delà du simple fait ne pas avoir à les retaper continuellement. C'est que le script devient ainsi un programme -- ou un outil --, facile à modifier et à personnaliser pour une application particulière.

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

#!/bin/bash
# Entête correcte pour un script Bash.

# Nettoyage, version 2

# À exécuter en tant que root, évidemment.
#  Insérer ici ce qu'il faut de code pour sortir en 
#+ imprimant un message d'erreur si on n'est pas root.

REP_TRACES=/var/log
# Les variables valent mieux que les valeurs codées en dur.
cd $REP_TRACES

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


echo "Journaux nettoyés."

exit # C'est la bonne méthode pour "quitter" un script.
     #  Un "exit" nu (sans paramètre) renvoie le code de sortie
     #+ de la commande qui précède.

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 quelques fonctionnalités qui seront expliquées 
#+ bien après.
#  Lorsque vous aurez lu toute la première moitié de ce livre, plus
#+ rien ne vous paraîtra mystérieux.

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, évidemment.
if [ "$UID" -ne "$UID_ROOT" ]
then
  echo "Vous devez être root pour exécuter ce script."
  exit $E_NONROOT
fi  

if [ -n "$1" ]
# Teste la présence d'un argument (non vide) sur la ligne de commande.
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 de la 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 "Boucles" pour tout comprendre.


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 effacer d'un coup tous les journaux système, cette variante du script conserve la dernière section des traces intacte. Vous découvrirez constamment en permanence de nouvelles méthodes pour affiner des scripts précédemment écrits et améliorer ainsi leur efficacité.

* * *

Le sha-bang (#!) [6] en tête de 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 [7] 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 par le début (la ligne suivant immédiatement celle avec le #!) et en ignorant les commentaires. [8]

#!/bin/sh
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/bin/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.

[9]

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 [10]

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. [11]

#! 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. [12] 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 de scripts. Prenez des notes, collectionnez les astuces sous forme de « blocs simples » de code pouvant être utiles pour de futurs scripts. À la longue, vous disposerez d'une bibliothèque assez fournie de routines bien conçues. À titre d'exemple, le début du script suivant teste si le script a été appelé avec le bon nombre de paramètres.

E_MAUVAIS_ARGS=85
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

Très souvent, vous écrirez un script pour réaliser 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 effectuer 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. Appel du script

Après avoir écrit un script, vous pouvez l'appeler avec la commande sh nom_script [13], ou avec bash nom_script (il n'est pas recommandé d'utiliser sh <nom_script car cela désactive la lecture de l'entrée standard stdin depuis l'intérieur du script). Plus simplement, on peut rendre le script directement exécutable avec un chmod.

Soit

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

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 [15]. 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.



[6] Sous cette forme, on le voit plus communément dans la littérature que sous la forme de she-bang ou de sh-bang. Ce terme est dérivé de la concaténation des caractères # (en anglais, sharp) et ! (en anglais, bang).

[7] 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. Aux dires de Sven Mascheck, c'est probablement un mythe.

[8] 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.

[9] Ceci permet des tours de passe-passe :

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

#  Rien de plus ne semble se produire lorsque vous le lancez... sauf
#+ que le fichier disparaît.

QUELQUECHOSE=85

echo "Cette ligne ne s'affichera jamais."

exit $QUELQUECHOSE  # Peu importe. Le script ne se terminera pas ici.
                # Tester un echo $? après la fin du script.
                # Vous obtiendrez 0, au lieu de 85.

De la même manière, essayez 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 solution -- voir Exemple 19.3, « Message multi-lignes en utilisant cat »).

[10] 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.

[11] Pour éviter ce risque, un script peut commencer avec une ligne sha-bang, #!/bin/env bash. Ceci pourrait être utile sur les machines UNIX où bash n'est pas dans /bin

[12] 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 #!.

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

[14] 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.

[15] 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.