Résolution des problèmes courants de configuration de scripts Bash
Maîtrisez l'art de résoudre les problèmes de configuration dans les scripts Bash. Ce guide détaille les techniques de débogage essentielles, en se concentrant sur les dépendances environnementales, les pièges syntaxiques courants comme les guillemets incorrects et la division des mots, ainsi que les échecs d'exécution critiques. Apprenez à utiliser des indicateurs robustes (`set -euo pipefail`), à gérer les erreurs d'analyse des arguments et à résoudre les problèmes courants tels que les fins de ligne DOS et les variables PATH incorrectes, garantissant ainsi que vos scripts d'automatisation s'exécutent de manière fiable dans n'importe quel environnement.
Résolution des problèmes courants de configuration de scripts Bash
Les problèmes de configuration Bash se manifestent généralement de manière vague : un script fonctionne depuis votre terminal mais échoue dans cron, un script de déploiement ne trouve pas kubectl, ou un chemin de fichier de configuration avec un espace ne fonctionne que pour un seul client. Le bogue ne se trouve souvent pas dans la logique principale. Il se trouve dans les hypothèses concernant l'environnement, les arguments, les guillemets, les permissions ou le shell qui a réellement exécuté le fichier.
Lorsque je résous un problème de script Bash, j'essaie d'abord de répondre à quatre questions : Quel shell l'exécute ? Quel environnement a-t-il reçu ? Quelles entrées a-t-il analysées ? Quelle commande a échoué en premier ? Cet ordre vous évite de courir après les symptômes.
Confirmer le shell et le contexte d'exécution
Un script qui commence par la syntaxe Bash mais s'exécute sous sh peut échouer de manière étrange. Les tableaux, [[ ... ]], source, la substitution de processus et set -o pipefail sont des fonctionnalités Bash. Si le fichier les utilise, le shebang doit indiquer Bash :
#!/usr/bin/env bash
Exécutez-le ensuite de la même manière que votre automatisation l'exécute. Ceci n'est pas équivalent :
./deploy.sh
bash deploy.sh
sh deploy.sh
./deploy.sh utilise le shebang. bash deploy.sh force Bash. sh deploy.sh peut utiliser dash, BusyBox ash ou un autre shell selon le système. Si la production appelle sh deploy.sh, un shebang Bash parfait n'aidera pas.
Cron, systemd, les exécuteurs CI, les commandes SSH forcées et les points d'entrée Docker fournissent tous des environnements différents. Un script qui fonctionne de manière interactive peut échouer car votre shell de connexion a défini PATH, AWS_PROFILE, NVM_DIR ou un gestionnaire de version de langage avant que vous ne l'exécutiez.
Ajoutez un bloc de diagnostic temporaire près du début :
printf 'shell=%s\n' "$BASH_VERSION" >&2
printf 'user=%s pwd=%s\n' "$(id -un)" "$PWD" >&2
printf 'PATH=%s\n' "$PATH" >&2
Supprimez ou conditionnez ceci une fois que vous avez la réponse. Les diagnostics sont utiles, mais la fuite de valeurs d'environnement dans les journaux peut exposer des secrets.
Utiliser le mode strict avec précaution, pas aveuglément
set -euo pipefail est une bonne valeur par défaut pour de nombreux scripts d'automatisation, mais il a des cas limites. set -u détecte les variables manquantes. pipefail rend visibles les échecs de pipeline. set -e s'arrête après de nombreux échecs de commande, bien qu'il se comporte différemment à l'intérieur des conditionnelles, des pipelines et des commandes composées que ce que les nouveaux utilisateurs de Bash attendent.
Un point de départ pratique est :
set -Eeuo pipefail
trap 'printf "Erreur à la ligne %s : %s\n" "$LINENO" "$BASH_COMMAND" >&2' ERR
Utilisez-le lorsqu'une commande échouée doit arrêter le script. Ne l'utilisez pas à la légère dans les scripts qui sondent intentionnellement des commandes et continuent. Pour les échecs attendus, écrivez la condition explicitement :
if ! grep -q '^enabled=true$' "$config_file"; then
printf 'La fonctionnalité est désactivée.\n'
fi
C'est plus clair que de laisser grep échouer sous set -e et de se demander pourquoi le script s'est arrêté.
Valider les arguments avant de lire les fichiers
Un bogue de configuration courant consiste à traiter $1 comme présent alors qu'il ne l'est pas. Sous set -u, référencer un $1 manquant entraîne une sortie immédiate. Sans set -u, il devient une chaîne vide.
Utilisez un petit bloc d'utilisation :
usage() {
printf 'Utilisation : %s <fichier-config> [environnement]\n' "${0##*/}" >&2
}
if (( $# < 1 )); then
usage
exit 2
fi
config_file=$1
environment=${2:-dev}
if [[ ! -r $config_file ]]; then
printf 'Le fichier de configuration n'est pas lisible : %s\n' "$config_file" >&2
exit 1
fi
Remarquez la valeur par défaut pour environment, mais pas pour config_file. Les valeurs par défaut sont utiles pour les valeurs optionnelles et dangereuses pour les valeurs obligatoires. Un script ne doit pas revenir silencieusement à ./config.yml pour un déploiement de production, sauf si ce comportement est très délibéré.
Mettre entre guillemets les chemins et les valeurs de la configuration
La plupart des scripts Bash lisent éventuellement un chemin à partir d'un fichier de configuration ou d'une variable d'environnement. Si cette valeur n'est pas entre guillemets, Bash effectue une division des mots et une expansion des globes.
backup_dir="/mnt/backups/May reports"
# Cassé : devient plusieurs arguments.
cp $backup_dir/latest.tar.gz /restore/
# Correct.
cp "$backup_dir/latest.tar.gz" /restore/
La même règle s'applique aux substitutions de commandes :
release_name=$(git describe --tags --always)
printf 'Déploiement de %s\n' "$release_name"
Si vous avez intentionnellement besoin de plusieurs arguments, utilisez un tableau au lieu d'une chaîne :
rsync_opts=(-a --delete --exclude '.git')
rsync "${rsync_opts[@]}" "$src/" "$dest/"
Cela évite le modèle fragile de opts="-a --delete" suivi de rsync $opts ....
Vérifier PATH et les dépendances de commandes externes
command not found est généralement un problème de contexte. Votre terminal peut trouver aws dans /opt/homebrew/bin/aws, alors que cron n'a que /usr/bin:/bin.
Au démarrage, vérifiez les outils requis :
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
printf 'Commande requise introuvable : %s\n' "$1" >&2
exit 127
}
}
require_cmd docker
require_cmd jq
require_cmd aws
Pour les utilitaires système critiques, les chemins absolus peuvent convenir. Pour les outils de développement installés à différents endroits, une vérification des dépendances avec un message d'erreur clair est généralement plus facile à maintenir.
Si un script est lancé par systemd, définissez l'environnement dans l'unité ou un fichier d'environnement au lieu de vous fier au .bashrc d'un utilisateur. Les shells non interactifs ne lisent pas nécessairement les mêmes fichiers de démarrage que votre terminal.
Analyser explicitement les variables d'environnement
La configuration pilotée par l'environnement est pratique, mais vide et non défini ne sont pas toujours la même chose. L'expansion des paramètres Bash vous permet d'être précis :
: "${APP_ENV:?APP_ENV doit être défini}"
log_level=${LOG_LEVEL:-INFO}
${APP_ENV:?message} échoue si la variable n'est pas définie ou est vide. ${LOG_LEVEL:-INFO} utilise une valeur par défaut si elle n'est pas définie ou vide. Si une chaîne vide a un sens dans votre script, utilisez les formes sans les deux-points, comme ${VAR-default}.
Évitez de vider tout l'environnement dans les journaux lors du dépannage. Il est trop facile d'imprimer des jetons, des mots de passe de base de données ou des informations d'identification cloud.
Surveiller les fins de ligne CRLF et les caractères invisibles
Un script édité sous Windows peut contenir des fins de ligne CRLF. Le symptôme classique est une erreur contenant ^M, ou un échec de shebang qui ressemble à un interpréteur inexistant.
Vérifiez avec :
file deploy.sh
sed -n 'l' deploy.sh | head
Corrigez avec l'une des commandes suivantes :
dos2unix deploy.sh
# ou, si dos2unix n'est pas disponible :
sed -i 's/\r$//' deploy.sh
Vérifiez également les valeurs de configuration copiées pour les espaces de fin. Une variable qui ressemble à prod mais qui est en fait prod peut manquer une branche case et vous faire tourner en rond.
Déboguer la première commande qui échoue
set -x affiche les commandes après expansion. C'est exactement ce dont vous avez besoin pour les bogues de guillemets et de configuration :
PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x
# section défaillante ici
set +x
N'activez pas xtrace autour des secrets. Si votre script gère des mots de passe, des jetons, des URL signées ou des clés privées, tracez uniquement la section étroite dont vous avez besoin.
Pour les fichiers de configuration, imprimez la valeur résolue et le test que vous êtes sur le point d'appliquer :
printf 'Utilisation de config_file=%q\n' "$config_file" >&2
[[ -r $config_file ]] || exit 1
%q est utile pour le débogage car il rend les espaces blancs visibles d'une manière compatible avec le shell.
Traiter les permissions comme une configuration également
Parfois, le script est correct, mais le compte qui l'exécute ne peut pas lire la configuration, exécuter l'assistant ou écrire le répertoire de sortie.
Vérifiez l'utilisateur réel :
id
namei -l "$config_file"
namei -l est particulièrement utile car chaque répertoire du chemin nécessite une permission d'exécution. Un fichier lisible à l'intérieur d'un répertoire parent inaccessible reste inaccessible.
Pour les scripts exécutables, définissez les permissions et les fins de ligne ensemble lors de l'empaquetage ou de la construction de l'image :
chmod 0755 /usr/local/bin/deploy
Si un script ne fonctionne qu'avec sudo, identifiez quel fichier ou commande nécessite un privilège. N'exécutez pas tout le script en tant que root simplement pour masquer une mauvaise définition de propriété.
Un passage de dépannage fiable
Lorsqu'un problème de configuration Bash n'est pas clair, effectuez ce passage dans l'ordre :
- Confirmez que le script s'exécute sous Bash s'il utilise des fonctionnalités Bash.
- Imprimez le répertoire de travail, l'utilisateur et
PATHpour le contexte défaillant. - Validez les arguments requis et les fichiers de configuration avant la logique principale.
- Mettez entre guillemets chaque expansion, sauf si vous voulez intentionnellement une division.
- Vérifiez les commandes externes requises avec
command -v. - Utilisez
set -xuniquement autour de la section défaillante, avec les secrets protégés. - Vérifiez les permissions et les fins de ligne avant de modifier la logique métier.
Cette séquence détecte la plupart des échecs du monde réel sans transformer le script en roman policier. Bash est petit, mais son contexte d'exécution est vaste ; dépannez d'abord le contexte.
Séparer le chargement de la configuration de l'exécution
Un script est plus facile à dépanner lorsque le chargement de la configuration est une étape distincte. Ne lisez pas un fichier, n'exportez pas de variables, ne créez pas de répertoires et ne redémarrez pas de services en un seul long bloc. Résolvez d'abord les valeurs. Validez-les ensuite. Puis exécutez le travail.
load_config() {
local file=$1
[[ -r $file ]] || {
printf 'Impossible de lire la configuration : %s\n' "$file" >&2
return 1
}
# Exemple pour un fichier KEY=VALUE délibérément simple.
# Ne sourcez pas les fichiers auxquels vous ne faites pas entièrement confiance.
while IFS='=' read -r key value; do
[[ -z $key || $key == \#* ]] && continue
case $key in
APP_PORT) APP_PORT=$value ;;
APP_ENV) APP_ENV=$value ;;
*) printf 'Ignorer la clé de configuration inconnue : %s\n' "$key" >&2 ;;
esac
done < "$file"
}
Sourcer un fichier de configuration avec . config.env est courant, mais cela exécute du code shell. Cela n'est acceptable que lorsque le fichier est fiable et possédé comme du code. Pour une configuration modifiable par l'utilisateur, analysez uniquement les clés que vous prenez en charge.
Rendre les échecs exploitables pour l'opérateur suivant
Un bon message d'erreur indique ce qui a échoué et quelle valeur en est la cause. Comparez ceci :
printf 'Erreur\n' >&2
et :
printf 'Impossible d'écrire le répertoire de sauvegarde : %s\n' "$backup_dir" >&2
Le deuxième message donne à la personne suivante quelque chose à vérifier. Cela compte dans les scripts DevOps car la personne qui voit l'échec n'est peut-être pas l'auteur. Elle peut être de garde, à moitié endormie et regarder les journaux CI d'un déploiement échoué.
Les codes de sortie peuvent également avoir une signification. Utilisez 2 pour les problèmes d'utilisation, 1 pour les échecs d'exécution généraux et les codes spécifiques à l'outil lorsque vous avez une raison documentée. Ne passez pas toute la journée à inventer une taxonomie, mais évitez de renvoyer un succès après une validation échouée simplement parce que le script a imprimé un avertissement.
Tester le contexte défaillant, pas votre contexte préféré
Si systemd exécute le script, testez avec systemd. Si cron l'exécute, testez avec un environnement dépouillé. Une approximation rapide est :
env -i HOME="$HOME" PATH=/usr/bin:/bin bash ./script.sh config.env
Cela supprime la couverture de sécurité de votre shell interactif. Les exportations manquantes et les hypothèses PATH apparaissent rapidement.
Pour les scripts de point d'entrée Docker, exécutez l'image avec le même environnement et les mêmes montages que la production aussi étroitement que possible :
docker run --rm --env-file app.env -v "$PWD/config:/config:ro" my-image:tag
S'il échoue uniquement dans CI, imprimez le répertoire de travail de l'exécuteur CI et la ligne de commande exacte. De nombreux échecs Bash CI sont simplement de mauvais chemins relatifs après le checkout, et non des problèmes de shell profonds.
Un passage de révision du monde réel avant de livrer
Avant de considérer un script ou une configuration de conteneur comme terminé, lisez-le une fois comme si vous étiez la personne suivante qui doit le déboguer à 2 heures du matin. Cela change ce que vous remarquez. Une invite qui avait du sens lors de l'écriture du script peut être ambiguë lorsqu'elle apparaît dans un journal CI. Un nom de service Docker qui semblait évident peut ne pas correspondre au nom de variable dans l'application. Une valeur par défaut Bash peut être sûre pour le développement et dangereuse pour la production.
J'aime faire un court essai à sec avec des valeurs délibérément gênantes. Utilisez un chemin avec des espaces. Utilisez une valeur optionnelle vide. Essayez un nom de fichier qui commence par un tiret. Exécutez le script à partir d'un répertoire de travail différent. Démarrez le conteneur sans une variable d'environnement attendue. Ces tests ne sont pas sophistiqués, mais ils détectent les hypothèses qui se brisent généralement en premier.
Vérifiez également le message d'échec. Si la seule sortie est échec, les conseils de l'article n'ont pas été intégrés dans l'implémentation. Un échec utile indique quelle valeur a été utilisée, quelle vérification a échoué et ce que l'opérateur peut modifier. Cela ne signifie pas vider chaque variable d'environnement ou imprimer des secrets. Cela signifie être spécifique là où la spécificité aide : le chemin de configuration, le nom de commande manquant, le nom du réseau, le nom d'hôte du service ou le port auquel le processus a tenté de se lier.
La dernière habitude est de garder les exemples proches de la façon dont le système est réellement exécuté. Si la production utilise Compose, testez avec Compose. Si un script est lancé par systemd, testez-le avec systemd ou avec un environnement tout aussi minimal. Si une commande est censée être sûre pour le copier-coller, incluez les guillemets, les séparateurs -- et la validation dans l'exemple lui-même. Les lecteurs copient les modèles de travail plus souvent qu'ils ne copient les avertissements.
Ce passage de révision n'est pas de la bureaucratie. C'est ainsi que la petite automatisation reste ennuyeuse. L'ennui est ce que vous voulez des invites shell, des chargeurs de configuration, de l'expansion des variables, des diagnostics de conteneur et de la mise en réseau Docker. Moins le comportement est surprenant, plus il est facile pour l'opérateur suivant de lui faire confiance.