Bash pour l'automatisation
Ce cours ouvre la partie Shell et outils du chapitre Fondations. Le cours Linux : premiers pas apprenait à taper des commandes ; celui-ci apprend à en faire des programmes : des scripts qui tournent la nuit sans personne pour les regarder, qui échouent bruyamment plutôt que de corrompre des données, et qu'un collègue peut relire, modifier et tester. Il s'adresse aux administrateurs et administratrices système, aux profils DevOps et SRE, et aux développeurs dont les pipelines de CI, les conteneurs et les tâches planifiées reposent sur des lignes de shell.
L'approche
On retrouve Signalements, l'API de signalement d'incidents de voirie, et sa machine de tâches de fond sig-outils (Debian 13). En la reprenant, vous découvrez les scripts laissés par Camille dans /opt/signalements/bin/ : l'export nocturne pour la mairie, la purge des pièces jointes, un script de déploiement vers sig-app-1 et sig-app-2, une vérification de santé. Ils fonctionnent, la plupart du temps. L'un casse dès qu'un nom de fichier contient une espace, un autre continue après une erreur et envoie un fichier vide à la mairie, un troisième laisse des fichiers temporaires à chaque interruption.
L'équipe décide de les ranger dans un dépôt Git, signalements-outils, et de les réécrire proprement. Chaque leçon apporte une pièce du langage au moment où un de ces scripts en a besoin, explique ce que Bash fait réellement de la ligne, et se termine par une version plus solide du script. À la fin, l'export est un vrai outil en ligne de commande, analysé par ShellCheck et couvert par des tests Bats en intégration continue.
Les exemples visent Bash 5.2, celui d'Ubuntu 24.04 LTS (5.2.21) et de Debian 13 (5.2.37). Les différences avec le shell POSIX sh (dash sur Debian et Ubuntu) sont signalées, parce qu'un script lancé par cron ou par /bin/sh n'est pas lancé par Bash.
Ce qu'il faut
- Avoir suivi Linux : premiers pas, ou en maîtriser le contenu, en particulier les leçons sur le shell, les redirections et tubes, l'environnement et les processus.
- Une machine Linux avec Bash 5, ou une machine virtuelle jetable Ubuntu 24.04 ou Debian 13, avec les paquets
shellcheck,batsetjq. Sous macOS, le/bin/bashlivré est une version 3.2 de 2007 : installez un Bash récent, sinon plusieurs exemples échoueront.
Les leçons
Les bases du langage
Structurer
- Boucles et lecture de données
- Fonctions et portée
- Tableaux indexés et associatifs
- Arguments, options et interface
Fiabiliser
- Gérer les erreurs
- Signaux, nettoyage et verrous
- Processus, parallélisme et délais
- Déboguer, analyser et tester
Pour valider : le quiz (niveau 100) et le lab (niveau 200).
Plan du cours
- Du terminal au script
100 Apprenti
Passer de la commande tapée au programme qui tourne sans vous : décider si un script Bash est le bon outil, choisir le shebang, comprendre les quatre façons de lancer un script (./, bash, sh, source) et pourquoi un script testé avec bash casse sous cron, écrire un en-tête utile, vérifier la syntaxe avec bash -n et passer ShellCheck, installer et versionner le script sans casser une exécution en cours ; sous le capot, la ligne #! lue par le noyau, le repli ENOEXEC du shell et la lecture du script par Bash. - Guillemets et expansions
100 Apprenti
Prévoir ce que Bash fait d'une ligne avant de l'exécuter : découpage en mots et opérateurs, ordre exact des expansions (accolades, tilde, paramètres, arithmétique, substitution de commande, découpage en mots selon IFS, chemins, suppression des guillemets), les trois mécanismes de citation et $'...', "$@" contre "$*", motifs sans correspondance (nullglob, failglob), noms de fichiers qui commencent par un tiret, citation pour un autre shell avec printf %q, et le bug des espaces qui faisait supprimer le mauvais fichier à la purge des pièces jointes. - Paramètres, chaînes et calculs
100 Apprenti
Tout ce que Bash sait faire d'une variable sans lancer un seul programme : affectation et attributs (readonly, declare), valeurs par défaut et variables obligatoires (${var:-}, ${var:=}, ${var:?}, ${var:+}), découpage de chemins et de noms (# ## % %%), remplacement et piège du & de Bash 5.2, casse, sous-chaînes, longueur en caractères et en octets, transformations @Q et @A, printf contre echo, dates avec %(...)T, arithmétique entière, piège des zéros en tête (08 et 09), décimales en virgule fixe ou avec awk, et l'injection de code par l'arithmétique. - Tests et conditions
100 Apprenti
Faire décider un script à partir de faits vérifiés plutôt que d'espoirs : le code de sortie comme booléen, if, elif et else, la différence entre test, [ et [[, comparaisons de chaînes et d'entiers, tests de fichiers et de variables, motifs et expressions régulières avec BASH_REMATCH, case et ses motifs, les opérateurs &&, || et !, la condition arithmétique (( )), puis la validation complète de l'export de Signalements et l'interprétation d'une réponse HTTP, sans oublier les injections par motif et par arithmétique. - Boucles et lecture de données
200 Compagnon
Parcourir des fichiers et des lignes sans rien casser : for sur des mots, des motifs, des intervalles et en style C, while et until, break et continue, lecture ligne à ligne avec while IFS= read -r, découpage en champs, dernière ligne sans saut de ligne et fins de ligne CRLF, tube et sous-shell qui perdent les variables, substitution de processus et lastpipe, noms de fichiers séparés par l'octet nul avec find -print0 et read -d '', nullglob, failglob, dotglob et globstar, mapfile, limites du CSV, et le moment où il faut passer à awk. Le script de purge des pièces jointes de Signalements est réécrit pour résister aux noms de fichiers hostiles. - Fonctions et portée
200 Compagnon
Découper un script en fonctions qui se lisent, se réutilisent et se testent : définition et appel, arguments, statut de retour contre sortie capturée et son coût, variables locales et portée dynamique de Bash, références de nom (declare -n) et collisions, fonctions qui masquent des commandes, command et builtin, bibliothèque lib/commun.sh chargée par un chemin relatif au script et protégée contre le double chargement, motif main "$@", pile d'appels avec FUNCNAME et caller, export -f et Shellshock. - Tableaux indexés et associatifs
200 Compagnon
Ranger plusieurs valeurs dans une seule variable sans les abîmer : tableaux indexés (création, ajout, longueur, indices, tableaux creux, suppression, tranches), différence entre "${t[@]}" et "${t[*]}", construction d'une ligne de commande dans un tableau plutôt que dans une chaîne ou par eval (options de ssh et d'aws, commande distante), tableaux associatifs et comptage des codes HTTP par hôte, ordre non garanti, test de présence d'une clé, passage d'un tableau à une fonction par nameref, set -u et tableaux vides en Bash 5.2, injection arithmétique par les indices, implémentation dans le code source de Bash et limites qui imposent jq ou Python. - Arguments, options et interface
200 Compagnon
Donner à un script une vraie interface en ligne de commande : paramètres positionnels, shift et set --, getopts et son mode silencieux, options longues écrites à la main (--opt valeur, --opt=valeur, options courtes groupées, --), getopt d'util-linux et ses limites, aide et --version, codes de sortie documentés, données sur la sortie standard et messages sur la sortie d'erreur, couleurs conditionnelles et NO_COLOR, priorité entre valeurs par défaut, fichier de configuration lu sans source, environnement et options, simulation et confirmation non interactive. - Gérer les erreurs
200 Compagnon
Faire échouer un script au bon moment, au bon endroit et avec le bon code : la sémantique exacte de set -e et la liste vérifiée des cas où il ne déclenche pas (conditions, listes && et ||, !, fonctions appelées dans un if, local qui masque une substitution, sous-shells et inherit_errexit, compteurs arithmétiques, erreurs d'expansion arithmétique qui échappent à set -e et au trap ERR), set -u, pipefail et le faux échec SIGPIPE de head, trap ERR et set -E pour afficher une pile d'appels, vérifications explicites, outils qui ne signalent pas leurs échecs (psql sans ON_ERROR_STOP, curl sans --fail, aws s3), et validation des résultats, jusqu'à corriger l'archive vide envoyée à la mairie. - Signaux, nettoyage et verrous
300 Expert
Faire qu'un script interrompu ne laisse rien derrière lui : qui envoie quels signaux à un script (Ctrl+C, coupure SSH, systemctl stop, kill, OOM), ce que Bash en fait par défaut, trap et le pseudo-signal EXIT, mktemp et un répertoire de travail toujours nettoyé, le piège différé tant qu'une commande tourne au premier plan, arrêter ses processus enfants, mourir du bon signal (128 + n), écrire atomiquement par renommage dans le même système de fichiers, idempotence, umask, et un verrou flock pris de l'intérieur du script avec ses fuites par héritage de descripteur. - Processus, parallélisme et délais
300 Expert
Savoir quels processus une ligne de Bash crée et ce qu'ils coûtent, puis s'en servir : sous-shells contre groupes d'accolades, $$ contre $BASHPID, coût d'un fork mesuré dans une boucle, tâches d'arrière-plan avec $!, wait, wait -n et wait -p, réserve de tâches bornée qui garde le code de chaque machine, xargs -P et GNU parallel, délais avec timeout (codes 124 et 137, -k, groupes de processus), SSH qui avale l'entrée standard d'une boucle, sorties entremêlées, coprocessus, et détacher un traitement avec systemd-run plutôt qu'avec nohup. Fil rouge : verifier-sante et deployer sur sig-app-1 et sig-app-2. - Déboguer, analyser et tester
300 Expert
Livrer des scripts Bash avec la même discipline que du code applicatif : vérifier la syntaxe avec bash -n, suivre l'exécution avec set -x, un PS4 qui donne le fichier, la ligne et la fonction, et BASH_XTRACEFD, sans divulguer de secret ; analyser le dépôt avec ShellCheck (gravités, codes fréquents, directives justifiées, .shellcheckrc, source-path et external-sources) ; formater avec shfmt ; tester publier-export avec Bats (run, $status, $output, $lines, setup, BATS_TEST_TMPDIR, bats-support et bats-assert) et une doublure de aws placée dans le PATH ; brancher le tout dans make verifier et un workflow GitHub Actions ; relire un script en revue de code et décider quand le réécrire en Python.