Introduction to Shell Scripting

Gazette Linux n°53 — Mai 2000

Thierry Hamon

Adaptation française 

Frédéric Marchal

Correction du DocBook 

Article paru dans le n°53 de la Gazette Linux de mai 2000.

Cet article est publié selon les termes de la 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

Conventions
Structures de contrôles
FOR;DO;DONE
WHILE;DO;DONE
UNTIL;DO;DONE
IF;THEN;[ELSE];FI
CASE;IN;;ESAC
BREAK et CONTINUE
Retour vers le futur
Vérification des erreurs
Pour conclure
« Script/citation » du mois
Bibliographie

Le mois dernier, nous nous sommes intéressés aux bases de l'écriture de scripts shell ainsi qu'à quelques mécanismes sous-jacents qui font que tout ça fonctionne. Cette fois-ci, nous allons voir comment les boucles et d'autres structures de contrôle nous permettent de gérer l'exécution de programmes dans des scripts, mais aussi d'adopter quelques bonnes habitudes lors de l'écriture de scripts shell.

Conventions

La seule chose à noter dans cet article est l'utilisation des points de suspension (...). Je les utilise pour indiquer que le code présenté n'est qu'un fragment, et non un script entier. Si ça peut vous aider, considérez les comme des lignes de code qui n'ont pas été écrites.

Structures de contrôles

FOR;DO;DONE

Souvent, les scripts sont écrits pour automatiser des tâches répétitives. Un exemple pris au hasard : si vous deviez éditer, à la suite, une série de fichiers dans un répertoire particulier, vous utiliseriez un script qui ressemble à ça :

#!/bin/bash

for n in ~/hebdomadaire/*.txt
do
   ae $n
done
echo "Terminé."

ou comme ça :

#!/bin/bash

for n in ~/hebdomadaire/*.txt; do ae $n;done; echo "Terminé."

Dans les deux cas, le code fait exactement la même chose. Mais la première version est beaucoup plus lisible, surtout si vous écrivez de gros scripts avec plusieurs niveaux. Un conseil pour l'écriture des scripts, indentez chaque niveau (les commandes à l'intérieur des boucles). Il sera plus facile de corriger les erreurs et de suivre votre code.

La structure de contrôle ci-dessus est appelée une boucle for (boucle « Pour tout »). Elle évalue le test pour tous les éléments restant dans une liste, c'est-à-dire Est-ce qu'il reste encore des fichiers, après ceux que nous avons déjà lus, et qui instancient l'expression ˜/hebdomadaire/*.txt ?). Si le résultat du test est vrai, le nom de l'élément courant dans la liste est affecté à la variable de la boucle (n dans notre exemple), le corps de la boucle est exécuté (la partie entre do et done), et le test à nouveau est évalué. Si le parcourt de la liste est terminé, for s'arrête de boucler et passe le contrôle à la ligne qui suit le mot clé done. Dans notre exemple, la commande echo.

Un petit truc que j'aimerai mentionner ici. Si vous voulez que votre boucle for tourne un certain nombre de fois, la syntaxe shell ressemblera à ça :

#!/bin/bash

for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
do
  echo $i
done

Quelle horreur ! Si vous voulez faire, disons, 250 itérations, vous devriez toutes les écrire ! Heureusement, il y a un raccourci, la commande seq, qui affiche une liste de nombre de 1 au nombre maximum donné en argument. Par exemple :

#!/bin/bash

for i in $(seq 15)
do
  echo $i
done

Sémantiquement, ce script est identique au précédent. seq fait partie du paquetage GNU shellutils, qui est probablement déjà installé sur votre système. Il y a aussi une option qui permet de faire une sorte de boucle while, mais c'est un peu plus difficile à utiliser.

WHILE;DO;DONE

Souvent, vous avez besoin d'un mécanisme de contrôle qui agit en fonction d'une condition spécifiée, plutôt que suivant les éléments d'une liste. La boucle while remplit ce rôle :

#!/bin/bash

pppd call provider &

while [ -n "$(ping -c 1 192.168.0.1|grep 100%)" ]
do
  echo "En cours de connexion"
done

echo "Connexion établie."

Le déroulement de ce script est le suivant : on invoque pppd le pingouin PPP... je veux dire le démon PPP :), puis en bouclant jusqu'à ce qu'une connexion soit établie (si vous voulez utiliser ce script, remplacer 192.168.0.1 par l'adresse IP de votre FAI). Voici maintenant les détails :

  1. La commande ping -c 1 xxx.xxx.xxx.xxx envoie un seul ping à l'adresse donnée en argument. Notez qu'il faut que ce soit une adresse IP et non une URL (le ping échouera immédiatement à cause de l'absence de DNS). S'il n'y a pas de réponse dans les 10 secondes qui suivent, quelque chose comme ça s'affichera :

    PING xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx): 56 data bytes
    ping: sendto: Network is unreachable
    ping: wrote xxx.xxx.xxx.xxx 64 chars, ret=-1
    
    
    --- xxx.xxx.xxx.xxx ping statistics ---
    1 packets transmitted, 0 packets received, 100% packet loss
    
  2. La seule ligne qui nous intéresse est celle qui nous donne le pourcentage de paquets perdus. Avec un seul paquet, il ne peut être que de 0% (la commande ping a été exécutée avec succès) ou 100%. En redirigeant la sortie de la commande ping avec un tube vers l'entrée de la commande grep 100%, nous mettons en évidence cette ligne, dans le cas où la perte est en fait de 100%. Si aucun paquet n'est perdu (0%), la sortie reste vide. Notez que la chaîne 100% n'est en rien spéciale : nous aurions pu utiliser ret=-1, unreacheable ou tout autre chose qui n'est affichée qu'en cas d'absence de réponse de l'adresse.

  3. Les crochets qui contiennent les commandes précédentes sont synonymes de la commande test. Celle-ci retourne 0 ou 1 (vrai ou faux) suivant l'évaluation de l'intérieur des crochets. L'opérateur -n retourne « vrai » si la longueur de la chaîne est plus grande que 0. Puisque nous supposons que la chaîne est contiguë (sans espace), et que la ligne que nous vérifions ne l'est pas, nous devons placer des doubles cotes (") autour du résultat de la commande (c'est une technique que vous utiliserez très souvent lorsque vous écrirez des scripts). Notez que les crochets doivent être entourés d'espaces, c'est-à-dire que [-n $STRING] ne marchera pas, [ -n $STRING ] est correct. Pour plus d'information sur les utilisations de test, tapez la commande help test. Vous verrez qu'un grand nombre d'opérateurs très utiles sont disponibles.

  4. Tant que le test ci-dessus retourne la valeur « vrai », c'est-à-dire aussi longtemps que la commande ping échouera, la boucle while continuera de s'exécuter, en affichant toute les dix secondes, le message "Connecting.... Aussitôt qu'une commande ping s'exécute avec succès, c'est-à-dire que le test retourne la valeur « faux », on sortira de la boucle while et l'instruction qui suit done prendra le contrôle.

UNTIL;DO;DONE

La boucle until est l'inverse du while. Elle continue de boucler tant que le test est faux, et s'arrête lorsqu'il devient vrai. Je n'ai jamais eu l'occasion de l'utiliser. La boucle while et les possibilités des tests disponibles étaient suffisants pour ce que j'ai fait jusqu'à maintenant.

IF;THEN;[ELSE];FI

A plusieurs reprises, nous aurons besoin de vérifier la valeur d'une condition et de contrôler l'exécution du script suivant le résultat. Pour cela, nous avons la structure if :

...

if [ $PATON="pauvre type" ]
then
    echo 'Pendre le boulot et bouscule le !'
else
    echo 'Reste dans les parages; ça peut valoir le coup.'
fi

...

Je vous accorde que ce n'est pas si facile... mais le sens logique est là. Quoiqu'il en soit, si une variable appelée PATRON a la valeur « pauvre type » (ATTENTION, pour les programmeurs en C  : = et == sont équivalents dans les tests, aucune affectation n'est réalisée), alors la première instruction echo sera exécutée. Dans tous les autres cas, ce sera la seconde instruction echo (si la variable PATRON a la valeur « idiot », l'exécution se déroulera aussi de cette manière. Désolé ;). Notez que la partie else est optionnelle, comme dans ce fragment de script :

...

if [ -n $ERROR ]
then
    echo 'Detection d'une erreur. Sortie.'
    exit
fi

...

Cette procédure sort bien entendu si la variable ERROR n'est pas vide, sinon l'exécution du programme n'en sera pas affectée.

CASE;IN;;ESAC

La dernière instruction que vous pouvez utiliser pour des redirections conditionnelles est celle correspondant à un if multiple, basée sur l'évaluation d'un test. Si, par exemple, nous savons que les seules valeurs possibles renvoyées par un programme imaginaire appelé intel_cpu_test sont 4, 8, 16, 32 ou 64, alors nous pouvons écrire le script suivant :

#!/bin/bash

case $(intel_cpu_test) in
    4) echo "vous faites tourner Linux sur une calculatrice ?";;
    8) echo "Ce 8088 a passé l'âge de la retraite ...";;
    16) echo "Vous êtes un adepte du 286, vous ?";;
    32) echo "Un de ceux qui ont les derniers gadgets à la mode !";;
    64) echo "Oooh... vraiment jaloux de ce CPU !";;
    *) echo "Mais quelle bête avez-vous ?";;
esac

(Avant de m'inonder de mail au sujet de l'utilisation de Linux sur un 286 ou 8088... Vous ne pouvez pas non plus le faire tourner sur une calculatrice. :)

Evidemment, le caractère « * » à la fin permet de prendre en compte toutes les autres valeurs : Si quelqu'un du Laboratoire Secret de Intel lance le script sur leur nouveau CPU (nom de code « Ultra-Maga-Super-du-Tonnerre »), nous voulons que le script soit capable de renvoyer une réponse plutôt que de planter. Notez les doubles point-virgules, elles terminent chaque couple « valeur/instruction » et sont (pour certaines raisons) une source d'erreur courante lors de l'utilisation de l'instruction case/esac. Faites très attention lorsque vous écrivez vos scripts.

BREAK et CONTINUE

Ces instructions interrompent l'exécution d'un programme dans des cas spécifiques. Le break, une fois exécuté, interrompt immédiatement l'exécution de la boucle dans laquelle il se trouve. L'instruction continue ignore l'itération courante de la boucle. Ces instructions peuvent être utiles dans de nombreuses situations, en particulier dans les boucles importantes où l'existence d'une condition donné rend les tests suivants inutiles. Voici un assez long exemple (mais, je l'espère, compréhensible) :

...

while [ faire_la_fete ]
do
  case $ETAT_NOURRITURE
  in
    plus_de_chips) reapprovisionnement_en_chips;;
    cacahuetes_terminees) remplir_le_bol_de_cacahuetes;;
    plus_de_bretzels) ouvrir_un_nouveau_paquet_de_bretzels;;
    ...
    ...
  esac

  if [ police_sur_place ]
  then
    parler_aux_gentils_policiers
    continue
  fi

  case $ETAT_ALCOOL
  in
    plus_de_vodka) ouvrir_une_nouvelle_bouteille_de_vodka;;
    plus_de_rhum_gone) ouvrir_une_nouvelle_bouteille_de_rhum;;
    ...
    ...
  esac

  case $ANALYZE_DU_COMPORTEMENT_DES_INVITES
  in
    abat_jour_sur_la_tete)    echo "Il est en train de boire";;
    parle_aux_plantes)	      echo "Elle est en train de fumer";;
    parle_aux_martiens)	      echo "Ils prennent du LSD";;
    objets_en_levitation)     echo "Qui a pris mon jus de fruit ??";;

  ...


  ...


  ...


  esac

done

echo "He mec ? ... quel jour sommes nous ?"

Deux points importants : notez que, dans la vérification du statut des différents amuse-gueules de la soirée, il est préférable de l'écrire ainsi plutôt que d'utiliser plusieurs instruction if, les chips et les bretzels peuvent venir à manquer en même temps, c'est-à-dire qu'ils ne sont pas mutuellement exclusifs. Tel que c'est écrit, les chips sont les plus prioritaires. Si deux types des gâteaux-apéritifs viennent à manquer simultanément, cela prendra deux itérations de la boucle pour tous remplacer.

Pour pouvez continuer à vérifier que le statut de la nourriture pendant que vous essayez de convaincre la police que vous êtes en fait en train de tenir une réunion de collectionneurs de timbres (en fait, maintenir l'approvisionnement en gâteaux apéritifs est un facteur crucial dans ce cas), mais nous devons ignorer le statut des alcools — au fait, nous avons descendu Joe du lustre...

L'instruction continue ignore la dernière partie de la boucle while aussi longtemps que la fonction police_sur_place retourne la valeur vraie. La boucle est tronquée à cet endroit. Notez que même si l'instruction est à l'intérieur de la structure if, cela affecte la boucle qui l'entoure. continue et break s'appliquent uniquement aux boucles, c'est-à-dire les structures de contrôle for, while et until.

Retour vers le futur

Voici le script que nous avions écrit le mois dernier :

#!/bin/bash
# "bkup" - copie les fichiers specifies dans le repertoire  ~/Backup
# de l'utilisateur apres avoir verifie les conflits de noms
a=$(date +%T-%d_%m_%Y)
cp -i $1 ~/Backup/$1.$a

C'est assez intéressant. Peu de temps après avoir terminer l'article du mois dernier, j'ai écrit un bout de code en C sur une machine où rcs (Revision Control System — le Système de contrôle des versions GNU) n'était pas installé. Ce script me fut très utile comme « micro-rcs ». Je l'ai utilisé pour conserver des versions du projet. C'est simple et des scripts de ce type peuvent être utiles lorsqu'on s'y attend le moins.

Vérification des erreurs

Le script ci-dessus est utilisable, pour vous comme pour n'importe qui est capable de le lire et de le comprendre. Regardons le quand même : ce que nous voulons d'un programme ou d'un script, c'est qu'il s'exécute lorsqu'on tape son nom, non ? C'est tout, ou alors qu'il nous dise pourquoi il ne fonctionne pas. Dans ce cas, nous avons ce message crypté :

cp: missing destination file Try `cp --help' for more information.
(Fichier destination absent. Essayez `cp --help' pour plus d'information)

Que ce soit pour n'importe qui, ou pour nous même, lorsque nous oublions exactement comment utiliser ce script extrêmement complexe avec innombrable options :), nous avons besoin d'effectuer une vérification des erreurs, et surtout, d'avoir des informations sur son utilisation et sa syntaxe. Voyons comment nous pouvons appliquer ce que nous venons juste d'apprendre :

#!/bin/bash

if [ -z $1 ]
then
    clear
    echo "'bkup' - copie les fichiers specifies dans le repertoire"
    echo "~/Backup de l'utilisateur apres avoir verifie les conflits"
    echo "de noms" 
    echo
    echo "Usage: bkup nom_du_fichier"
    echo
    exit
fi

a=$(date +%T-%d_%m_%Y)
cp -i $1 ~/Backup/$1.$a

L'opérateur -z de test retourne la valeur « 0 » (vrai) lorsque la chaîne en argument est de longueur nulle. De cette manière, nous vérifions si bkup a été exécuté sans nom de fichier. A mon avis, la meilleure place pour afficher l'aide et les informations sur l'utilisation du script est au tout début. Si vous oubliez quelles sont les options, vous avez juste à exécuter le script sans rien et vous pourrez ainsi vous rafraîchir la mémoire. Vous n'avez même plus besoin de mettre les commentaires du début, maintenant. Notez que vous avez les simplement inclus dans les informations concernant l'utilisation du script. C'est quand même une bonne idée de mettre des commentaires pour expliquer les points qui ne sont pas évidents. C'est ce truc qui vous évitera de vous prendre la tête sur ce script l'année prochaine, on ne sait jamais...

Avant de vous amuser avec ce script, voici quelques possibilités supplémentaires. Que se passe-t-il si vous voulez avoir la possibilité de copier différents types de fichiers dans différents répertoires ? Utilisons ce que nous avons appris :

#!/bin/bash

if [ -z $1 ]
then
    clear
    echo "'bkup' - copie les fichiers specifies dans le repertoire"
    echo "~/Backup de l'utilisateur apres avoir verifie les conflits"
    echo "de noms" 
    echo
    echo "Usage: bkup nom_du_fichier [rep_sauvegarde]"
    echo
    echo "bkup_dir Sous-repertoire optionel dans '~/Backup' ou le"
    echo "fichier sera sauvegarde."
    echo
    exit
fi

if [ -n $2 ]
then
    if [ -d ~/Backup/$2 ]
    then
        subdir=$2/
    else
        mkdir -p ~/Backup/$2
        subdir=$2/
    fi
fi

a=$(date +%T-%d_%m_%Y)
cp -i $1 ~/Backup/$subdir$1.$a

Voici un résumé des modifications :

  1. Dans la section contenant le message d'aide, on peut lire maintenant «  ... l'arborescence des répertoires » plutôt que juste « répertoire ». Ceci indique les changements que nous avons fait.

  2. La ligne « Usage : » a été modifiée pour indiquer l'argument optionnel (comme le montre les crochets). Nous avons ajouté une explication sur l'utilisation de cet argument, puisqu'elle pourrait ne pas être évidente pour tout le monde.

  3. Une structure de contrôle if a été ajoutée afin de vérifier si $2 (le deuxième argument de la commande bkup) existe. Si c'est le cas, on vérifie que le répertoire dont le nom est donné en second argument existe dans l'arborescence de ˜/Backup, et le crée s'il n'existe pas (l'opérateur -d permet de tester que le fichier existe et qu'il s'agit d'un répertoire).

  4. Une variable subdir est maintenant insérée dans la commande cp, entre Backup/ et $1.

    Vous pouvez désormais taper ce genre de chose :

    bkup my_new_program.c c
    bkup filter.awk awk
    bkup filter.awk filters
    bkup Letter_to_Mom.txt docs
    

    etc. Vous pouvez ainsi tout ranger dans la catégorie que vous voulez. De plus, l'ancien comportement de bkup est toujours disponible :

    bkup file.xyz
    

copiera une sauvegarde de file.xyz dans le répertoire ˜/Backup. C'est utile pour les fichiers que vous ne savez pas où ranger.

Au fait, pourquoi avons nous ajouté un / à $2 dans le test if qui précède la ligne cp ? Hé bien, si $2 n'existe pas, vous voulez que bkup se comporte comme avant, c'est-à-dire qu'il copie le fichier dans le répertoire ˜/Backup. Si vous écrivez quelque chose comme ça :

cp -i $1 ~/Backup/$subdir/$1.$a

(notez le / supplémentaire entre $subdir et $1), et que $2 n'est pas spécifié, alors $subdir est vide et la ligne ci-dessus devient :

cp -i $1 ~/Backup//$1.$a

Notez que ce qui n'est pas un résultat particulièrement souhaitable, étant donné que nous voulez rester, si possible, fidèles aux habitudes d'écriture en shell.

En fait, c'est vraiment une bonne idée de considérer toutes les possibilités, lorsque vous placez des variables dans une chaîne. Une erreur classique de ce type ressemble au script suivant :

NE PAS UTILISER CE SCRIPT !
#!/bin/bash
# Ecrit par Larry, Moe, and Shemp - the Deleshun PoWeR TeaM!!!
# Verifie par Curly: "Pourquoi, Bien sur qu'il marche ! Yek-Yek-Yek !"
# Tout ce que vous avez a faire est d'entrer le nom de ce programme
# suivi de ce que vous voulez effacer : repertoire, fichiers normaux,
# plusieurs fichier, tout est bon !

rm -rf $1*

NE PAS UTILISER CE SCRIPT !

(Au moins, ils vous aurons prévenus :)

Que se passe-t-il si quelqu'un lance le programme ci-dessus sans argument ? La commande devient alors  :

rm -rf *

En supposant que vous êtes l'utilisateur Dupond dans votre répertoire de travail, le résultat est plutôt horrible : tous vos fichiers personnels seront détruits. Ça devient une catastrophe si vous vous trouvez en super-utilisateur à la racine : le système est complètement anéanti !

Les virus semblent vraiment gentils, des choses vraiment inoffensives ...

Faites attention lorsque vous écrivez vos scripts. Comme vous l'avez vu, vous avez la possibilité de détruire complètement votre système en un éclair.

 

Unix was never designed to keep people from doing stupid things, because that policy would also keep them from doing clever things. (Unix n'a jamais été conçu pour empêcher les gens de faire des choses stupides, parce que ça les empêcherait aussi de faire des choses intelligentes)

 
 -- Doug Gwyn
 

Unix gives you just enough rope to hang yourself — and then a couple more feet, just to be sure. (Unix vous donne juste assez de corde pour vous pendre vous même — seulement quelques mètres, pour être sûr)

 
 -- Eric Allman

La philosophie prend tout son sens : puissance illimitée dans les outils, restrictions grâce aux permissions. Mais elle impose une responsabilité : vous devez faire attention. Comme corollaire, lorsque vous vous connectez en root, ne lancez pas des scripts si vous n'êtes pas sûr qu'ils sont inoffensifs (notez la hypothèse très importante dans cette phrase : vous pouvez prouver qu'ils sont inoffensifs).

Pour conclure

Les structures de contrôle (boucles et condition) sont très importantes dans la plupart des scripts. Lorsque nous analyserons d'autres scripts shell, dans les futurs articles, vous verrez une myriade de façon de les utiliser (un script même de complexité moyenne ne peut exister sans ces structures).

Le mois prochain, nous jetterons un coup d'œil sur quelques outils qui sont couramment utilisés dans les scripts shell (des outils qui vous sont peut-être familiers, lorsque vous les utilisez en ligne de commandes). Nous explorerons aussi comment ils peuvent être connectés entre eux pour produire les résultats voulus. Nous disséquerons deux scripts (les miens, à moins que quelqu'un d'autre soit suffisamment courageux pour envoyer le résultat de ses concoctions au clavier). Ayez Peur, Ayez Très Peur :)

Tous commentaires et corrections sur cette série d'articles sont les bienvenus, ainsi que n'importe quel script intéressant que vous pourriez m'envoyer. Toutes les attaques/critiques seront envoyées à /dev/null (Oh, non, c'est déjà plein...)

Au mois prochain,

Linuxement !

« Script/citation » du mois

 

What's this script do?

'unzip; touch; finger; mount; gasp; yes; umount; sleep'

Hint for the answer: not everything is computer-oriented. Sometimes you're in a sleeping bag, camping out with your girlfriend.

Que fait ce script ?

'unzip; touch; finger; mount; gasp; yes; umount; sleep'

Un indice : Il ne faut pas toujours penser qu'à l'informatique. Quelquefois, vous êtes dans un sac de couchage, en camping avec votre petite amie"

 
 -- Frans van der Zande

Bibliographie

Les pages de manuel de 'bash', 'seq', 'ping', 'grep'

La commande "help" pour 'for', 'while', 'until', 'if', 'case', 'test', 'break', 'continue'

"Introduction to Shell Scripting - The Basics" par Ben Okopnik (Introduction à l'écriture de scripts shell, les bases, Linux Gazette #52

Adaptation française de la Gazette Linux

L'adaptation française de ce document a été réalisée dans le cadre du Projet de traduction de la Gazette Linux.

Vous pourrez lire d'autres articles traduits et en apprendre plus sur ce projet en visitant notre site : http://www.traduc.org/Gazette_Linux.

Si vous souhaitez apporter votre contribution, n'hésitez pas à nous rejoindre, nous serons heureux de vous accueillir.