Blog Zenika

#CodeTheWorld

Web

Shell, mon amour

Depuis de nombreuses années, peu de personnes comprennent mon affection pour les scripts Shell (OS X ou Linux). Je suis triste sur les missions où un poste Windows est imposé sans possibilité d’installer un git bash ou un cygwin… L’absence de grep, sed, awk est un manque cruel pour le développeur que je suis.
Je ne vais pas vous faire un mode d’emploi des différentes commandes, mais plutôt vous présenter les différentes astuces que j’utilise pour écrire des scripts fiables.

La suite de l’article est volontairement restreinte à bash. Bien que cet interpréteur ne soit pas le plus complet, il reste le plus installé sur les différentes machines et son utilisation assure une bonne portabilité (le débat associé n’est pas du ressort de cette prose).

Couleur

Qu’est-ce qui est plus austère qu’un fichier de traces dans une console visualisée avec less ou vi(m) ? Il est tout de même possible d’écrire facilement des traces en couleur, je l’utilise beaucoup pour signifier la séparation d’étapes de traitement ou pour mettre en avant un message important par rapport à une ligne de debug.
De nombreux sites (comme celui-ci) référencent les codes d’échappement pour écrire en couleur, mais il faut être honnête, c’est plus simple de se rappeler d’un style CSS que \e[36.
En début de script, j’ajoute alors quelques déclarations pour m’aider :

#!/bin/bash
# Définition des couleurs de base
RED='\033[0;31m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
NC='\033[0m'
echoBlue () {
   echo -e "${BLUE}$*${NC}"
}
echoMagenta () {
   echo -e "${MAGENTA}$*${NC}"
}
echoRed () {
   echo -e "${RED}$*${NC}"
}
echoBlue "Un message en bleu"
echoMagenta "En violet"
echoRed "En rouge"

Ce qui donne :

Ici, ce script déclare des variables RED, BLUE, MAGENTA contenant les caractères d’échappement Shell pour écrire dans les couleurs idoines accompagnées de la variable NC qui permet de revenir à la couleur standard.
Les fonctions echoBlue, echoMagenta et echoRed factorisent l’appel à la commande echo avec l’option -e pour interpréter les caractères d’échappement.
La chaîne « ${BLUE}$*${NC} » est la concaténation de la valeur de la variable BLUE, de tous les arguments donnés à la fonction echoBlue suffixés par la valeur de la variable NC.

Fail fast

Par défaut, l’exécution d’un script Shell ne s’arrête pas à la première erreur d’exécution d’une commande (code de retour différent de zéro), mais continue le plus loin possible. Ce comportement peut vite produire des effets plutôt indésirables. 😢
Une solution possible consiste à valoriser le shebang (première ligne du fichier) de manière adéquate en utilisant l’option -e de set.
#!/bin/bash devient #!/bin/bash -e.
Exemple :

#!/bin/bash -e
[...]
echoBlue "Un message en bleu"
echoRed "En rouge"
commande inexistante # un plantage est attendu à cette ligne
echoMagenta "En violet"

Résultat :

Sans -e avec -e

 
Le script s’arrête désormais dès la première erreur.
Ce fonctionnement peut être globalement activé en mettant -e dans le shebang ou localement en appelant l’instruction set -e. Pour le désactiver, il faut appeler set +e.

Debug

La mise au point d’un script Shell peut être laborieuse, il n’existe pas de mode pas à pas comme nous pouvons l’avoir avec des langages de plus haut niveau. Néanmoins, il est possible de produire des traces détaillées affichant chaque instruction ou commande externe exécutée : set -x. Comme pour le -e vu précédemment, il faut le mettre soit dans le shebang, soit autour de la zone à débugger.
Exemple :

#!/bin/bash -x
[...]

Résultat :

Les lignes affichées commençant par + indiquent l’instruction qui sera exécutée. Les chaînes de caractères sont affichées en version finale (l’expansion des variables faite).
Il est même possible de combiner le -e et -x :
#!/bin/bash -xe

Messages d’erreurs

Comme dans les autres langages, la gestion des erreurs est primordiale, notamment pour supprimer des fichiers temporaires ou même afficher un message d’erreur explicite. Je propose ici un simple gestionnaire d’erreur qui affiche une trace en rouge lorsqu’une erreur est rencontrée :

#!/bin/bash -e
# Définition des couleurs de base
RED='\033[0;31m'
NC='\033[0m'
echoRed () {
   echo -e "${RED}$*${NC}"
}
set -o pipefail  # trace les erreurs dans les pipes
set -o errtrace  # trace les erreurs dans les fonctions appelées
function error() {
  JOB=$(basename "$0")
  echoRed "ERREUR dans ${JOB} : ligne $1 avec le code d'erreur $2" 1>&2
  exit 1
}
trap 'error ${LINENO} ${?}' ERR

Ici, la fonction error() sera appelée avec le numéro de la ligne sur laquelle l’erreur a été rencontrée et le code d’erreur rencontré. Elle affiche sur la stderr en rouge un message indiquant le nom du script, le numéro de la ligne ainsi que le code d’erreur rencontré. Enfin, le programme s’arrête avec le code d’erreur 1.
L’instruction trap  enregistre cette fonction sur le signal ERR.
Les instructions set -o complètent la détection d’erreur dans les pipelines et dans les fonctions appelées.
Résultat :

Remarque : le gestionnaire d’erreur doit être le plus fiable possible, car s’il plante lui aussi lors du traitement d’une erreur tous ces efforts seront perdus 😢
De la même manière, la suppression de fichiers temporaires (par exemple) peut être réalisée avec un trap :

trap 'rm -f ${temp_file}' SIGINT SIGQUIT EXIT

Mutualisation de code

Pour améliorer la lisibilité de notre code, nous allons mettre toutes les instructions vues précédemment dans un fichier distinct de notre code. En plus, il pourra être inclus dans un autre script afin de mutualiser le code ! (Comme dans le reste du monde de la programmation finalement 😉 )
common.sh contient les fonctions communes :

#!/bin/bash -e
# Définition des couleurs de base
RED='\033[0;31m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m'
echoBlue () {
   echo -e "${BLUE}$*${NC}"
}
echoMagenta () {
   echo -e "${MAGENTA}$*${NC}"
}
echoRed () {
   echo -e "${RED}$*${NC}"
}
# Traitement des erreurs
set -o pipefail  # trace les erreurs via les pipes
set -o errtrace  # trace les erreurs dans les fonctions appelées
function error() {
  JOB=$(basename "$0")
  echoRed "ERREUR dans ${JOB} : ligne $1 avec le code d'erreur $2" 1>&2
  exit 1
}
trap 'error ${LINENO} ${?}' ERR

monscript.sh :

#!/bin/bash -e
SCRIPTPATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
source ${SCRIPTPATH}/common.sh
# suite du script
[...]

La valeur de la variable SCRIPTPATH sera le dossier contenant le script monscript.sh. Son écriture est un peu barbare et pourrait être simplifiée avec dirname, mais au détriment de la portabilité (Linux, OS X, cygwin…).
L’instruction source permet de charger le fichier donné en paramètre, comme un include en C ou import en js.

Pour résumer

Les scripts Shell sont très intéressants pour commencer à automatiser des traitements, mais sans ces quelques instructions, ils seront moins fiables.
Il manque également un point important, à savoir les tests unitaires.
Bien que les scripts Shell existent depuis la nuit des temps, la pratique du TDD n’est pas courante et reste difficile à mettre en oeuvre : il faut mocker toutes les commandes appelées, les manipulations de fichiers, etc. Néanmoins, BATS et bash-spec sont des bons points de départ. L’ajout d’un linter dans sa CI comme shellcheck permet également d’aller plus loin dans la démarche.
N’hésitez pas à poster vos commentaires et partages d’expériences ci-dessous 🙂
 

Une réflexion sur “Shell, mon amour

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

En savoir plus sur Blog Zenika

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Continue reading