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 :

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 :

À 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.
À sa décharge, l'accusé a néanmoins une très bonne prise en charge des valeurs d'arguments, ce que ne fait pas ce script. Si vous préférez 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.