Programmation de scripts en shell

Un shell, quel qu'il soit, peut exécuter des commandes prises dans un fichier. Un fichier contenant des commandes pour le shell est appelé un script. C'est en fait un programme écrit dans le langage du shell. Ce langage comprend non seulement les commandes que nous avons déjà vues, mais aussi des structures de contrôle (constructions conditionnelles et boucles).

Pour la programmation du shell, nous allons utiliser le shell sh, qui est le plus répandu et standard. Ce que nous avons vu jusqu'ici s'applique aussi bien à sh qu'à zsh et aussi à csh, à l'exception de setenv et de certaines redirections signalées.

Créer un script

Rendre un script exécutable

Pour être un script, un fichier doit commencer par la ligne:

#!/bin/sh

Il doit aussi avoir être exécutable (bit x). Le #!/bin/sh sur la première ligne indique que ce script doit être exécuté par le shell sh dont on indique le chemin d'accès. Pour rendre un fichier exécutable, tapez :

chaland ~ chmod u+x fichier

(pour en savoir plus sur les droits attachés à un fichier, consultez la page sur les droits d'accès).

Le chemin d'une commande

Pour comprendre ce qui suit, vous devez savoir ce qu'est le PATH. Si ce n'est pas le cas, lisez la page principale sur le shell.

Quand vous exécutez un script, vous pouvez vous trouver à n'importe quel endroit de l'arborescence de vos répertoires. Si le répertoire courant ne se situe pas dans votre PATH et que vous voulez exécuter un programme qui s'y trouve, vous ne pouvez pas taper :

clipper ~ commande

car si le répertoire courant n'est pas dans le PATH, le shell n'ira pas y chercher commande.

Vous recevrez donc un message comme :

clipper ~ commande
zsh: command not found: commande

Spécifier le chemin d'une commande

Pour que le shell comprenne où chercher votre commande, il faut donc spécifier l'emplacement de la commande en donnant son chemin, qu'il soit absolu :

clipper ~ /home/toto/repertoire/courant/commande

ou relatif :

clipper ~ repertoire/courant/commande

ou encore sous la forme :

clipper ~/repertoire/courant ./commande

Le répertoire ~/bin

Il y a un certain nombre de commandes que l'on peut vouloir utiliser depuis n'importe quel répertoire. Dans ce cas, il est fastidieux de :

Il suffit donc de mettre tous vos scripts dans un même répertoire, et de mettre ce répertoire dans le PATH. Par convention, ce répertoire s'appelle bin et se place dans votre répertoire personnel. Si votre répertoire personnel est /home/toto, ce répertoire sera donc /home/toto/bin.

Commencez donc par créer ce répertoire :

clipper ~ mkdir bin

Ensuite, vérifiez qu'il soit bien dans votre PATH :

clipper ~ echo $PATH

Si vous voyez par exemple $HOME/bin dans votre PATH, alors c'est bon, tous les fichiers exécutables situés dans ce répertoire seront accessibles depuis n'importe quel répertoire.

Si ce n'est pas le cas, il faut ajouter ce répertoire au PATH. Pour cela, ajoutez dans le fichier de configuration de votre shell, par exemple le fichier .zshrc, la ligne :

PATH=$PATH:$HOME/bin

Cette ligne indique que la prochaine fois que vous ouvrirez votre shell, le répertoire bin figurera dans votre PATH.

Principes généraux des scripts shell

Une succession de commandes

Si vous manipulez déjà le shell en ligne de commande, vous pouvez commencer vos premiers scripts. Un script shell est en effet avant tout une succession de commandes.

Par exemple, si vous avez coutume de taper successivement, quand vous vous loguez à l'ENS :

clipper ~ mozilla &
clipper ~ mutt

vous pouvez vous créer le script suivant dans le fichier ~/bin/amorce :

#!/bin/sh

mozilla &
mutt

Ainsi, dès que vous vous connectez, vous pouvez taper amorce dans le shell, et vos commandes s'exécuteront automatiquement.

Commentaires

Presque tous les langages informatiques autorisent d'insérer des commentaires ; le shell n'échappe pas à la règle. Pour cela, il suffit de faire précéder chaque ligne de commentaire du caractère « # ». Exemple :

#!/bin/sh

# Tout ce que j'écris ici ne sera pas lu.
echo "Ce que je tape ici sera lu."

Les lignes blanches ne sont pas interprétées non plus. N'hésitez donc surtout pas à espacer votre script, les lignes blanches ne consomment presque rien en termes d'espace disque, ce n'est donc pas une ressource rare.

L'impératif de lisibilité

Pourquoi espacer son script ? Pourquoi insérer des commentaires ? Pour une seule et même raison : votre script doit être lisible. Pourquoi être lisible ?

D'abord, pour autrui : si d'autres gens lisent votre script, il doit être intelligible, et les passages complexes doivent être explicités par des commentaires.

Ensuite, pour vous-même ; au moment où vous écrivez un script, vous comprenez tout, naturellement ; mais si vous le relisez dans quelques mois, voire quelques années, les passages obscurs risqueront d'être incompréhensibles, ce qui est particulièrement pénible quand on essaye de débuguer un programme, c'est-à-dire d'en corriger les erreurs.

Structures de contrôle

C'est avec les structures de contrôle qu'un programme peut commencer à devenir sérieux. Le principe général de ces structures est le suivant : adapter le comportement du programme selon les réponses apportées à certaines questions.

Nous avons déjà vu une application possible des structures de contrôle en parlant des variables. Le programme hachoir conçu par Robert le jardinier pose la question suivante : est-ce que $USER vaut « Robert » ?

La boucle if

La structure de contrôle if est la plus courante de toutes, et la plus élémentaire. Elle est constituée de quatre termes :

  1. if (si), qui marque la condition à remplir ;
  2. then (alors), qui marque les conséquences si la condition est remplie ;
  3. else (sinon), qui est facultatif et marque le comportement à adopter si la condition n'est pas remplie ;
  4. fi (if à l'envers), qui marque la fin de la boucle.
if commande ; then commandes ; else commandes ; fi

ou bien (car le point et virgule équivaut à un saut de ligne) :

if commande  
then commandes 
else commandes  
fi

Par exemple, pour Robert le jardinier, on aura :

if test $USER = Robert
    then hacher_menu_comme_chair_à_pâté $*
    else echo "Quand tu seras grand, $USER." 
         echo "Et fais bien attention en traversant la rue."
fi

La boucle for

La boucle for affecte successivement une variable chaque chaîne de caractères trouvée dans une liste de chaînes, et exécute les commandes une fois pour chaque chaîne.

for var in liste de chaînes ; do commandes ; done

ou bien :

for var in liste de chaînes 
do commandes 
done

Rappel : $var accède à la valeur courante de var. La partie commandes est une suite de commandes, séparées par des points et virgules (;) ou des retours à la ligne.

Exemple :

for i in *; do echo "$i"; done

affiche les noms de tous les fichiers du répertoire courant, un par ligne.

Remarque : plus un programme grossit, plus il importe que son contenu soit lisible. Les noms de variable doivent donc être le plus lisibles possible, pour permettre à d'autres gens, ou bien à vous-même dans quelques mois, de comprendre rapidement votre script. Ainsi, il peut être plus explicite d'écrire, au lieu de « for i in *; do echo "$i"; done » :

for fichier in *; do echo "$fichier"; done

En outre, pour des raisons de lisibilité, n'hésitez pas à gaspiller des lignes de code en sautant des lignes ; les lignes de code ne sont pas une ressource rare :

for fichier in * 
do echo "$fichier" 
done

La boucle while

while commande  
do commande  
done

La boucle while exécute les commandes de manière répétée tant que la première commande réussit.

Par exemple : tant que le mot de passe correct n'a pas été donné, refuser l'accès et redemander le mot de passe.

La boucle case

case chaîne in
  pattern) commande ;;
  pattern) commande ;;
esac

La boucle case exécute la première commande telle que la chaîne est de la forme pattern. Un pattern (motif) est un mot contenant éventuellement les constructions *, ?, [a-d], avec la même signification que pour les raccourcis dans les noms de fichiers. Exemple :

case $var in
  [0-9]*) echo 'Nombre';;
  [a-zA-Z]*) echo 'Mot';;
  *) echo 'Autre chose';;
esac

Vous observerez que l'on clôt la boucle case avec esac, qui est le mot case à l'envers, de même que l'on clôt if avec fi.

Code de retour

On remarque que la condition des commandes if et while est une commande. Chaque commande renvoie un code de retour (qui est ignoré en utilisation normale). Si le code est 0, la commande a réussi ; sinon, la commande a échoué. Par exemple, le compilateur cc renvoie un code d'erreur non nul si le fichier compilé contient des erreurs, ou s'il n'existe pas.

Les commandes if et while considèrent donc le code de retour 0 comme « vrai », et tout autre code comme « faux ».

Il existe une commande test, qui évalue des expressions booléennes (c'est-à-dire dont la valeur ne peut être que vraie ou fausse, 1 ou 0) passées en argument, et renvoie un code de retour en fonction du résultat. Elle est bien utile pour les scripts. Exemple :

if test $var = foo
then echo 'La variable vaut foo'
else echo 'La variable ne vaut pas foo'
fi

Vous êtes maintenant en mesure de faire ces exercices pour vous entraîner.

Basé sur un polycopié de Roberto Di Cosmo, Xavier Leroy et Damien Doligez. Modifications : Nicolas George, Baptiste Mélès. Dernière modification le .