GAZETTE N°25: Bourne/Bash : Introduction à la programmation en shell

Introduction à la programmation en shell

Par Rick Dearman

Adaptation française : Éric Jacoboni


Précédent Suivant Table des matières

1. Bourne/Bash : Introduction à la programmation en shell

Tôt ou tard, tout utilisateur UNIX aura besoin d'un script de shell (« shell script », en anglais). On peut simplement vouloir rendre une tâche répétitive plus facile, ou donner un coup de fouet à un programme existant. L'utilisation d'un shell script rend ce genre de choses plus faciles. L'un des premiers shell scripts que j'ai écrit consistait à traiter un répertoire plein de fichiers dont les noms étaient en majuscules afin de les transformer en minuscules :

1 #!/bin/sh
2
3 DIR=$1
4
5 for a in `ls $DIR`
6 do
7 fname=`echo $a | tr A-Z a-z`
8 mv $DIR/$a $DIR/$fname
9 done;
10 exit 0
11 # Ce script affichera une erreur si le nom du fichier est deja 
12 # en minuscules et suppose que son paramètre est un répertoire

La première ligne indique au système le shell à utiliser, ici il s'agit de « sh », le Bourne shell (cela peut être un lien vers le shell bash). La combinaison des deux symboles #! est spéciale et indique à Linux quel shell exécutera les commandes contenues dans le fichier abritant le script. Elle n'est donc pas ignorée comme les autres lignes de commentaires. La ligne 3 initialise une variable nommée DIR avec la valeur du premier paramètre d'entrée (les paramètres débutent par $0, qui est le nom du script shell, ici : LCem.sh.

À la ligne 5, on entre dans une boucle. Ici, il s'agit d'un for. Traduit en français, cela signifie que je veux faire quelque chose pour toute entrée « a » obtenue par la commande « ls $DIR ». Le shell remplacera le nom de la variable $DIR par ce qui a été entré sur la ligne de commande. La ligne 6 commence la boucle.

À la ligne 7, on utilise deux commandes UNIX, echo et tr. Nous produisons ainsi la valeur courante de $a et nous l'envoyons à tr (qui signifie translate). Dans notre cas, on traduit les minuscules en majuscules et on affecte le résultat à une variable nommée fname.

La ligne 8 renomme le fichier $DIR/$a, quel qu'il soit, en $DIR/$fname. La ligne 9 indique au shell qu'il doit recommencer pour traiter toutes les autres variables $a. Enfin, la ligne 10 termine le script avec un code erreur de 0. Les lignes 11 et 12 sont des commentaires.

Ce script n'aurait pas été nécessaire pour changer les noms d'un ou de deux fichiers, mais puisque je voulais en modifier une centaine, il m'a évité beaucoup de saisie. Afin de pouvoir l'exécuter sur votre machine, vous devez le rendre exécutable avec chmod +x LCem.sh ou vous pouvez invoquer directement le shell en lui donnant le nom de votre script avec sh LCem.sh. L'utilisation de la combinaison #! indiquera au noyau quel shell doit être invoqué, c'est la façon de faire la plus commune. Rappelez vous que si vous utilisez #!, le fichier doit avoir les droits d'exécution.

Il y a seulement douze lignes mais elles nous en montrent beaucoup sur les scripts. On a appris comment dire au système de lancer le script en utilisant la combinaison #!. Cette combinaison d'une marque de commentaire et d'un point d'exclamation est utilisée pour lancer un script sans avoir à invoquer d'abord le shell. On a appris qu'un # permet d'écrire un commentaire qui sera ignoré lorsque le script sera traité. On a appris comment passer des paramètres au script afin de récupérer la saisie de l'utilisateur et on sait comment initialiser une variable. On a eu un aperçu de l'une des nombreuses structures de contrôle utilisables pour contrôler les fonctionnalités d'un script.

Ne vous affolez pas si vous ne maîtrisez pas encore tout cela. Nous allons maintenant expliquer certaines des structures de contrôle permettant de prendre une décision. La première est l'instruction « if ». Dans tout langage de programmation, on veut pouvoir changer le déroulement du programme en fonction de diverses conditions. Par exemple, si un fichier est dans ce répertoire, on fait une chose, s'il n'y est pas, on fait autre chose. La syntaxe de la commande « if » est la suivante :

if expression then
    commandes
fi

Si expression est vraie, les instructions du bloc if sont exécutées. Voici un exemple d'utilisation de l'instruction if.

WhoMe.sh

1    #!/bin/sh
2
3    # initialise la variable ME avec le premier paramètre suivant la commande
4    ME=$1
5
6    # recherche de $ME dans le fichier passwd (sans affichage)
7    if  grep $ME /etc/passwd > /dev/null
8    then
9    # si $ME est dans le fichier, afficher un message
10        echo "Vous êtes un utilisateur"
11   fi

Notez l'utilisation des commentaires aux lignes 3, 6 et 9. Commentez bien tous vos scripts car quelqu'un pourrait avoir besoin de les étudier plus tard. Dans six mois, vous pouvez ne pas vous rappeler ce que avez fait et avoir vous-même besoin de ces commentaires.

En utilisant l'instruction if, on peut maintenant corriger certaines des erreurs du script de mise en minuscules. Dans LCem.sh, le script se plantait si l'utilisateur n'entrait pas un répertoire comme paramètre. Pour vérifier la présence d'une chaîne vide, on peut utiliser la forme suivante :

if [ ! $1 ]

qui signifie « si $1 n'existe pas ».

La nouveauté est l'utilisation du point d'exclamation en tant qu'opérateur logique NOT. Utilison cela dans notre programme.

#!/bin/sh

1    if [ ! $1 ]
2    then
3    echo "Usage: `basename $0` nom_repertoire"
4     exit 1
5    fi
6
7    DIR=$1
8
9    for a in `ls $DIR`
10   do
11     fname=`echo $a | tr A-Z a-z`
12     mv $DIR/$a $DIR/$fname
13   done;

Désormais, si l'utilisateur tape la commande sans le répertoire, le script se terminera en produisant un message sur la façon adéquate de l'invoquer, suivi d'un code erreur de 1.

Que se passe-t-il si l'on ne veut changer que le nom d'un seul fichier ? On a déjà cette commande et ce serait bien si elle pouvait s'en charger. Si l'on veut faire cela, il faut pouvoir tester si le paramètre est un fichier ou un répertoire. Voici une liste des opérateurs de test portant sur les fichiers :

-b fichier

Vrai si fichier est un fichier spécial de type bloc

-c fichier

Vrai si fichier est un fichier spécial de type caractère

-d fichier

Vrai si fichier est un répertoire

-f fichier

Vrai si fichier est un fichier ordinaire

-r fichier

Vrai si fichier est un fichier accessible en lecture

-w fichier

Vrai si fichier est un fichier accessible en écriture

-x fichier

Vrai si fichier est un fichier exécutable

Il y a d'autres opérateurs mais ceux-ci sont les plus utilisés. Maintenant, on peut tester si notre script a en entrée un répertoire ou un fichier : modifions donc encore un peu le programme.

1    #!/bin/sh
2
3    if [ ! $1 ]
4    then
5     echo "Usage: `basename $0` nom_repertoire"
6     exit 1
7    fi
8
9    if [ -d $1 ]
10    then
11     DIR="/$1"
12    fi
13
14    if [ -f $1 ]
15    then
16     DIR=""
17    fi
18
19    for a in `ls $DIR`
20    do
21     fname=`echo $a | tr A-Z a-z`
22     mv $DIR$a $DIR$fname
23    done;

Nous avons inséré les lignes 9 à 17 pour effectuer les vérifications fichier/répertoire. S'il s'agit d'un répertoire, on initialise DIR à « /$1 », sinon on l'initialise à vide. Remarquez que l'on met maintenant le / du répertoire dans la variable DIR et que nous avons modifié la ligne 22 pour qu'il n'y ait pas de / entre $DIR et $a. Avec cette méthode, les chemins sont corrects.

On a encore quelques problèmes. L'un d'eux est que, si un fichier porte déjà le nom que nous voulons attribuer, le script produit une erreur. On doit donc vérifier le nouveau nom de fichier avant de renommer. Un autre problème est de savoir ce qui se passe si quelqu'un met plus d'un paramètre. Nous allons modifier notre script pour accepter plus d'un répertoire ou fichier.

Le premier problème se corrige facilement en utilisant un simple test de chaîne et une instruction if, comme nous l'avons fait plus haut. Le deuxième problème est légèrement plus difficile car nous devons connaître le nombre de paramètres que l'utilisateur a fourni en entrée. Pour obtenir cette information, on utilise une variable spéciale du shell, toute préparée pour nous. Il s'agit de la variable $# qui contient le nombre de paramètres de la ligne de commande. Ce que nous voulons faire est une boucle sur tous les paramètres. Cette fois-ci, nous utiliserons la boucle while pour faire ce travail. Enfin, nous aurons besoin de comparer des valeurs entières car on veut pouvoir comparer le nombre de fois où l'on est passé dans la boucle au nombre de paramètres. Il existe des options de test spéciales pour évaluer les entiers, les voici :

int1 -eq int2

(equal) Vrai si l'entier 1 est égal à l'entier 2.

int1 -ge int2

(greater or equal) Vrai si l'entier 1 est supérieur ou égal à l'entier 2.

int1 -gt int2

((greater than) Vrai si l'entier 1 est supérieur à l'entier 2.

int1 -le int2

(lesser or equal) Vrai si l'entier 1 est inférieur ou égal à l'entier 2.

int1 -lt int2

(lesser than) Vrai si l'entier 1 est inférieur à l'entier 2.

int1 -ne int2

(nor equal) Vrai si l'entier 1 est différent de l'entier 2.

Avec ces nouvelles informations, nous pouvons modifier notre programme.

1    #!/bin/sh
2
3    if [ ! $1 ]
4    then
5      echo "Usage: `basename $0` nom_repertoire"
6      exit 1
7    fi
8
9    while [ $# -ne 0  ]
10   do
11     if [ -d $1 ]
12     then
13       DIR="/$1"
14     fi
15
16     if [ -f $1 ]
17     then
18       DIR=""
19     fi
20
21     for a in `ls $DIR`
22     do
23       fname=`echo $a | tr A-Z a-z`
24       if [ $fname != $a ]
25       then
26         mv $DIR$a $DIR$fname
27       fi
28     done;
29
30     shift
31   done

Nous avons inséré une boucle while en ligne 9 qui vérifie si le nombre de paramètres est égal à zéro. Cela peut ressembler à une boucle sans fin mais l'instruction shift en ligne 30 nous sauve. Elle annule le paramètre le plus proche du nom de la commande et le remplace par le suivant. Cette boucle annulera tous les paramètres éventuels : leur nombre sera alors de zéro, ce qui terminera la boucle.

Enfin, remarquez l'instruction if en ligne 24 : elle vérifie si le nom de fichier est déjà en minuscules et, si c'est le cas, l'ignore.

J'espère que vous avez apprécié cette brève introduction à la programmation Bourne/Bash. Je vous encourage à essayer vous-même ces exemples. En fait, si vous pourriez améliorer ce script en lui ajoutant une option comme -m pour les minuscules et -M pour les majuscules.

J'assume la pleine responsabilité des erreurs ou confusions dans cette documentation. Merci d'envoyer vos commentaires ou questions à rick@ricken.demon.co.uk.

1.1 Références

« The UNIX programming environment »

Brian W. Kernighan & Rob Pike, Prentice Hall.

« Inside UNIX »

New Riders.


Précédent Suivant Table des matières

Copyright © 1998, Rick Dearman. Publié dans le n°25 de la « Linux Gazette », Février 1998

Adaptation française : Éric Jacoboni.