Gestion des erreurs, set -e et/ou trap

Afin d’uniformiser les pratiques de packaging, je propose de discuter de la gestion d’erreur dans les scripts avec set -e et trap.

Tout d’abord, petit état des lieux de la situation.

L’objectif est d’empêcher le script de continuer si il a rencontré une erreur pouvant potentiellement compromettre le bon fonctionnement du package.
Or, par défaut, le mécanisme des sorties de commandes étant ce qu’il est, une erreur au milieu du script n’empêchera pas le script de se terminer correctement si la dernière commande n’échoue pas. (Puisque c’est la dernière commande du script qui transmettra son code de sortie.)
C’est là qu’entrent en jeu set -e et trap, tout deux permettent de repérer une commande qui aurait échouée durant l’exécution du script, ce qui permet de stopper son exécution avant son terme.

Quelles différences en set -e et trap?
Toutes deux sont des commandes interne de bash (Mais ce n’est pas un problème dans notre cas d’usage). Ce qui implique que leur man se trouvera sur man bash

  • set -e provoque un arrêt immédiat du script si une commande renvoi un code de sortie différent de 0. Je ne connais pas son code de sortie, mais j’imagine que c’est celui de la dernière commande.
    set -e ne permet aucune autre action, il agit immédiatement.
    Il est très simple à mettre en place, puisque qu’un simple set -e en début de script suffit à l’activer. Il peux être désactivé avec set +e

  • trap capture les codes de sortie de toutes les commandes du scripts et peux exécuter une commande si certains codes de sorties sont capturés.
    Dans notre cas d’usage, trap sera configuré pour déclencher un exit si une commande renvoi un code de sortie différent de 0.
    trap peux exécuter une autre commande que exit, et en particulier il peux exécuter une fonction afin de s’assurer de quitter proprement le script.
    trap est plus plus délicat à mettre en place que set -e, puisqu’il doit d’abord être activé avec trap fonction ERR, et que fonction doit être une fonction déclarée en amont. C’est cette fonction qui devra se terminer par exit.
    Un peu de documentation sur trap, adapté à notre usage.

La question à présent est, doit-on préférer l’usage de set -e ou de trap. set -e est plus simple et donc plus fiable, mais trap permet d’autres actions avant de quitter. Tel qu’afficher un message d’erreur ou nettoyer des fichiers résiduels.


Par exemple, un cas d’usage de trap sur le package jenkins.


Ici trap permet d’arrêter l’exécution de tail en arrière plan, qui est démarré ici. Sans ça, si le script quitte durant la boucle de démarrage, tail continue son exécution en arrière plan et l’installation ne se termine jamais.

Merci pour cette introduction claire. Je manque cependant encore de connaissances pour avoir un avis.

Que se passe-t-il au juste dans le code de ynh quand une install échoue ? Il lance le script remove non ? Ça ressemble ainsi à une sorte d’implémentation simplifiée de trap ne nécessitant rien de plus que l’existant.

Peut-on utiliser trap sans fonction ? Ou avec une fonction par défaut similaire à set -e, definie en amont dans le code ynh ?

Parce que dans l’etat devoir rajouter des fonctions trap dans les package les alourdit d’autant plus …

De plus, quels sont les cas d’usages fréquents ? J’avoue ne jamais avoir vraiment réfléchi à ça sur mes apps

Quand l’install échoue, le script remove est exécuté oui. Le problème est de savoir quand et comment il échoue, c’est là qu’interviennent trap et set -e, ils forcent la sortie du script avant qu’il n’aille plus loin si une commande rencontre une erreur.
Le remove n’est exécuté qu’après le script, si celui-ci a échoué.

Oui, comme on peut le voir sur le package jenkins, la fonction appelée par trap n’est pas dans le script, mais dans un helper.
Toutefois cette fonction appelée par trap appelle une seconde fonction qui est dans le script pour nettoyer les résidus propre au package lui-même.

Toujours sur jenkins

  • tail qui tourne en arrière plan et qui bloque le script si il n’est pas arrêté. En l’occurrence, le script remove n’est jamais exécuté si tail ne s’arrête pas. Car le script install ne se termine pas tant que tail continue à tourner.
  • L’entrée dans le host, qui n’est pas retirée par le script remove car c’est le script install qui la retire en fin d’installation.
  • Et plus globalement afficher un message d’erreur indiquant clairement un échec de l’installation.

Et de façon encore plus globale, que ce soit pour trap ou set -e, éviter de poursuivre une installation si, par exemple, le wget n’a pas atteint la source. Parce qu’alors, le script install pourrait se terminer avec succès alors même que le package n’est pas du tout fonctionnel.

Je jette ça comme ça.

L’option -x de set permet un trace, pas inutile.
Et peut-être que -E pourrait permettre de passer dans trap. C’est à explorer.

Décidément, teampass ne vas pas avancer si je continue à me disperser…

MAIS ! J’ai trouvé la solution qui réconcilie set -e et trap.
Et qui permet aussi d’utiliser set -u, qui est un vilain set qui outrepasse trap à tout les coups!

Donc effectivement, l’usage de set -E permet de passer par trap et ne pas sortir tout de suite, par contre, set -u, lui passe tout droit!

Petit rappel: set -u permet d’arrêter le script en cas d’usage d’une variable non initialisée. Ce qui évite de faire n’importe quoi.

La solution est donc de déplacer le champs d’action de trap, on ne lui demande plus de saisir les codes de sorties en erreur, mais de saisir la sortie du shell. On laisse ainsi à set -eu le boulot de surveiller les sorties de commandes, et si set décide de quitter le script, trap entre en action.
Bien évidemment, cela à une conséquence facheuse (trop facile sinon), c’est que lorsque le script atteint son terme normalement, il quitte le shell… Donc il déclenche trap…
Trap doit donc prendre en compte le code de sortie de la dernière commande du shell.

Tout ça revient simplement à ces quelques modifications:

trap EXIT_PROPERLY ERR

est remplacé par

trap EXIT_PROPERLY EXIT

Et au début de EXIT_PROPERLY on ajoute ces quelques lignes:

        exit_code=$?
        if [ "$exit_code" -eq 0 ]; then
                exit 0
        fi
        trap '' EXIT
        set +eu

Pour quitter simplement si tout va bien, ou enchaîner avec le trap habituel si il y a une erreur.

Reste plus qu’à espérer qu’il quittera le shell avec un sous-process en arrière plan. Et là, c’est pas sûr…
EDIT: Ça fonctionne très bien, le shell est quitté et le processus en arrière plan est terminé.
Et set -eu peux être intégré à la fonction TRAP_ON pour simplifier l’appel.

Très intéressant tout cela, merci.

Du coup on peut l’utiliser dans tous les scripts (install, upgrade, backup, restore) sauf remove ?

Il y a un helper en attente pour ça, tu peux le repomper.

Quand aux cas d’usage. Je crois que ce n’est pas si simple.
Pour install c’est évident, mais pour les autres je m’interroge sur ce qu’il se passe si on arrête le script en pleine exécution.

Quand à remove, je pense qu’il ne doit pas avoir de gestion d’erreur de la sorte.