Cette petite série est aussi disponible sur Github, avec le snippet du code commenté dans ce billet.
Bash n'est pas un langage facile.
Ou plutôt, il y est incroyablement facile d'y faire une erreur dont les conséquences seront catastrophiques. Écrire un script bash demande vite une énorme rigueur et des ruses de sioux. De fait, sa puissance est méconnue : Certains font même du TDD avec !
Pour l'instant, on va partir dans l'idée que vous avez un serveur applicatif professionnel, que des scripts tournent dessus (par exemple en cron), ou sont disponibles pour une administration en remote. On va voir comment rendre vos programmes plus pratiques et professionnels en y proposant une interface en ligne de commande digne de ce nom, avec des options dans le style POSIX.
Et on va se concentrer sur les options GNU-style, c'est à dire explicites. Parce que le one-letter porridge -lhAzkjfsoplS
… au secours.
Alors, de suite, une question : Pourquoi l'avoir fait en bash ?
Parce que des fois on ne maitrise absolument pas ce qui tourne sur l'ordinateur où il va être déployé, ou même son architecture (y'a pas que x86 dans la vie), et plutôt que réinventer un récupérateur de données via https:// ou écrire un parseur json, il est plus facile de demander à ce que deux/trois utilitaires standards soient installés. Et d'une manière étonnante, bash est souvent mésestimée. Oui, zsh et bien plus puissant, mais il ne fait pas tout, et n'est pas disponible partout.
TL;DR Montre ton code !
Attention : ce snippet doit être dans le code principal, pas en fonction. Le script doit absolument fonctionner en bash, précisez donc bien #!/bin/bash
dans votre entête.
set -e
while [ '-' == "${1:0:1}" ] ; do
case "${1}" in
-h|--help)
echo "${HELP}"
exit 0
;;
-u|--unlock)
_unlock_script
exit 0
;;
-v|--verbose)
VERBOSE="y"
option_sub_script="${option_sub_script} --verbose"
;;
-i|--interactive)
ALWAYS_AUTO="n"
option_sub_script="${option_sub_script} --interactive"
;;
--)
shift
break
;;
*)
echo "Invalid \"${1}\" option. See ${0} --help"
exit 1
;;
esac
shift
done
Simple et basique
set -e
forcera votre script bash à crasher au moindre bug. C'est un comportement nettement plus acceptable si vous faites des opérations destructives en fin de traitement, ou s'il manque un exécutable important. Sérieux, invoquez-le le plus souvent possible. Cette option est réversible avec set +e
, les autres options de bash devraient aussi vous intéresser.
J'encapsule et j'échappe toujours les chaines de caractères.
Quand j'utilise une variable, je fais toujours en sorte d'utiliser la notation échappée ${VAR}
, c'est un réflexe à prendre qui peut vous être bien utile.
Nommez explicitement vos variables, vos constantes et vos valeurs magiques, dit le mec qui utilise extensivement ${1}
dans cette démo.
Je préfixe les fonctions internes par un _. Parfois je le fais pas (voir le script de lock), quand c'est non pas le script principal, mais une inclusion d'utilitaires.
Le bash étant encore plus susceptible de vous faire des blagues à la moindre typo qu'un javascript, il est bienvenu dans une comparaison de mettre la constante comme premier argument. Donc if [ 'y' == ${VERBOSE} ] ; then
, ce qui se montre parfois plus lisible que l'inverse.
Soyez très discipliné avec l'indentation, sinon vous allez vous y perdre... La coloration syntaxique d'un IDE ou de vim ne fait pas tout.
Les arguments, ou paramètres d'une ligne d'appel d'un script shell, sont découpés par les espaces, et classés dans ${1}
, ${2}
, ${3}
... Ainsi dans truc.sh muche bidule
, ${2}
retournera bidule
Attention :
- Si vous êtes dans une déclaration de fonction, ces paramètres font référence à l'appel de la fonction,
${0}
est l'appel à votre programme,- Il y a une autre méthode,
getopt
, mais je l'ai écartée car elle incite à de mauvaise pratiques
Les options sont en début de train de paramétrage, on va donc faire une boucle tant qu'on a un argument dont le premier caractère commence par -
La notation étendue des variables permet des choses assez magiques comme la substitution ou encore de n'utiliser qu'une partie de la chaine de caractère. ${VAR:0:1}
est une manière habile de ne récupérer que le premier caractère de la variable ${VAR}
Boucle si jamais, tant que
:
While [ condition ] ; do
[…]
done
. dont on peut s'échapper avec break
. Ici, tant que le paramètre analysé a comme premier caractère un -
shift
est une commande qui décale/dépile les paramètres positionnels, à rapprocher dans l'idée d'un Array.pop()
dans tout langage plus structuré. Ainsi ${2}
devient ${1}
... Une fonction particulièrement magique pour faire des boucles sur les paramètres
case <var> in
est un switch
-like . À noter sa syntaxe très alambiquée, comme tout ce qui est un peu technique en bash. Ainsi, il se termine avec son mot clé renversé, esac
, comme fi
pour un if
... C'était quel personnage DC qui prononce des invocations à l'envers pour les annuler ?
Chaque cas commence par <regex>)
. Nous avons donc pour le premier cas une règle qui accepte -h
ou --help
, puisque |
est une alternative logique, un ou
.
La fin du cas est symbolisée par ;;
.
Le traitement d'un cas non expressivement décrit se fait via *)
, regex oblige ;)
Rédigez toujours un message d'aide en début de votre script. La documentation atomique toujours au plus près de votre code atomique, ce qui permet de le lier directement à vos commits git. Je le déclare en multiligne dans la variable HELP
, que je ressors dans le programme avec l'option --help
, ou -h
pour les personnes pressées.
Inspirez-vous des autres programmes pour faire une jolie sortie d'aide en ligne, avec la mise en forme, les rubriques et les chapitres les plus standards possibles. Plus elle informera l'utilisateur, moins vous chercherez rageusement à faire un man
, un help
, un lynx bing.com/search?q=site:stackoverflow
...
Avec ça, vous pourrez vous passer de créer une manpage, un .rst spécifique et autre help files exotiques.
option --unlock
parce que si votre script est global, pensez toujours à mettre un lock pour éviter une désastreuse exécution concurrentielle. J'ai expliqué précédemment comment. L'intérêt de cette option est de ne pas avoir à chercher comme un fou où est ce foutu satané lock s'il n'est pas dans /var/run
.
option --verbose
parce que si votre script est assez tarabiscoté, pensez à documenter ce que fait chaque fonction dans la sortie standard, et à passer ce paramètre à chaque programme important que vous utilisez
option --interactive
pour que le pupitreur puisse valider ou sauter des étapes internes à votre script sans pleurer à tout commenter. Voici typiquement l'invite que cela produit sur mes scripts d'administrations :
Dump de la base vers /backup/2018-01-05-07h15.sql : [E]xécuter, [S]auter, [C]ontinuer automatiquement, [Q]uitter ?
Croyez-moi que mes collègues me bénissent quand ils découvrent cette option.
option --
, une excellente convention des utilitaires GNU pour éviter qu'un fichier qui s'appelle --truc
ou -h
ne vous mettent un bazar incroyable dans votre ligne de commande parce qu'elle est interprétée comme une option. Oui, cela arrive. Méfiez-vous des rm *
dans un répertoire avec beaucoup de fichiers. #protip
Je sors de ma boucle while
avec un break
en toute tranquillité.
Plantez à la moindre option d'appel inconnue, sauf en mode --help
.
Pour terminer votre script, exit
accepte un paramètre numérique pour informer d'une fin normale ou malheureuse :
- 0 si tout va bien
- Tout autre nombre, mais de préférence entre 1 et 255, si vous avez un bug ce qui fera aussi planter un éventuel script bash parent si
set -e
est présent. La signification de ces valeurs magique manque singulièrement de standardisation.
À améliorer
Le code actuel ne gère pas les options paramétriques du type --verbose=8
J'ai volontairement écarté pour cet exemple getopt
car
- il n'est pas toujours facile à lire par tous les devs,
- sa syntaxe incite plus à écrire des options en un caractère, et donc d'être beaucoup moins explicite.
getopt
, je vous recommande cette excellente ressource en français.
L'ensemble est très nettement perfectible, j'en suis absolument sûr. Mais pour les antiques
scripts init, cette construction était parfaite.
4 réactions
1 De Sileht - 08/01/2018, 20:51
Mieux que getopts de Bash y'a getopt (sans s). Ç'est très ressemblant à ta solution:
https://bioinfo-fr.net/astuce-ajout...
2 De Da Scritch - 09/01/2018, 07:33
quel "s" ???
Je trouve getopt illisible dans sa syntaxe. Je l'ai pratiqué et sur le long terme, les déclarations en vrac qui font doublon avec ton case, je trouve ça un peut lourd à maintenir.
3 De Simon - 11/01/2018, 06:13
Pour ma part, je suis assez fan de shFlags : https://github.com/kward/shflags
- gestion options longues et courtes
- déclaration des options très simple
- gestion automatique des booléen (avec le no[option] automatiquement)
- documentation automatique
Tous mes scripts depuis 2009 utilise ça :)
4 De Da Scritch - 09/11/2022, 07:11
Un article ultra passionnant avec une belle liste d'astuces bash https://sharats.me/posts/shell-script-best-practices/