Les pièges courants du scripting Bash et comment les éviter
Évitez les bugs courants de la programmation Bash grâce à une gestion plus sûre des erreurs, des guillemets, des tableaux, des pièges et de l'analyse des arguments.
Pièges courants de la programmation Bash et comment les éviter
Les pièges de la programmation Bash apparaissent généralement lorsque votre script rencontre de vrais noms de fichiers, des variables manquantes, des commandes échouées ou des entrées inattendues. Un script qui fonctionne sur votre ordinateur portable peut casser en CI ou en production s'il repose sur des valeurs par défaut laxistes.
Vous n'avez pas besoin de rendre chaque script shell compliqué. Vous devez citer les expansions, vérifier les échecs intentionnellement et tester avec des noms contenant des espaces.
Définir des valeurs par défaut plus sûres avec soin
De nombreux scripts commencent par :
#!/usr/bin/env bash
set -euo pipefail
C'est une bonne base pour de nombreux scripts d'automatisation, mais chaque option a des angles morts :
set -ese termine lorsqu'une commande simple échoue, sauf dans des endroits comme les testsif, les parties des listes&&et||, et certaines substitutions de commandes.set -use termine lorsque vous développez une variable non définie.set -o pipefailfait échouer un pipeline si une commande du pipeline échoue, pas seulement la dernière commande.
Utilisez ces options lorsqu'un échec précoce est plus sûr que de continuer. Pour les commandes où l'échec est attendu, gérez le statut explicitement.
if ! grep -q "ready" status.txt; then
echo "le service n'est pas encore prêt"
exit 1
fi
Citer les expansions de variables
Les variables non citées sont le bug Bash le plus courant. Bash effectue le découpage de mots et l'expansion des globes sur les expansions non citées, donc un chemin comme release notes/*.txt peut devenir plusieurs arguments ou correspondre à des fichiers que vous n'aviez pas prévus.
file="release notes.txt"
# Mauvais : casse car la valeur est divisée en deux mots.
rm $file
# Bon : passe un argument exact.
rm -- "$file"
Utilisez -- avant les noms de fichiers contrôlés par l'utilisateur lorsqu'une commande le supporte. Cela empêche un nom de fichier comme -rf d'être interprété comme une option.
Utiliser des tableaux pour les listes d'arguments
Ne stockez pas une commande avec des arguments dans une seule chaîne et ne l'exécutez pas. La citation devient rapidement fragile.
# Mauvais
flags="-a --exclude node_modules"
rsync $flags "$src" "$dest"
# Bon
flags=(-a --exclude "node_modules")
rsync "${flags[@]}" "$src" "$dest"
Les tableaux préservent les limites des arguments. Cela compte lorsqu'un argument contient des espaces, des caractères génériques ou des valeurs qui commencent par un tiret.
Préférer $(...) aux backticks
Les backticks sont difficiles à imbriquer et faciles à mal lire. Utilisez $(...) pour la substitution de commandes.
current_branch="$(git rev-parse --abbrev-ref HEAD)"
echo "construction de la branche : $current_branch"
Gardez les substitutions de commandes citées sauf si vous voulez délibérément le découpage de mots.
Lire les fichiers sans perdre de données
Ce modèle semble inoffensif mais casse sur les espaces et peut déformer les barres obliques inverses :
for line in $(cat hosts.txt); do
echo "$line"
done
Lisez les fichiers avec while IFS= read -r à la place.
while IFS= read -r host; do
echo "vérification de $host"
done < hosts.txt
IFS= préserve les espaces de début et de fin. -r empêche l'interprétation des échappements par barre oblique inverse.
Gérer les fichiers temporaires avec mktemp et trap
Les chemins temporaires codés en dur peuvent entrer en collision avec un autre processus ou laisser des fichiers obsolètes derrière eux. Créez un chemin unique et nettoyez-le à la sortie.
tmp_file="$(mktemp)"
cleanup() {
rm -f "$tmp_file"
}
trap cleanup EXIT
printf '%s\n' "données de travail" > "$tmp_file"
Pour les répertoires, utilisez mktemp -d et supprimez le répertoire dans votre fonction de nettoyage.
Analyser les options avec getopts
L'analyse manuelle des arguments manque souvent de cas limites. Pour les options courtes, le getopts intégré de Bash est généralement suffisant.
verbose=false
output=""
while getopts ":vo:" opt; do
case "$opt" in
v) verbose=true ;;
o) output="$OPTARG" ;;
:)
echo "L'option -$OPTARG nécessite un argument" >&2
exit 2
;;
\?)
echo "Option inconnue : -$OPTARG" >&2
exit 2
;;
esac
done
shift "$((OPTIND - 1))"
getopts gère les drapeaux courts comme -v et -o file. Si votre script a besoin d'options longues comme --output, écrivez un analyseur soigneux ou utilisez un langage avec une bibliothèque d'analyse d'arguments plus robuste.
Vérifier les commandes qui peuvent échouer
Ne supposez pas qu'une commande a fonctionné parce qu'elle a affiché quelque chose. Vérifiez les opérations importantes avant d'utiliser leur sortie.
if ! archive="$(tar -czf app.tar.gz app 2>&1)"; then
echo "l'archive a échoué : $archive" >&2
exit 1
fi
Pour les pipelines, activez pipefail lorsqu'un échec au milieu doit faire échouer tout le pipeline.
set -o pipefail
journalctl -u api.service | grep -i "error"
Sans pipefail, le statut du pipeline provient normalement de la dernière commande.
Éviter Bash lorsque la portabilité compte
Si votre script utilise des tableaux, [[ ... ]], mapfile ou pipefail, c'est un script Bash. Commencez-le par :
#!/usr/bin/env bash
Si vous avez besoin de la portabilité POSIX sh, évitez les fonctionnalités propres à Bash et testez avec le shell que votre système cible utilise. N'écrivez pas un script Bash avec #!/bin/sh en espérant qu'il se comporte de la même manière partout.
À retenir
Le moyen le plus rapide d'améliorer vos scripts Bash est de les tester avec des entrées désordonnées : espaces dans les noms de fichiers, variables manquantes, fichiers vides et commandes échouées. Citez les expansions, utilisez des tableaux pour les listes d'arguments, nettoyez les fichiers temporaires avec trap et rendez les chemins d'échec explicites. Votre futur moi passera moins de temps à déboguer des scripts qui ne fonctionnaient que sur des entrées parfaites.