Les bases de l’écriture d’un script
Forme générale d’un script
Un script est ni plus ni moins qu’un enchaînement de commandes, les unes après les autres, sous la forme d’un fichier texte. Cependant, certaines règles sont à respecter pour qu’un shell puisse comprendre ce que l’on attend de lui.
1. Nommage de fichier
Un script shell porte généralement l’extension .sh ; cependant, cela n’est pas une obligation : certains scripts portent d’autres extensions, d’autres scripts n’en portent pas du tout.
Il est préférable, de manière générale, de réserver l’extension .sh aux scripts compatibles POSIX. De cette manière, on sait que ce script peut être exécuté avec n’importe quel shell qui respecte cette norme. Par conséquent, lorsque l’on développe un script qui utilise des spécificités d’un shell particulier, il vaut mieux utiliser une extension qui indique clairement le script à utiliser ; les shells portant des noms plutôt courts, on utilise simplement leur nom pour l’extension : .bash pour bash, .zsh pour Zsh...
La plupart des systèmes d’exploitation autorisent les espaces dans les noms de fichiers. Cela est également valable pour les noms des scripts ; de même pour les caractères spéciaux, accentués et autres. Cependant, un script sera amené à être appelé en ligne de commande, son nom tapé à la main. Pour cette raison, il est préférable de garder un nom simple, court, sans caractère spécial, sans accent, comme pour n’importe quel nom de commande car même si un nom peut être tapé sur votre clavier, cela ne veut pas dire qu’il peut l’être sur n’importe quel clavier : pensez simplement aux claviers QWERTY qui ne proposent pas les caractères...
Variables et assignation
Lorsque l’on tape des commandes à la main, on utilise rarement des variables, même si cela est possible. Dans un script par contre, la possibilité de créer des variables est beaucoup exploitée. En effet, quel serait l’intérêt d’un script où tout serait écrit dans le marbre, qui ne pourrait toujours exécuter que les mêmes commandes ?
Les exemples ci-après sont repris dans le script ch4/variables.sh des données téléchargeables, vous permettant de visualiser les différents cas en direct.
1. Assignation d’une valeur
Comme avec n’importe quel langage de programmation, l’assignation d’une valeur à une variable se fait avec le signe d’égalité « = ». Cependant, la syntaxe dans son ensemble est différente de ce que l’on peut trouver dans les autres langages. En effet, le fonctionnement d’un shell suit toujours le même schéma : une instruction, c’est une commande suivie d’arguments. Par conséquent, il n’est notamment pas possible de mettre des espaces autour du signe « = », car de tels espaces signifieraient que le nom de variable est une commande, que son premier argument est le signe d’égalité et son second argument est la valeur qu’on essaie de placer dans la variable :
var="val"
... ceci est une syntaxe correcte.
var = "val"
... cette ligne revient à exécuter la commande var avec les arguments « = » et « val ».
Bien que certains shells acceptent des noms de variables avec caractères spéciaux (notamment les accents), il est préférable de ne pas les utiliser : la meilleure façon d’être en sécurité est de se limiter aux caractères alphabétiques de base, ainsi qu’éventuellement le caractère de soulignement « _ ». Les majuscules et les minuscules, quant à elles, sont correctement différenciées.
2. Type de données
Dans l’exemple ci-dessus, la valeur a été explicitement donnée sous forme d’une chaîne de caractères, entourée de guillemets. En réalité...
Environnement et communication
Les scripts shell sont rarement imaginés comme des entités totalement indépendantes et n’interagissant pas avec leur environnement. Bien au contraire, leur rôle est souvent lié à une interaction avec d’autres éléments, en particulier un utilisateur.
1. Entrée et sorties
Comme tous les processus exécutés sur une machine UNIX, un script shell en cours d’exécution a une entrée (stdin) et deux sorties (stdout et stderr).
Lorsqu’il est exécuté à la main par un utilisateur, un script est « connecté » avec le processus qui l’a exécuté (son parent), par exemple un terminal. Cela signifie que son entrée standard reçoit les données transmises par ce parent (dans le cas d’un terminal, ce sont les frappes au clavier) et sa sortie standard est envoyée à ce parent (dans le cas d’un terminal, celui-ci affiche à l’écran ce qui est retourné par le script).
C’est cela qui permet, par exemple, d’obtenir des données de la part de l’utilisateur avec la commande read (concrètement, cette commande lit sur stdin, elle n’est pas dédiée exclusivement à l’interaction avec un clavier) ou d’afficher des messages avec la commande echo (de la même manière, elle n’interagit pas avec un affichage, elle envoie simplement des données sur stdout).
Lorsqu’un script est exécuté automatiquement, dans le cadre d’une tâche programmée avec le programme cron notamment (cf. chapitre Automatisation - Planification régulière), cela fonctionne différemment. En effet, une tâche programmée n’est pas reliée à un terminal. Dans ce cas, bien souvent, l’entrée standard du script est totalement déconnectée (impossibilité de poser une question à l’utilisateur avec la commande read) et ses sorties sont soit déconnectées elles aussi (sorties textuelles perdues), soit récupérées pour être envoyées dans un e-mail par exemple. Ceci s’applique bien sûr non seulement aux scripts, mais aussi à n’importe quel programme exécuté...
Enchaînements et redirections
La puissance des scripts shell et des nombreuses commandes tient dans le principe KISS : chaque outil a son propre but. Pour que cela soit utile, il est nécessaire de pouvoir faire interagir les différentes commandes que l’on appelle dans le script.
1. Redirections des entrées/sorties
Les flux d’entrée et de sortie des commandes (stdin, stdout, stderr) peuvent être redirigés, afin d’avoir une autre entrée que le clavier et une autre sortie que l’écran. Dans le cadre de scripts exécutés automatiquement, cela permet d’effectuer des traitements sur la base de données contenues dans des fichiers précédemment créés, ou encore de générer une sortie formatée d’une certaine manière ou encore des logs témoignant du bon (ou du mauvais) fonctionnement du script.
a. Redirection de la sortie standard
Le flux de sortie stdout peut être redirigé dans un fichier grâce à l’opérateur « > », suivi du nom du fichier dans lequel écrire la sortie en question :
ls -1 /etc > /tmp/configuration.txt
… cette commande enregistre la liste des fichiers du répertoire /etc dans le fichier /tmp/configuration.txt, en écrasant ce fichier s’il existe déjà.
On peut également demander au shell de ne pas écraser le fichier, mais d’ajouter la sortie de la commande à la suite du fichier, avec l’opérateur « >> » :
echo -n " liste datant du " > /tmp/configuration.txt
date >> /tmp/configuration.txt
ls -1 /etc >> /tmp/configuration.txt
… ces trois commandes enregistrent cette liste de fichiers dans le même fichier, en la faisant précéder par une ligne « liste datant du [date du jour] ».
On pourra également découvrir ici l’existence du pseudo-fichier /dev/null. Celui-ci est une espèce de « trou noir » vers lequel on peut renvoyer tout flux qui ne nous intéresse pas, lorsque l’on ne veut pas l’afficher à l’écran :
ls -1 /etc > /dev/null
… cette commande fait la liste des fichiers du répertoire...
Fonctions
Tout comme la plupart des autres langages de programmation, on peut organiser le code d’un shell script en fonctions. Cela permet notamment de réutiliser une même série de commandes plusieurs fois, respectant ainsi le principe DRY, Don’t Repeat Yourself (« ne vous répétez pas »).
L’utilisation des fonctions est illustrée dans le script ch4/fonctions.sh des éléments téléchargeables.
1. Déclaration et appel
La déclaration d’une fonction dans un shell script est extrêmement simple :
nom_de_la_fonction() {
contenu de la fonction
}
Aucun préfixe n’est accolé devant le nom de la fonction, aucun argument n’est précisé entre les parenthèses. Toute fonction doit être déclarée avant d’être appelée : on place alors généralement les fonctions en début de script.
Pour faire appel à cette fonction, il suffit de l’exécuter comme n’importe quelle instruction, en donnant son nom comme commande à exécuter. Cela signifie notamment que si vous définissez une fonction qui porte le même nom qu’une commande, cette commande deviendra inaccessible dans ce script avec son nom.
Pour utiliser une commande dont le nom a été « écrasé »...
Inclusion d’un autre fichier
En général, des variables ou des fonctions créées dans un script ne peuvent être utilisées que par lui-même. Ceci même si l’on exécute ce script, car l’exécution d’un script correspond à l’instanciation d’un nouveau shell, qui n’a aucun contrôle sur le shell courant (ou sur le script courant).
Cependant, on peut vouloir factoriser son code en créant un fichier contenant les fonctions communes à plusieurs scripts, puis inclure ce fichier dans les scripts concernés. Pour cela, on utilise la commande « . » (oui, un simple point). Dans certains shells, cette commande peut aussi être appelée source, mais ça ne fait pas partie de la norme POSIX.
On peut par exemple avoir un fichier bibliotheque.sh comme suit :
alerte() {
echo "Attention : $@ !"
}
Ce fichier peut être inclus et utilisé dans un autre script mon_script.sh de la manière suivante :
. bibliotheque.sh
alerte "Ça fonctionne"
Exécuter cette commande retournerait, comme on s’y attend :
$ ./mon_script.sh
Attention : Ça fonctionne !