Aller au contenu

Quiz : Bash pour l'automatisation

100 Apprenti ⏱ 45 min bashlinuxshellcheck

Ce quiz valide le niveau 100 (Apprenti) des notions du cours Bash pour l'automatisation. Visez au moins 19 bonnes réponses sur 24 ; chaque réponse renvoie à la leçon à relire. Le niveau 200 se valide avec le lab.

Toutes les questions supposent Bash 5.2, celui d'Ubuntu 24.04 et de Debian 13, et un script lancé depuis un fichier (pas tapé au terminal).

Les bases du langage

1. Le script publier-export de Camille commence par #!/bin/bash et contient des [[ ]]. Il fonctionne quand on le lance à la main, mais la ligne de crontab 30 5 * * * sh /opt/signalements/bin/publier-export produit un résultat faux, sans erreur visible. Pourquoi ?

  • a) cron ne lit pas la ligne #! des scripts
  • b) sh script lance Dash, qui ignore la ligne #! (ce n'est qu'un commentaire pour lui) et exécute le script avec ses propres règles, où [[ n'existe pas
  • c) Le PATH de cron ne contient pas /bin
  • d) Bash refuse de s'exécuter sans terminal
Réponse

b. La ligne #! n'est lue que par le noyau, quand on exécute le fichier lui-même (./script ou son chemin). sh script demande explicitement à /bin/sh, c'est-à-dire Dash sur Debian et Ubuntu, de lire le fichier. Un bashisme peut alors échouer, ou pire, s'exécuter de travers avec un code 0. Lancez le script par son chemin, et vérifiez la portabilité avec shellcheck -s dash si un script doit tourner sous sh. Leçon 1.

2. Le script est rangé dans /srv/donnees/outils/, son droit x est bien positionné, et pourtant ./publier-export répond Permission denied (code 126). Quelle est la cause la plus probable sur sig-outils, et que faire ?

Réponse

/srv/donnees est monté avec l'option noexec : le noyau refuse d'exécuter tout fichier de ce système de fichiers, quel que soit son droit x. C'est voulu pour un volume de données. On installe les scripts dans /opt/signalements/bin/ (ou /usr/local/bin), avec install -o root -g root -m 0755, qui crée un nouveau fichier au lieu de réécrire celui qu'un processus est peut-être en train de lire. Leçon 1.

3. Le script de purge de Camille contient rm -f $f. Un fichier se nomme photo du 12 mars.jpg. Que reçoit rm ?

  • a) Un argument : photo du 12 mars.jpg
  • b) Quatre arguments : photo, du, 12, mars.jpg, en plus de -f
  • c) Une erreur de syntaxe avant l'exécution
  • d) Rien : Bash ignore les noms qui contiennent des espaces
Réponse

b. Le résultat d'une expansion non protégée passe par le découpage en mots selon IFS (espace, tabulation, saut de ligne), puis par l'expansion des chemins. rm tente de supprimer quatre fichiers qui n'existent peut-être pas, ou, s'ils existent, des fichiers sans rapport. Écrire rm -f -- "$f" : les guillemets doubles empêchent découpage et expansion des chemins, -- protège contre un nom qui commencerait par un tiret. ShellCheck signale l'oubli (SC2086). Leçon 2.

4. Après set -- "a b" c, combien d'arguments reçoit printf '<%s>' dans chacun de ces cas : $@, "$@", "$*" ?

Réponse

Trois (<a><b><c>), deux (<a b><c>) et un (<a b c>). "$@" transmet les arguments intacts, un mot par argument : c'est presque toujours la forme voulue. "$*" les joint en une seule chaîne (avec le premier caractère d'IFS). Sans guillemets, $@ et $* redécoupent tout. Leçon 2.

5. Pour ranger le récapitulatif mensuel, un script calcule le mois précédent par precedent=$(( mois - 1 )), où mois vient de date +%m. Que se passe-t-il le 1er septembre, avec mois=09, dans un script lancé normalement par Bash ?

  • a) precedent vaut 8
  • b) Le script s'arrête avec le code 1
  • c) Bash affiche value too great for base, abandonne la commande, et le script continue avec precedent vide, même sous set -e
  • d) precedent vaut 0
Réponse

c. Un nombre qui commence par 0 est lu en octal, et 9 n'est pas un chiffre octal. Hors mode POSIX, une erreur d'expansion arithmétique ne termine pas un script : Bash abandonne la commande en cours et continue, et set -e ne l'intercepte pas. Le récapitulatif part alors vers un chemin comme 2026//. Il faut forcer la base 10 : $(( 10#$mois - 1 )), et valider toute valeur avant de calculer avec. Leçons 3 et 9.

6. Pourquoi écrire rm -rf -- "${REPERTOIRE_EXPORTS:?}/archives" plutôt que rm -rf -- "$REPERTOIRE_EXPORTS/archives" ?

Réponse

Si la variable est absente ou vide (faute de frappe, fichier de configuration non lu), la seconde forme devient rm -rf -- /archives, un chemin à la racine. ${VAR:?} arrête le script avec un message dès que la variable est vide ou absente, avant que rm ne soit lancé. C'est la protection minimale devant toute commande destructrice qui compose un chemin. Leçon 3.

7. Ce test doit vérifier qu'une variable n'est pas vide :

if [ -n $destination ]; then
  deposer
fi

Que se passe-t-il quand destination est vide, et comment l'écrire correctement ?

Réponse

Le test est vrai. [ est une commande : la variable vide non citée disparaît, [ ne reçoit plus que -n et ], soit un test à un seul argument, « la chaîne -n est-elle non vide ? », toujours vrai. Le dépôt part avec une destination vide. Écrire [ -n "$destination" ], ou [[ -n $destination ]] : [[ ]] est un mot-clé du shell, qui ne découpe pas les variables. Leçon 4.

8. Le script valide une date avec [[ $date_export =~ "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ]]. Toutes les dates correctes sont refusées. Pourquoi ?

  • a) Bash ne connaît pas les accolades {4} dans une expression régulière
  • b) Le membre droit de =~ entre guillemets est comparé comme un texte littéral, pas comme une expression régulière
  • c) Il faut == et non =~
  • d) ^ et $ sont interdits dans [[ ]]
Réponse

b. Les parties citées du membre droit de =~ correspondent littéralement (SC2076). On place l'expression dans une variable, utilisée sans guillemets : motif='^[0-9]{4}-[0-9]{2}-[0-9]{2}$' puis [[ $date_export =~ $motif ]]. La forme ne suffit pas : 2026-02-30 passe le motif, d'où la vérification complémentaire par date -d. Leçon 4.

Structurer

9. Ce rapport affiche toujours 0 lignes. Pourquoi, et comment le corriger sans fichier temporaire ?

n=0
grep 'python3\[' "$journal" | while IFS= read -r ligne; do
  n=$(( n + 1 ))
done
echo "$n lignes"
Réponse

Chaque élément d'un tube s'exécute dans un sous-shell : la boucle incrémente une copie de n, perdue à la fin du tube. On garde la boucle dans le shell courant avec une substitution de processus : while IFS= read -r ligne; do ...; done < <(grep 'python3\[' "$journal"), ou avec shopt -s lastpipe dans un script. Pour compter, grep -c ou un awk suffiraient d'ailleurs. Leçon 5.

10. Le répertoire /srv/donnees/exports est vide. Combien de fois ce corps de boucle s'exécute-t-il, et que vaut f ?

for f in "$REPERTOIRE_EXPORTS"/*.csv; do
  traiter "$f"
done
Réponse

Une fois, avec f égal au motif lui-même, /srv/donnees/exports/*.csv : un motif sans correspondance reste littéral. traiter reçoit donc un nom de fichier qui n'existe pas. Parades : shopt -s nullglob (le motif disparaît, zéro tour), ou [[ -e $f ]] || continue en tête de boucle. Leçon 5.

11. Pourquoi local nb=$(compter_lignes "$csv") rend-il le contrôle d'erreur inopérant, et comment l'écrire ?

Réponse

Le code de sortie de la ligne est celui de la commande interne local, qui réussit toujours ; l'échec de compter_lignes est masqué, y compris pour set -e (SC2155). On déclare d'abord, on affecte ensuite : local nb puis nb=$(compter_lignes "$csv"). Même règle pour export, readonly et declare. Leçons 6 et 9.

12. Dans deployer, une boucle for ((i = 0; i < 2; i++)) parcourt les machines et appelle preparer_hote, qui contient sa propre boucle for ((i = 0; i < 3; i++)) sans local i. Que se passe-t-il ?

  • a) Bash crée automatiquement une variable i propre à la fonction
  • b) La fonction modifie le i de l'appelant : à son retour, i vaut 3, la boucle externe s'arrête après la première machine
  • c) Une erreur « variable already defined »
  • d) La boucle externe recommence à zéro
Réponse

b. Les variables de Bash sont globales par défaut, et une fonction voit et modifie celles de son appelant. Ici la seconde machine n'est jamais déployée, sans aucun message. Déclarez local toutes les variables d'une fonction, compteurs de boucle compris. Même avec local, la portée de Bash est dynamique : une fonction voit aussi les variables locales des fonctions qui l'ont appelée. Leçon 6.

13. Pourquoi construire la commande de dépôt dans un tableau plutôt que dans une chaîne ?

# Version A
options="--only-show-errors --endpoint-url $POINT_ACCES"
aws s3 cp $options "$archive" "$cible"

# Version B
aws_s3=(aws s3 cp --only-show-errors --endpoint-url "$POINT_ACCES")
"${aws_s3[@]}" "$archive" "$cible"
Réponse

Dans A, $options non cité est redécoupé selon les espaces et soumis à l'expansion des chemins : une valeur qui contient une espace ou un * casse la commande, et citer "$options" donnerait un seul argument. Dans B, chaque élément du tableau reste un argument, quelles que soient les valeurs, et l'on peut ajouter des options conditionnelles avec +=. C'est l'alternative propre à eval, qui relirait les données comme du code. Leçon 7.

14. rapport-journaux compte les codes HTTP par machine dans declare -A compte. Pourquoi le rapport affiche-t-il les machines dans un ordre qui change, et pourquoi (( compte[$h]++ )) est-il dangereux sous set -e ?

Réponse

Un tableau associatif est une table de hachage : l'ordre de "${!compte[@]}" n'est pas garanti. On trie (sort) ou l'on garde l'ordre dans un tableau indexé à côté. Quant à (( x++ )), il vaut l'ancienne valeur : au premier comptage, elle vaut 0, donc le code de sortie est 1, et set -e arrête le script. Écrire compte[$h]=$(( ${compte[$h]:-0} + 1 )) ou (( ++compte[$h] )). Leçons 7 et 9.

15. publier-export analyse ses options avec getopts ":nd:v". Un collègue lance publier-export --date=2026-10-07. Que se passe-t-il ?

  • a) getopts refuse l'option longue avec un message clair
  • b) getopts découpe --date=... comme un groupe d'options courtes : - (inconnue), puis -d avec la valeur ate=2026-10-07
  • c) getopts reconnaît --date comme -d
  • d) L'argument est ignoré et la date du jour est utilisée
Réponse

b. getopts ne connaît que les options courtes de POSIX. Selon la façon dont la boucle traite le ?, le script signale une option inconnue ou continue avec une date absurde. C'est pourquoi le cours retient, pour un outil d'équipe, une boucle while (( $# > 0 )); do case $1 in ... esac; shift; done, qui gère --date valeur, --date=valeur, -d valeur et --, puis valide chaque valeur. Leçon 8.

16. Pour quelles raisons publier-export lit-il /etc/signalements/publier-export.conf ligne à ligne avec read, plutôt que de le charger par source ?

Réponse

source exécute le fichier avec les droits du script : une ligne comme DESTINATION=$(commande) ou PATH=/tmp serait appliquée, et une faute de syntaxe ferait échouer le script. Lu comme une donnée (IFS='=' read -r cle valeur), le fichier ne fournit que des valeurs littérales, pour une liste blanche de clés ; une clé inconnue produit un avertissement avec le numéro de ligne, ce qui attrape aussi les fautes de frappe. Leçon 8.

Fiabiliser

17. Avec set -e, ce script continue après l'échec de false et affiche suite. Pourquoi ?

set -e
verifier() {
  false
  echo "suite"
}
if verifier; then
  echo "vérifié"
fi
Réponse

set -e est suspendu dans tout contexte de condition : test d'un if ou d'un while, membres non finaux de && et ||, commande précédée de !. Ce contexte est contagieux : une fonction appelée dans un if s'exécute entièrement sans errexit. Ici verifier renvoie le code de echo, 0, et le script affiche aussi « vérifié ». Une fonction doit vérifier ses propres étapes (false || return) au lieu de compter sur set -e. Leçon 9.

18. Sous set -o pipefail, la ligne entete=$(gzip -dc -- "$archive" | head -n 1) échoue certaines nuits avec le code 141. Expliquez.

Réponse

head se termine après la première ligne. Si gzip a encore des données à écrire (archive plus grosse que le tampon du tube), il reçoit SIGPIPE et meurt avec 128 + 13 = 141. Avec pipefail, cet échec devient celui du tube, et selon la taille de l'export du jour, le script s'arrête ou non. Corrections : lire sans tube (IFS= read -r entete < <(gzip -dc -- "$archive")) après un gzip -t, ou accepter 141 pour l'écrivain seul. Leçon 9.

19. Pourquoi le piège de nettoyage doit-il être écrit entre apostrophes, trap 'rm -rf -- "$rep_travail"' EXIT, et non entre guillemets doubles ?

Réponse

Entre guillemets doubles, $rep_travail est développé au moment du trap : si la variable est encore vide, le piège ne supprimera rien ; si elle change ensuite, le piège visera l'ancienne valeur (SC2064). Entre apostrophes, l'expansion a lieu quand le piège s'exécute. La variable doit en outre être globale : une variable local de main a disparu quand le piège s'exécute après une fin normale. Leçon 10.

20. systemctl stop signalements-publication.service arrive pendant un dépôt de deux minutes lancé au premier plan par aws s3 cp. Le script a un trap sur TERM. Quand le piège s'exécute-t-il, et comment rendre le script interruptible tout de suite ?

Réponse

Bash diffère le piège jusqu'à la fin de la commande au premier plan : il s'exécute après le dépôt, ou jamais si systemd envoie SIGKILL avant (90 secondes par défaut). On lance la commande en arrière-plan et on l'attend : aws s3 cp ... & enfant=$!; wait "$enfant". wait est interrompu par le signal, le piège s'exécute aussitôt, arrête l'enfant par son PID, nettoie, puis se renvoie le signal (trap - TERM EXIT; kill -s TERM "$$") pour que l'appelant voie 143. Leçon 10.

21. Deux exécutions de publier-export ne doivent jamais tourner en même temps. Pourquoi flock vaut-il mieux qu'un fichier .pid ou qu'un « si le fichier verrou existe, sortir » ?

  • a) flock est plus rapide
  • b) Le verrou est tenu par le noyau sur un fichier ouvert : il est pris de façon atomique et libéré automatiquement à la mort du dernier processus qui le détient, même tué par SIGKILL
  • c) flock fonctionne aussi entre plusieurs machines
  • d) Un fichier .pid est interdit sous systemd
Réponse

b. Un test « existe, sinon créer » laisse une fenêtre entre le test et la création, où deux exécutions passent ; et un fichier laissé par un processus tué bloque toutes les suivantes. Avec exec {fd}>>"$verrou"; flock -n "$fd" || mourir -c "$EX_VERROU" ..., l'échec donne le code 5. Le verrou suit la description de fichier ouverte : un enfant détaché en hérite et le garderait, d'où {fd}>&- pour ne pas le lui transmettre. Leçon 10.

22. verifier-sante interroge les machines en parallèle :

for h in sig-app-1 sig-app-2; do
  verifier_une "$h" &
done
wait
echo "code=$?"

sig-app-2 est en panne, et pourtant le script affiche code=0. Pourquoi, et comment récupérer le code de chaque machine ?

Réponse

wait sans argument attend toutes les tâches et renvoie toujours 0. On range chaque PID aussitôt après le & (pids[$h]=$!), puis on attend chaque PID : code=0; wait "${pids[$h]}" || code=$?, et l'on compte les échecs. wait -n sert à attendre qu'une place se libère dans une réserve de tâches bornée, pas à recueillir les codes : en Bash 5.2, une tâche déjà notifiée (tuée par un signal, par exemple) peut lui échapper. Leçon 11.

23. Une boucle while read -r hote; do ssh "deploiement@$hote" ...; done < hotes.txt ne traite que la première machine de la liste. Pourquoi, et quelles sont les deux corrections ?

Réponse

ssh lit son entrée standard pour la transmettre à la commande distante : il consomme le reste de hotes.txt, et read ne trouve plus rien au tour suivant. Corrections : ssh -n (ou < /dev/null) pour fermer l'entrée de ssh, ou lire la liste sur un autre descripteur (while read -r -u 3 hote; do ...; done 3< hotes.txt). Dans un script, ajoutez aussi -o BatchMode=yes et un délai de connexion. Leçons 5 et 11.

24. Après une modification, publier-export ne détecte plus les exports vides : il tente le dépôt et sort avec le code 4 au lieu de 3. Pourtant ce test Bats passe toujours. Pourquoi, et comment l'écrire ?

@test "un export vide est refusé" {
  : > "$BATS_TEST_TMPDIR/exports/signalements-2026-10-07.csv"
  run publier-export -d 2026-10-07
}
Réponse

run capture le code et la sortie de la commande dans $status et $output, et réussit toujours : un test qui s'arrête après run ne vérifie rien. On ajoute les assertions, sur le code et sur le message : run -3 publier-export -d 2026-10-07 (avec bats_require_minimum_version 1.5.0), puis [[ $output == *vide* ]] ou assert_output --partial vide avec bats-assert. Pour s'assurer qu'un test peut échouer, on casse exprès le contrôle dans le script et l'on vérifie qu'il devient rouge (test de mutation). Leçon 12.

Plan du cours

    +50 XP 100 Apprenti

    Validation sur l'honneur, enregistrée dans ce navigateur. Elle valide le niveau 100 de Scripts Bash et fait monter en rareté l'équipement de votre cosmonaute.