Erreurs fréquentes dans les scripts shell : Différence entre versions

De Ensiwiki
Aller à : navigation, rechercher
m (lien mort)
(Utilisation de constructions spécifiques à un shell {{Ancre|posix}})
Ligne 306 : Ligne 306 :
 
Il existe plusieurs implémentation du shell Unix. La plupart sont des descendants plus ou moins lointain du « [http://en.wikipedia.org/wiki/Bourne_shell Bourne Shell] ». Les fonctionnalités de base des shell ont depuis été standardisées dans la norme POSIX (cf. la page [http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html 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.
 
Il existe plusieurs implémentation du shell Unix. La plupart sont des descendants plus ou moins lointain du « [http://en.wikipedia.org/wiki/Bourne_shell Bourne Shell] ». Les fonctionnalités de base des shell ont depuis été standardisées dans la norme POSIX (cf. la page [http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html 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 un script commençant par <code>#!/bin/sh</code> 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é).
+
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 <code>#!/bin/sh</code> 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 :
 
Solutions :

Version du 22 décembre 2016 à 13:44

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

x='un nom de fichier avec espaces.txt'
ls $x

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 :

ls "$x"

(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 :

# Ne marche pas :-(
if [ "$image" = *.jpg ]; then
	echo "c'est un *.jpg"
fi

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

# Ne marche pas :-(
if [ "$image" = toto.jpg tutu.jpg tata.jpg ]; then
        echo "c'est un *.jpg"

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 :
  if echo "$image" | grep -q '\.jpg$'; then
          echo "c'est un *.jpg"
  fi
  • Une autre solution, un peu plus rapide à l'exécution est d'utiliser une structure case :
  case "$image" in
  *.jpg)
          echo "c'est un *.jpg"
          ;;
  esac

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 :
  # Contenu du fichier utilitaires.sh
  dire_bonjour () {
          echo "bonjour."
  }
  # Contenu du fichier main.sh
  
  # Cette ligne demande au shell de lire le fichier utilitaires.sh.
  # C'est comme si on avait copié-collé utilitaires.sh à cet endroit.
  # Comme utilitaires.sh ne contient que des définitions de fonctions,
  # le fait de lire le fichier n'exécute rien.
  . utilitaires.sh
  
  # Appel de la fonction définie pendant la lecture de utilitaires.sh
  dire_bonjour
  • Écrire un script, qui sera exécuté depuis le script principal. Par exemple :
  #! /bin/bash
  # Contenu du fichier bonjour.sh
  
  # Ce fichier est un script, il doit être exécutable (chmod +x)
  # Quand on lance le script, il exécute « echo bonjour ».
  echo bonjour
  # Contenu du fichier main.sh
  
  # On ne lit pas le contenu de bonjour.sh, mais on l'exécute comme   n'importe quel exécutable.
  ./bonjour.sh

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 :

source="$1"
destination="$2"

cd "$destination"
cp "$source"/image.jpg .

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 :

repertoire="$1"
fichier="$2"

fichier_dans_repertoire="$repertoire"/"$fichier"

ou plus simplement

fichier_absolu="$PWD"/"$fichier"

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 :

cd "$source"
echo "je suis dans la source"
cd ..
echo "je suis revenu là où j'étais"

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 :
  HERE="$PWD"
  cd "$source"
  echo "je suis dans la source"
  cd "$HERE"
  echo "je suis revenu là où j'étais"
  • Utiliser cd - (mais produit un affichage non souhaité en général), ou pushd/popd (à éviter dans un script, non POSIX)
  • Utiliser un sous-shell :
  (
  cd "$source"
  echo "je suis dans la source"
  )
  echo "je suis revenu là où j'étais (en fait, je n'ai pas bougé, c'est le sous-shell qui est allé dans la source)"

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 :

# Charger une fichier contenant des fonctions
. ./utilities.sh

ou bien :

# Recopier des images dans le dossier généré
cp ./fleche-gauche.png ./fleche-droite.png "$dest"/design/

En pratique, ces deux exemples marcheront si le script est appelé depuis le répertoire qui le contient, par exemple avec :

./gallery-shell.sh

Mais que se passe-t-il si l'utilisateur l'appelle depuis un autre répertoire ? par exemple :

cd tests/
../gallery-shell.sh

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 :

SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)"

. "$SCRIPT_PATH"/utilities.sh

cp "$SCRIPT_PATH"/fleche-gauche.png "$SCRIPT_PATH"/fleche-droite.png "$dest"/design

Imbrication de guillemets

Pour afficher une chaîne contenant des guillemets doubles, on peut tenter :

echo "Je dis "bonjour"."

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 bonjour en dehors de guillemets veut aussi dire « la chaine bonjour »...). 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 (\) :
  echo "Je dis \"bonjour\"."
  • Utiliser des guillemets simples :
  echo 'Je dis "bonjour".'

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 :

echo "La variable i vaut "$i" et la variable j vaut "$j""

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 :
  echo "La variable i vaut $i et la variable j vaut $j"
  • Si on souhaite afficher des guillemets doubles autour des valeurs de $i et $j, on peut utiliser :
  echo "La variable i vaut \"$i\" et la variable j vaut \"$j\""

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/sh et se limiter aux fonctionnalités spécifiées dans POSIX.
  • Si on veut utiliser des fonctionnalités de bash, commencer ses scripts par #!/bin/bash. Le script ne fonctionnera pas sur une machine où bash n'est pas installé (c'est assez rare de nos jours)

Autres erreurs fréquentes