Erreur fréquentes dans les scripts shells
Oubli des guillemets en utilisant des variables
Une particularité du shell POSIX est que l'interprétation des blancs se fait après l'expansion des noms de variables.
Par exemple, si on écrit
1 2 | |
alors $x sera expansé en un nom de fichier avec espaces.txt, et la
commande exécutée sera
ls un nom de fichier avec espaces.txt
qui va chercher les fichiers « un », « nom », « de », etc. (ce qui en général n'est pas ce qu'on souhaite). La version correcte dans ce cas serait :
1 | |
(avec des guillemets doubles autour de $x). La variable $x sera
remplacée par sa valeur, et les guillemets doubles protègeront sa valeur
pendant l'interprétation des blancs.
Diagnostique :
- Tester son code avec des valeurs de variables contenant des espaces (typiquement, des noms de fichiers ou de répertoires contenant des espaces)
Solutions :
- À moins d'avoir une bonne raison, toujours utiliser la syntaxe
"$x"pour utiliser une variable shell.
Mauvaise utilisation des wildcards (*.jpg, ...)
Les wildcards (par exemple, *.jpg, *.ad[bs], ...) sont bien
pratiques, mais ne peuvent pas être utilisés partout. Par exemple :
1 2 3 4 | |
Ce test ne veut pas dire « Est-ce que la variable image ressemble à
*.jpg », mais « expanse la valeur de *.jpg, puis teste si la
condition obtenue est vraie ». L'expansion de *.jpg va probablement
donner quelque chose comme
1 2 3 | |
et la commande [ (alias test) va lever une erreur puisque
l'opérateur != n'est pas censé prendre plusieurs valeurs en opérande
droit.
Solutions :
-
Ne jamais utiliser de wildcard à un endroit où on s'attend à un seul argument
-
Pour tester si une chaine correspond à un motif, on peut par exemple utiliser
grep:
1 2 3 | |
- Une autre solution, un peu plus rapide à l'exécution est
d'utiliser une structure
case:
1 2 3 4 5 | |
Confusion entre inclusion et exécution d'un script
Pour découper un script en plusieurs fichiers, on a deux solutions :
- Écrire des fonctions shell dans un fichier séparé, demander au fichier principal de lire ce fichier, puis utiliser les fonctions. Par exemple :
1 2 3 4 | |
1 2 3 4 5 6 7 8 9 10 | |
- Écrire un script, qui sera exécuté depuis le script principal. Par exemple :
1 2 3 4 5 6 | |
1 2 3 4 5 | |
Une erreur fréquente est de mélanger les deux.
Solutions :
- Si un fichier est fait pour être chargé via la directive « . », il ne doit contenir que des définitions de fonctions. Lire le fichier ne doit exécuter aucune commande.
- Si un fichier est fait pour être exécuté comme un script, il ne doit pas être lu avec la directive « . », doit être exécutable et commencer par le shell-bang (#!/bin/sh), comme tous les scripts shell.
Chemin relatif, chemin absolu et « cd »
Un chemin relatif est un chemin comme ./repertoire/fichier.jpg, ou
plus simplement toto.jpg. Contrairement à un chemin absolu, sa
signification dépends de l'endroit d'où il est utilisé.
Une erreur fréquente est d'utiliser un chemin relatif après la commande
cd. Par exemple :
1 2 3 4 5 | |
Si on appelle ce script avec la ligne de commande
script repertoire1 repertoire2, alors il va essayer de copier
repertoire2/repertoire1/image.jpg, ce qui n'est pas ce que
l'utilisateur aurait voulu.
Solutions :
-
Ne pas utiliser la commande
cd -
Ou bien, rendre tous les chemins absolus avant d'utiliser
cd.
Concaténation de chemins
Il est tentant d'écrire du code comme celui-ci :
1 2 3 4 | |
ou plus simplement
1 | |
Malheureusement, ces deux exemples font l'hypothèse que "$fichier"
est un chemin relatif. Par exemple, dans le deuxième cas, si
"$fichier" vaut /home/john/mon/image.jpg et que le script est
exécuté dans /home/smith/repertoire, alors le résultat sera
/home/smith/repertoire//home/john/mon/image.jpg, qui a priori
n'existera pas.
Solutions :
- Ne jamais faire de concaténation de chemin de type
"$un"/"$deux"si"$deux"est un chemin spécifié par un utilisateur (qui pourra être relatif ou absolu)
Suppositions abusives sur des noms de répertoires, cd ..
Il est tentant d'écrire :
1 2 3 4 | |
Malheureusement, ce code ne marche que si \"\$source\" est exactement un
niveau de répertoire au-dessous du point de départ. Si
source=dir1/dir2/dir3, alors après exécution des deux cd, on se
trouve dans dir1/dir2. Si source=/autre/repertoire, alors on va dans
le répertoire /autre/ ...
Solutions :
- Mémoriser le point de départ dans une variable :
1 2 3 4 5 | |
-
Utiliser
cd -(mais produit un affichage non souhaité en général), oupushd/popd(à éviter dans un script, non POSIX) -
Utiliser un sous-shell :
1 2 3 4 5 6 7 | |
Supposition abusive sur le répertoire d'exécution du script
Pour accéder à des fichiers se trouvant dans le même répertoire qu'un script il est tentant de considérer que ces fichiers sont dans le répertoire courant (.).
Par exemple :
1 2 | |
ou bien :
1 2 | |
En pratique, ces deux exemples marcheront si le script est appelé depuis le répertoire qui le contient, par exemple avec :
1 | |
Mais que se passe-t-il si l'utilisateur l'appelle depuis un autre répertoire ? par exemple :
1 2 | |
Utilisé comme cela, nos deux exemples vont tenter d'accéder à utilities.sh, fleche-gauche.png, fleche-droite.png dans le répertoire courant, c'est-à-dire tests, et ça ne va pas marcher.
Dans la vraie vie, il est assez rare de se trouver dans le répertoire
contenant un exécutable pour le lancer (pour lancer Firefox, avez-vous
vraiment envie d'écrire cd /usr/local/bin; ./firefox ?). Donc, en
dehors de vos TPs, le cas qui ne marche pas est le plus courant.
Solution :
\"\$0\" est le nom du script (possiblement relatif), \"\$(dirname \"\$0\")\" est le répertoire contenant le script (toujours possiblement relatif), et \"\$(cd \"\$(dirname \"\$0\")\" && pwd)\" est le répertoire contenant le script (absolu). On peut donc écrire :
1 2 3 4 5 | |
Imbrication de guillemets
Pour afficher une chaîne contenant des guillemets doubles, on peut tenter :
1 | |
Malheureusement, cette chaîne doit être interprétée comme :
- Un guillemet ouvrant
- La chaîne « Je dis »
- Un guillemet fermant
- La chaîne « bonjour » en dehors des guillemets (et oui, en shell,
écrire simplement
bonjouren dehors de guillemets veut aussi dire « la chainebonjour»...). Avec un peu de chance, votre éditeur de texte va matérialiser le fait que bonjour est en dehors des guillemets en le colorant différemment du reste de la chaîne (c'est le cas avec la coloration syntaxique d'EnsiWiki). - Un guillemet ouvrant
- Un point
- Un guillemet fermant
Concrètement, la commande va afficher Je dis bonjour., sans les
guillemets autour de bonjour.
Diagnostique :
La coloration syntaxique de votre éditeur de texte devrait vous mettre sur la voie (Emacs le fait très bien) : le contenu de la chaine est colorié d'une couleur, et dans notre exemple, le mot « bonjour » n'est pas coloré (la coloration syntaxique du wiki met également le bug en évidence ci-dessus).
Solution :
- Échapper les guillemets à l'intérieur des chaînes en utilisant un backslash (\) :
1 | |
- Utiliser des guillemets simples :
1 | |
Variable mal protégée à l'intérieur d'une chaîne
Une variante du problème ci-dessus est de tenter de protéger deux fois une variable avec des guillemets doubles :
1 | |
Ici encore, les guillemets autour de $i et $j sont un guillemet
fermant suivi d'un guillemet ouvrant, les deux variables se trouvent en
dehors des guillemets.
Solution :
- À l'intérieur d'une chaîne, on est déjà protégés par les guillemets doubles, on peut écrire :
1 | |
- Si on souhaite afficher des guillemets doubles autour des valeurs de
$iet$j, on peut utiliser :
1 | |
Utilisation de constructions spécifiques à un shell
Il existe plusieurs implémentation du shell Unix. La plupart sont des descendants plus ou moins lointain du « Bourne Shell ». Les fonctionnalités de base des shell ont depuis été standardisées dans la norme POSIX (cf. la page Shell Command Language). Sur un Unix raisonnable, le shell /bin/sh implémente la norme POSIX. Beaucoup de shell implémentent plus de fonctionnalités celles de la norme : par exemple, bash ajoute une notion de tableau qu'il est tentant d'utiliser.
Sur certains Unix, /bin/sh est un lien symbolique vers /bin/bash (par
exemple, Red Hat et donc CentOS), et un script commençant par
#!/bin/sh pourra donc utiliser les fonctionnalités de bash. C'est en
fait une très mauvaise idée de le faire : le même script ne pourra pas
s'exécuter sur un système où /bin/sh est plus limité (par exemple,
Debian, où /bin/sh est un lien symbolique vers dash, volontairement
limité). En pratique, cela veut dire que votre script écrit et testé à
l'Ensimag sur une CentOS ne marchera pas forcément sur la machine de
votre correcteur ...
Solutions :
- Si on veut être très portable, commencer ses scripts par
#!/bin/shet se limiter aux fonctionnalités spécifiées dans POSIX.
1 | |
#!/bin/bash. Le script ne fonctionnera pas sur une
machine où bash n'est pas installé (c'est assez rare de nos jours)
Variables nommées ou paramètres numérotés
Ce n'est pas une erreur à proprement parler mais un problème de style :
utiliser $1, $2, ... au lieu d'utiliser des variables
judicieusement nommées rend le code peu lisible. Comparons :
1 2 3 4 5 6 7 8 9 10 | |
Et la version utilisant des variables nommées :
1 2 3 4 5 6 7 8 9 10 | |
Le deuxième cas est plus lisible, et également moins sujet à erreur :
c'est facile de se tromper dans la numérotation et d'écrire $4 au
lieu de $5, mais si les variables sont bien nommées il n'y a aucune
raison de se tromper en les utilisant. Il n'y a pas réellement plus de
code : le commentaire présent dans la première version n'est plus
nécessaire dans la seconde, donc le code utilisant les variables est
même un peu plus compact.
Autres erreurs fréquentes
- UUOC, UUOL, [ http://archive.today/9Zcyu ], ...