Introduction à l'écriture de scripts shell — Les bases

Gazette Linux n°111 — Février 2005

Ben Okopnik

Article paru dans le n°111 de la Gazette Linux de février 2005.

Traduction française par Raphaël Semeteys .

Relecture de la traduction française par Joëlle Cornavin .

Article publié sous Open Publication License. La Linux Gazette n'est ni produite, ni sponsorisée, ni avalisée par notre hébergeur principal, SSC, Inc.


Table des matières
1. Introduction
2. Philosophie de l'écriture de scripts
3. Prérequis
4. Construction d'un script
5. Amélioration du script
6. Résumé
7. Récapitulation
8. Références

1. Introduction

L'écriture de scripts shell est une combinaison intéressante d'art et de science qui vous donne accès à la souplesse et à la puissance incroyables de Linux avec des outils très simples. Au début de l'ère des PC, j'étais considéré comme un expert des fichiers de traitement par lots (batch files) de DOS, ce qui, je le réalise à présent, était une pêle et inconsistante imitation des shells d'Unix. D'une manière générale, je n'ai pas l'habitude du bash de Microsoft™ — je pense qu'ils ont fait de bonnes choses à leur époque, bien que leurs tentatives pour créer un système d'exploitation aient été franchement malheureuses — mais leur BFL (Batch File Language) était une plaisanterie en comparaison. Ce n'était même pas particulièrement amusant.

Comme l'écriture de scripts est une part indissociable de la compréhension de l'utilisation du shell en général, une partie de cet article est consacrée aux particularités, aux méthodes et aux spécificités du shell. Soyez patient, cela fait partie des connaissances nécessaires pour écrire de bons scripts.


2. Philosophie de l'écriture de scripts

Linux — et Unix en général — n'est pas un système tiède et confus destiné à des utilisateurs novices. Plutôt que de spécifier les manipulations et les opérations exactes qu'il faut effectuer (et ainsi vous limiter uniquement aux opérations décrites), il vous offre une myriade de petits outils qu'il est possible d'interconnecter en un nombre littéralement infini de combinaisons pour obtenir pratiquement n'importe quel résultat (je trouve que le slogan de PERL TMTOWTDI, There's More Than One Way To Do It (« il y a plus d'une manière de le faire ») s'applique particulièrement à l'ensemble d'Unix). Ce type de capacité et de souplesse induit bien évidemment un coût — une complexité accrue et une exigence de compétence supérieure pour l'utilisateur. Tout comme il y a une différence énorme entre, par exemple, faire fonctionner une bicyclette et un avion de combat supersonique, il y en a une énorme entre suivre aveuglément les règles imposées par une interface graphique utilisateur (GUI, Graphical User Interface) standardisée et créer votre propre programme, ou script shell, qui exécute exactement les fonctions dont vous avez besoin et de la manière la plus appropriée à vos besoins.

L'écriture de scripts shell est de la programmation, certes simplifiée, avec un peu, s'il y a lieu, de structure formelle. C'est un langage interprété, avec sa propre syntaxe, mais uniquement la syntaxe que vous utilisez lorsque vous invoquez des programmes depuis votre ligne de commande ; quelque chose que je qualifie de « savoir recyclable ». Ce comportement est en fait ce qui donne toute leur utilité aux scripts shell : pendant que vous les écrivez, vous continuez d'en apprendre davantage sur les spécificités de votre shell et le fonctionnement de votre système. C'est cette connaissance qui est véritablement payante à long terme comme à court terme.


3. Prérequis

Du fait que j'ai une forte prédilection pour Bash et qu'il s'avère être le shell par défaut de Linux, les scripts qui suivent ont été écrits pour ce shell (bien que j'aie essayé de réduire au maximum les spécificités de Bash. La plupart, sinon la totalité de ces scripts devrait fonctionner sous le sh bien connu). Même si vous utilisez autre chose, il n'y a aucun problème : du moment que vous avez installé Bash, ces scripts s'exécuteront correctement. Comme vous le verrez, les scripts invoquent le shell dont ils ont besoin. C'est ce que fait tout script bien écrit.

Je pars dorénavant du principe que vous allez faire tous ces exercices dans votre répertoire personnel (/home) : vous ne voulez pas que ces fichiers soient éparpillés un peu partout, là où vous ne pourrez pas les retrouver plus tard. Je considère également que vous en savez assez pour appuyer sur la touche Entrée après chaque ligne que vous saisissez et que, avant de choisir un nom pour votre script shell, vous vérifierez que vous n'avez pas un exécutable du même nom dans votre chemin (saisissez which bkup pour savoir s'il y a un exécutable appelé bkup). Vous ne devriez également pas appeler votre script test : c'est un point traité dans la FAQ Unix (« Pourquoi mon script shell/programme ne fait rien du tout ? »). Il existe un exécutable test dans /usr/bin qui ne fait rien, enfin, rien de visible, lorsqu'il est appelé...

Il va sans dire que vous devez connaître les bases de la manipulation des fichiers, copie, déplacement, etc. ainsi que les principes de bases du système de fichiers. Par exemple, . est le répertoire actuel, .. le répertoire parent (celui qui est au-dessus du répertoire actuel), ~ votre répertoire personnel (/home), etc. Si vous ne le saviez pas, cette lacune est désormais comblée !

Peu importe l'éditeur de texte que vous employez, vi, emacs, mcedit (l'éditeur de style DOS de Midnight Commander) ou tout autre, du moment que vous n'enregistrez pas votre travail dans un format propre à un traitement de texte — ce doit être du texte ordinaire. Si vous n'êtes pas sûr ou que vous continuez à avoir des « parasites » lorsque vous essayez d'exécuter votre script, vous pouvez vérifier le contenu brut du fichier que vous avez créé à l'aide de la commande :

 

cat nom_du_script

pour être sûr.

Afin d'éviter une répétition constante des éléments, je vais numéroter les lignes au fur et à mesure que nous parcourons et étudions les différentes parties d'un fichier de script. Ces numéros de lignes ne figureront évidemment pas dans le script final.


4. Construction d'un script

Commençons par les bases de la création d'un script. Ceux d'entre vous qui trouvez cela évident et simpliste sont tout de même invités à suivre. Au fur et à mesure que nous progresserons, les choses deviendrons plus complexes et un « rappel » n'est jamais vain. Le public que vise cet article est le débutant Linux, quelqu'un qui n'a jamais créé de script shell auparavant mais souhaite devenir un expert en la matière.

Dans sa forme la plus simple, un script shell n'est rien de plus qu'un raccourci — une liste de commandes que vous saisirez normalement, l'une après l'autre — pour qu'elles soient exécutées à l'invite de votre shell — plus un soupçon de « magie » pour avertir le shell qu'il s'agit effectivement d'un script.

La « magie » consiste en deux choses simples :

  1. Une indication au début du script qui spécifie le programme à utiliser pour l'exécuter et

  2. Une modification des droits d'accès sur le fichier contenant le script pour le rendre exécutable.

À titre d'exemple pratique, créons un script qui « sauvegardera » un fichier donné dans un répertoire sélectionné. Nous allons parcourir les étapes et la logique qui rendent cela possible.

Créons tout d'abord le script. Démarrez votre éditeur avec le nom de fichier que vous souhaitez créer :



mcedit bkup

La première ligne de tous fichiers de script que nous créons sera la suivante (encore une fois, ne prenez pas en compte le numéro de ligne et les deux points au début de la ligne) :


1: #!/bin/bash

Cette ligne est désignée sous le nom de shebang. La chose intéressante à ce propos est que le caractère dièse est en fait un marqueur de commentaires — tout ce qui suit un # sur une ligne est supposé être ignoré par le shell — mais le construct #! est unique à cet égard et interprété comme un préfixe au nom de l'exécutable qui va effectivement traiter les lignes qui le suivent.

Le shebang doit :

À propos, il y a un aspect subtil certes, mais important ici : quand un script s'exécute, il démarre en fait un processus bash additionnel qui s'exécute en arrière-plan de l'actuel. Ce processus exécute le script et se termine, vous renvoyant au shell d'origine qui l'a engendré. C'est pourquoi un script qui, par exemple, change de répertoire lorsqu'il s'exécute ne vous laissera pas dans le nouveau répertoire quand il se termine : le shell d'origine n'a pas été informé qu'il devait changer de répertoire et vous êtes exactement que lorsque vous avez démarré ‐ même si le changement est effectif pendant l'exécution du script.

Revenons à notre script :


2: # "bkup" - copie les fichiers spécifiés dans le répertoire ~/Backup
3: # de l'utilisateur après avoir vérifié qu'il n'y a pas de conflits de nom.

Comme je l'ai mentionné, le caractère « # » est un marqueur de commentaires. Il est judicieux, du fait que vous créerez probablement un certain nombre de shells dans le futur, d'insérer quelques commentaires dans chacun d'eux pour indiquer ce qu'il fait — ou bien, vous aurez du mal à un certain moment à vous rappeler pourquoi vous l'avez écrit. Dans des articles ultérieurs, nous explorerons des moyens un peu plus automatiques pour s'en souvenir, mais continuons.


4: cp -i $1 ~/Backup

La syntaxe -i de la commande cp la rend interactive. Cela signifie que si nous exécutons bkup file.txt et qu'un fichier appelé file.txt existe déjà dans le répertoire ~/Backup, cp vous demandera si vous souhaitez l'écraser et annulera l'opération si vous appuyez sur une autre touche que y.

$1 est un « paramètre positionnel » qui désigne le premier paramètre que vous saisissez après le nom du script. En fait, il y a une liste complète de ces variables :


$0 - Le nom du script en cours d'exécution - dans ce cas, "bkup".
$1 - Le premier paramètre - dans ce cas, "file.txt" ; n'importe quel paramètre
peut être désigné par $<numéro> de cette manière.
#@ - La liste complète des paramètres - "$1 $2 $3..."
$# - Le nombre de paramètres.

Il y a plusieurs autres moyens permettant d'accéder aux paramètres positionnels et de les manipuler (voir la page de man de Bash) — mais cela suffira pour le moment.


5. Amélioration du script

Jusqu'ici, notre script ne fait pas grand chose : il ne valait pas vraiment la peine de se fatiguer n'est-ce pas ? Bien, rendons-le un peu plus utile. Qu'en est-il si vous voulez à la fois garder le fichier dans le répertoire ~/Backup et enregistrer le nouveau — peut-être en lui ajoutant une extension pour indiquer la « version » ? Essayons cela ; nous nous contenterons d'ajouter une ligne et de modifier la dernière comme suit :


4: a=$ (date +'%Y%m%d%H%M%S')
5: cp -i $1 ~/Backup/ $1.$a

Ici, nous commençons à voir un aperçu de la véritable puissance des scripts shell : la capacité à utiliser les résultats des autres outils Linux, appelée « substitution de commande ». L'effet du construct $(commande) est d'exécuter la commande à l'intérieur des parenthèses et de remplacer la chaîne $(commande) entière par le résultat. Dans ce cas, nous avons demandé à date d'afficher la date et l'heure actuelles, avec une précision à la seconde et de passer le résultat à une variable appelée a ; puis, nous avons ajouté cette variable à la fin du nom de fichier à enregistrer dans ~/Backup. Notez que quand nous affectons une valeur à une variable, nous utilisons son nom ( a=xxx ), mais quand nous souhaitons utiliser cette valeur, nous devons ajouter un $ au début de ce nom ($a). Les noms des variables n'ont pas d'importance, excepté les mots réservés du shell, c'est-à-dire :


case do done elif else esac fi for function if in select then until while time

et ne peuvent pas contenir de méta-caractères ou caractères réservés comme :


! { } | & * ; ( ) < > espace tabulation

Il ne devront également pas correspondre au nom d'une variable standard du système, comme :


PATH PS1 PWD RANDOM SECONDS 

Note

Reportez-vous à man bash pour en obtenir la liste complète.

L'effet des deux dernières lignes de ce script est de créer un nom de fichier unique —  quelque chose comme file.txt.20000117221714 — qui ne devrait pas entrer en conflit avec quoi que ce soit d'autre dans ~/Backup. Notez que j'ai laissé le commutateur -i par mesure de « précaution » : si, pour une raison obscure, deux noms de fichiers entrent en conflit, cp vous donne une ultime chance d'interrompre l'opération. Dans le cas contraire, il ne fera aucune différence.

À propos, l'ancienne version du construct $(commande) — la `commande` (notez qu'on emploie des apostrophes inversés plutôt que des apostrophes) — est plus ou moins obsolète. La syntaxe $() peut être facilement imbriquée, comme dans $(cat $($2$(basename file1 txt))) par exemple. On ne peut pas le faire avec les apostrophes inversés, du fait que la seconde apostrophe inverse «fermerait» la première, ce qui ferait échouer la commande ou produirait un effet indésirable. Vous pouvez encore les utiliser cependant — dans des substitutions simples, non imbriquées (le cas le plus courant) ou comme la paire la plus intérieure ou extérieure d'une imbrication — mais si vous utilisez la nouvelle méthode exclusivement, vous éviterez toujours cette erreur.

Regardons à présent ce que nous avons jusqu'ici, une fois le caractère d'espacement ajouté pour plus de lisibilité et les numéros de lignes supprimés (c'est un vrai script !) :


#!/bin/bash

# "bkup" - copie les fichiers spécifiés dans le répertoire ~/Backup
# de l'utilisateur après avoir vérifié qu'il n'y a pas de conflits de nom.

a=$(date +'%Y%m%d%H%M%S')
cp -i $1 ~/Backup/$1.$a

Oui, ce n'est qu'un script de deux lignes, mais qui commence à devenir utile. La dernière chose que nous devons faire est pour le transformer en programme exécutable — bien que nous puissions déjà le lancer avec bash bkup — est de changer son mode :


chmod +x bkup

Il y a un dernier point : une autre FAQ Unix. Si vous essayez d'exécuter votre nouveau script en saisissant bkup à l'invite, vous obtenez ce message de reproche familier :


bash: bkup: command not found

Que s'est-il donc passé ?

À la différence de DOS, l'exécution de commandes et de scripts dans le répertoire actuel est désactivée par défaut, par mesure de sécurité. Imaginez ce qui arriverait si quelqu'un créait un script appelé ls contenant rm -rf * (« tout supprimer ») dans votre répertoire personnel et que vous saisissiez ls ! Si le répertoire actuel (.) arrivait avant /bin dans votre variable PATH, vous seriez en fait très ennuyé...

Pour cette raison et à cause d'un certain nombre d'exploits possibles, vous devez spécifier le chemin d'accès à tous les exécutables que vous souhaitez lancer ici — une sage restriction. Vous pouvez également déplacer votre script dans un répertoire qui est dans votre /path une fois qu'il est finalisé : /usr/local/bin est un bon candidat pour ce faire (saisissez echo $PATH pour savoir quels sont les répertoires qu'il contient).

En attendant, pour l'exécuter, saisissez simplement :


./bkup fichier.txt

Le ./ signifie juste que le fichier à exécuter se trouve dans le répertoire actuel. Utilisez ~/ à la place si vous le lancez depuis un autre répertoire. Il s'agit ici d'indiquer un chemin d'accès complet à l'exécutable, du fait qu'il n'est pas dans un des répertoires de votre variable PATH.

Ceci suppose bien sûr que vous ayez dans votre répertoire actuel un fichier appelé fichier.txt et que vous ayez créé un sous-répertoire appelé Backup dans votre répertoire personnel (/home). Dans le cas contraire, vous obtiendrez une erreur. Nous continuerons à expérimenter ce script dans le prochain numéro.


6. Résumé

Dans cet article, nous avons étudié quelques notions de base relatives à la création d'un script shell, ainsi que certaines spécificités :


7. Récapitulation

Voilà déjà un certain nombres d'informations pour un début. Faites vos propres expérimentations ! L'écriture de scripts shell est une grande partie de l'attrait et de la puissance de Linux. Le mois prochain, nous aborderons la vérification des erreurs — ce que votre script devrait faire si la personne qui l'utilise fait une erreur de syntaxe, par exemple — ainsi que les boucles et l'exécution conditionnelle. Peut-être traiterons-nous de quelques-uns des « super outils » couramment utilisés dans les scripts shell.

N'hésitez pas à m'envoyer vos suggestions concernant toute correction ou amélioration, ainsi que vos tuyaux préférés sur l'écriture de scripts shell ou toute astuce réellement intéressante que vous avez découverte. Comme toute personne dont l'ego n'a pas affecté le bon sens, je me considère comme un étudiant, toujours prêt à apprendre quelque chose de nouveau. Si jamais j'utilise une de vos contributions, je vous citerai.

À bientôt — Bon Linux !


8. Références

Les pages de man de bash, cp et chmod.

Ben est le rédacteur en chef de la Linux Gazette© et il est membre de l'« Answer Gang© ».

Ben est né à Moscou en Russie en 1962. Il a commencé à s'intéresser à l'électricité dès l'age de 6 ans en enfonçant une fourchette dans une prise et déclenchant ainsi un incendie, depuis il n'a jamais cessé de s'intéresser à la technologie. Il travaille avec les ordinateurs depuis les Temps Anciens où l'on devait souder soi-même des composants sur des cartes à circuits imprimés et où les programmes devaient tenir dans 4 ko de mémoire. Il serait heureux de payer tout psychologue capable de le guérir des cauchemars récurrents qu'il a gardés de cette époque.

Ses expériences suivantes comprennent la création de programmes dans pratiquement une douzaine de langages, la maintenance de réseaux et de bases de données pendant l'approche d'un ouragan et l'écriture d'articles pour des publications allant des magazines de voile aux journaux technologiques. Après une croisière de 7 ans dans l'Atlantique et les Caraïbes ainsi que des passages sur la côte Est des États-Unis, il a désormais jeté l'ancre à St-Augustine, en Floride. Instructeur technique chez Sun Microsystems©, il travaille également à titre privé comme consultant open source et développeur web. Ses passe-temps actuels sont notamment l'aviation, le yoga, les arts martiaux, la moto, l'écriture et l'histoire romaine. Son Palm Pilot© est truffé d'alarmes dont la plupart contiennent des points d'exclamation.

Il travaille avec Linux depuis 1997 et lui doit d'avoir perdu tout intérêt sur les retombées d'une guerre nucléaire dans le nord-est du Pacifique.