- Quand un script Bash ne se comporte pas comme prévu, voir telles quelles les commandes exécutées suffit souvent à faciliter la recherche de la cause
set -xaffiche chaque ligne après expansion des variables, ce qui permet de vérifier quelles commandes le script exécute réellement- Lancer
bash -x script.shen ligne de commande produit le même effet que mettreset -xen haut descript.sh - En combinant le trap
DEBUGavecread, on peut s’arrêter avant l’exécution de chaque ligne pour vérifier le nom du fichier, le numéro de ligne et la prochaine commande - La fonction
die() { echo $1 >&2; exit 1; }, ajoutée après une commande qui échoue, simplifie le flux consistant à afficher un message sur la sortie d’erreur standard puis quitter
Vérifier visuellement le flux d’exécution
set -xaffiche les lignes exécutées par le script, avec les variables remplacées par leurs valeurs développées- On peut l’utiliser en ajoutant
set -xen haut du script - Le même comportement peut aussi être activé depuis la ligne de commande
$ bash -x script.sh- C’est équivalent à mettre
set -xtout en haut descript.sh
S’arrêter à chaque ligne pour vérifier
- Le trap
DEBUGs’exécute avant chaque ligne de code - En plaçant le code ci-dessous au début du script, l’exécution attend une pression sur Entrée avant de lancer la commande suivante
trap '(read -p "\[$BASH_SOURCE: $LINENO] $BASH_COMMAND")' DEBUGread -paffiche un message et attend une pression sur Entrée$BASH_SOURCEcorrespond au nom du fichier de script$LINENOcorrespond au numéro de ligne$BASH_COMMANDcorrespond à la commande qui sera exécutée ensuite
Laisser un message puis quitter en cas d’échec
- La fonction
diepeut servir à afficher un message et terminer le programme lorsqu’une commande échouedie() { echo $1 >&2; exit 1; }- Il suffit de l’ajouter après une commande susceptible d’échouer, par exemple
some_command || die "oh no!"
- Cette fonction envoie le message vers l’erreur standard et termine avec
exit 1
1 commentaires
Avis sur Hacker News
On trouve une fonction de journalisation
zdebugun peu partout dans le code : https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...Avec la journalisation de débogage activée, appuyer sur Ctrl-T dans le menu principal affiche ce type d’écran : https://i.imgur.com/Ge75zkP.png
Il y a aussi du profilage par flamegraph, activable via https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme... ; en réassemblant les données vidées via le port série, on peut produire un graphe comme https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master...
Bash est étonnamment flexible
set -x,PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: 'est très utileCela affiche le nom du fichier, le nom de la fonction et le numéro de ligne, ce qui aide pas mal pour déboguer de gros scripts Bash
shellcheck. Même s’il ne détecte pas directement le problème, il signale des problèmes potentielsJe recommande également de réécrire les scripts dans un autre langage. Dans mon entreprise, nous remplaçons des scripts Bash par du Rust ; le coût d’entrée est plus élevé, mais le code obtenu est bien plus facile à maintenir et plus fiable
Bash reste très bien pour les scripts rapides, mais au-delà d’une centaine de lignes, il vaut la peine d’utiliser un langage offrant des garanties plus fortes
die()est une fonction utilitaire qui affiche un message d’erreur sur la sortie d’erreur standard, puis quitte avec le code d’erreur indiquéOn trouve davantage de codes de sortie de scripts shell et de fonctions utilitaires ici : https://github.com/SixArm/unix-shell-script-kit/blob/main/un...
Cela dit, la philosophie de ce
dieme laisse un peu sur ma faim. Une fonctiondiedevrait, par défaut, transmettre le code de sortie de la commande qui a échoué, et ne pas masquer non plus la sortie d’erreur de cette commandeDans un gros script, si je veux donner moi-même un sens particulier à l’échec d’une commande, j’utiliserai un
dieplus spécialisé. Mondieressemble en gros à__errex "$?" "${LINENO}" "$0", qui affiche l’erreur fatale, le numéro de ligne, le nom du script et le message, puis termine avec ce code de sortieUn exemple d’implémentation se trouve ici : https://github.com/TritonDataCenter/sdc-headnode/blob/master...
En l’utilisant comme
some-command || fail "message", sisome-commandrenvoie un état de sortie non nul, cela produit une trace de pile et quitte le shellSi vous voulez produire une trace de pile depuis une fonction puis retourner, vous pouvez faire
some-command || softfail "message" || return $?Il permet de faire ce dont on a besoin, mais il est rugueux et sa syntaxe est atroce. Quand un script atteint une certaine taille ou complexité, il vous force à passer à un vrai langage ; peut-être que c’est même une conception voulue
Sur une distribution moderne, on a généralement une version assez récente de Bash, et tant qu’on ne cherche pas à utiliser des choses comme les tableaux, il n’est pas vraiment nécessaire de se soucier de la version
L’attrait de Bash tient à la place qu’il occupe entre les autres langages et les outils. Il est idéal pour enchaîner d’autres outils, assez proche du système d’exploitation pour être pratique, et il n’exige pas d’installer des bibliothèques comme Python
On entend souvent dire qu’il faut migrer les scripts plus complexes vers un langage comme Python, mais cela ajoute une couche de complexité qui peut ne pas aider sur le long terme. Un script Bash écrit il y a 20 ans tourne encore très bien, alors qu’un programme Python vieux de 20 ans risque d’avoir des problèmes de version
Le
rcde Plan 9 est un peu plus propre, mais personne ne migre simplement pour quelque chose de « similaire, mais plus propre ». On peut dès maintenant installer quelque chose de similaire mais meilleur depuis https://pkgsrc.se/shells, et pourtant on ne l’utilise pas, et cela ne change pas non plus la façon dont les autres l’exécutentPour remplacer une technologie bien établie, il faut être plusieurs fois meilleur sur des aspects essentiels. Plan 9 est lui aussi meilleur que les systèmes de la famille UNIX, mais pas suffisamment pour les remplacer
Il est difficile de créer quelque chose d’assez bon pour remplacer la niche du scripting shell Bourne. Avant d’atteindre ce niveau, on bascule déjà dans la niche écologique ou le domaine de problèmes de vrais langages de script comme Perl, Python ou Ruby
Dans un domaine étroit, un optimum local aspire tout l’air, ce qui rend difficile l’émergence d’un concurrent proche de l’optimum global théorique
Les ajouts récents se sont bien greffés sur
sh/Bash, mais au fond, le scripting shell reste un moyen d’arriver à ses fins et doit évoluer bien plus lentement qu’un langage de programmation généralisteLa caractéristique essentielle de Bash/
sh, c’est son côté anti-entropique. Il y a très peu de développement ou d’évolution, donc peu de chances d’avoir des ennuis à cause de dépendances ou de nouvelles fonctionnalités, et ce qui fonctionnait il y a 20 ans reste dans les outils de basePar conception, cela en fait un système réticent au changement, et quand on en atteint les limites, cela incite les gens à aller voir ailleurs
La plupart des scripts de FreeBSD sont écrits pour
sh, et commeshfait partie de la norme POSIX, j’ai l’impression qu’il est bien plus largement pris en charge. Bash me semble simplement assez populaireMais Bash est tellement mauvais que j’ai créé tout un tas d’utilitaires avec des espaces de noms raccourcis pour pouvoir écrire des scripts Groovy. On peut développer avec un IDE, le système de bibliothèques est sûr, et Groovy gomme presque toutes les lourdeurs de Java, donc c’était bien mieux
Il a quelques limites, mais peut généralement être utile : https://github.com/ketancmaheshwari/pd
die()est bonne, mais Bash a une caractéristique agaçante. Si on tente de faire unexitdans un sous-shell, seul le sous-shell se termine et le reste du script continue à s’exécuterPar exemple, si on appelle
diedans un pipeline commecat myfile | while read line; do ... die "Found match" ... done, leecho "I don't want this line"qui suit sera quand même affichéOn peut souvent éviter les sous-shells, et dans cet exemple
shellchecka raison de signaler l’UUOC ; le corriger règle aussi le problème dediedans le sous-shellMais parfois, on ne peut pas éviter les sous-shells, ou les éviter rendrait le script trop complexe. Dans ce cas, on peut capturer le PID au début du script avec
MYPID=$$, puis tuer le script avec quelque chose commedie() { echo "$1" >&2; kill -9 $MYPID; exit 1; }Bien sûr, cela reste un compromis. Cette manière de tuer le processus est assez brutale et, sans que je sache pourquoi, elle ne s’est pas non plus révélée parfaitement fiable
set -epour que le script se termine aussi quand le sous-shell sort avec un code d’erreur non nulJ’ai du mal à imaginer une bonne raison de ne pas mettre
set -edans n’importe quel script shellset -euxo pipefailCela rend les tests conditionnels un peu plus difficiles, mais
pipefailà lui seul s’est déjà rentabilisé plusieurs foisset +xLe laisser activé en permanence devient assez vite fastidieux
En revanche, je garde
-xpour les moments où j’ai vraiment besoin de voir toute la sortie de débogage bien verbeuse