Grafana Alerting : notifications Telegram, Discord et Gotify pour ton homelab
Le monitoring sans alertes, c'est un dashboard que tu regardes uniquement quand tu penses à…
Un script bash qui marche, c'est facile. Un script qui échoue proprement, nettoie derrière lui et se relit dans six mois, ça demande cinq habitudes. Voici le squelette complet.
Un script bash, ça commence toujours pareil : on tape trois commandes dans un fichier, on le rend exécutable, et ça marche. Puis un jour, une des commandes échoue au milieu, le script continue quand même comme si de rien n’était, et il efface le mauvais dossier.
Ce n’est pas de la malchance, c’est le comportement par défaut de bash : il continue. Cet article présente le petit squelette que je recopie au début de chaque nouveau script, et surtout pourquoi chaque ligne est là.
Voici le fichier entier. On le décortique juste après.
#!/usr/bin/env bash
set -euo pipefail
NOM_SCRIPT="$(basename "$0")"
readonly NOM_SCRIPT
TMP_DIR="$(mktemp -d)"
log() {
printf '%s [%s] %sn' "$(date '+%F %T')" "$NOM_SCRIPT" "$*"
}
die() {
log "ERREUR : $*" >&2
exit 1
}
nettoyer() {
rm -rf "$TMP_DIR"
}
trap nettoyer EXIT
main() {
log "Démarrage"
command -v curl >/dev/null || die "curl n'est pas installé"
log "Travail dans $TMP_DIR"
# ... ici, le vrai travail du script ...
log "Terminé"
}
main "$@"
Copie-le dans un fichier exemple.sh, fais chmod +x exemple.sh, lance-le : il affiche deux lignes horodatées et nettoie son dossier temporaire.
#!/usr/bin/env bash
Cette ligne dit au système quel interpréteur utiliser. Avec env bash, on laisse le système retrouver bash où qu’il soit installé, ce qui évite de se tromper de chemin (/bin/bash, /usr/bin/bash…). Sans shebang, le script est lu par le shell de l’utilisateur qui le lance, et le comportement peut changer d’une machine à l’autre.
set -euo pipefail
C’est la ligne la plus importante. Trois options en une :
-e : le script s’arrête dès qu’une commande échoue. Sans ça, bash continue joyeusement.-u : utiliser une variable non définie est une erreur. Sans ça, rm -rf "$DOSSIER/" avec $DOSSIER vide devient rm -rf /… (bash a quelques garde-fous, mais ne comptons pas dessus).-o pipefail : dans commande1 | commande2, l’échec de commande1 compte. Sans ça, seul le code de retour de la dernière commande est pris en compte.Ce mode n’est pas magique. Quelques pièges à connaître :
# -e ne s'applique pas dans une condition : ceci ne stoppe pas le script
if grep -q "motif" fichier.txt; then
echo "trouvé"
fi
# Pour tolérer volontairement un échec, on le dit explicitement
grep "motif" fichier.txt || true
# Avec -u, une variable optionnelle se déclare avec une valeur par défaut
nom="${1:-inconnu}"
log et dielog() { printf '%s [%s] %sn' "$(date '+%F %T')" "$NOM_SCRIPT" "$*"; }
die() { log "ERREUR : $*" >&2; exit 1; }
Deux petites fonctions qui rendent les scripts lisibles :
log affiche un message horodaté. Quand un script tourne la nuit via cron, ces dates sont précieuses pour comprendre ce qui s’est passé et quand.die affiche l’erreur sur la sortie d’erreur (>&2) et s’arrête avec un code de retour non nul. Le code de retour est ce qui permet à cron, à systemd ou à un autre script de savoir que ça s’est mal passé.J’utilise printf plutôt que echo : son comportement est le même partout, alors que celui d’echo varie selon les shells quand le texte contient des antislashs ou commence par un tiret.
trap : nettoyer quoi qu’il arriveTMP_DIR="$(mktemp -d)"
nettoyer() { rm -rf "$TMP_DIR"; }
trap nettoyer EXIT
mktemp -d crée un dossier temporaire au nom unique (pas de collision si deux instances du script tournent en même temps). trap nettoyer EXIT demande à bash d’exécuter la fonction nettoyer à la sortie du script, qu’elle soit normale, causée par une erreur (set -e), ou par un Ctrl+C.
Résultat : plus de fichiers temporaires qui traînent dans /tmp après un plantage.
mainMettre le travail dans une fonction main appelée tout en bas (main "$@") a un avantage discret : bash lit le script entier avant de l’exécuter. Si tu modifies le fichier pendant qu’une instance tourne (ça arrive avec cron), le comportement reste cohérent. Et le fichier se lit de haut en bas comme un petit programme : les outils d’abord, la logique à la fin.
command -v curl >/dev/null || die "curl n'est pas installé"
command -v teste si une commande existe. Cinq lignes de vérification au début d’un script valent mieux qu’un plantage obscur à la ligne 80, après que le script a déjà fait la moitié du travail.
Même idée pour les fichiers et les dossiers dont dépend le script :
[[ -d "$SOURCE" ]] || die "dossier introuvable : $SOURCE"
[[ -r "$CONFIG" ]] || die "fichier de configuration illisible : $CONFIG"
Mets toujours tes variables entre guillemets doubles : "$variable", pas $variable. Sans guillemets, bash découpe le contenu aux espaces. Un fichier nommé mes photos.jpg devient deux arguments (mes et photos.jpg), et la commande ne fait plus du tout ce que tu veux. J’y consacre un article complet un peu plus loin dans cette série.
Deux réflexes avant d’exécuter un nouveau script :
bash -n exemple.sh # vérifie seulement la syntaxe, n'exécute rien
shellcheck exemple.sh # relit le script et signale les pièges classiques
Et pour comprendre ce qu’un script fait pas à pas :
bash -x exemple.sh # affiche chaque commande avant de l'exécuter
main, en le testant d’abord dans un dossier de test.shellcheck.scripts/ sous git. Un script qui n’existe que sur une machine est un script qu’on perdra.#!/usr/bin/env bash + set -euo pipefail : le strict minimum.log et die : des messages datés, et un vrai code de retour.trap … EXIT : on nettoie toujours derrière soi.main en bas du fichier, des vérifications en haut.shellcheck avant de lancer.Dans le prochain article, on rend le script configurable avec des arguments et des options, comme une vraie commande.
Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisis celui qui te convient.