14 - Scripts bash : automatiser pour ne plus se répéter
Ce que tu vas apprendre
- Écrire un script bash correct avec le bon shebang
- Utiliser les variables, le quoting, les conditions et les boucles
- Gerer les erreurs avec les exit codes et set -euo pipefail
- Écrire des fonctions reutilisables
- Un vrai script de déploiement comme exemple concret
Prerequisites
Comprendre les variables d'environnement et etre a l'aise avec le terminal bash.
Pendant longtemps, j'ai écrit mes scripts bash comme on écrit un brouillon : vite, sans structure, sans gestion d'erreurs. Ca marchait jusqu'au jour ou un script de déploiement a continue a s'exécuter apres un échec de build et a ecrase la prod avec du code casse. Depuis, je prends le bash au serieux.
Le shebang : première ligne obligatoire
bash#!/bin/bash
# Ou mieux, pour la portabilite :
#!/usr/bin/env bash
La première forme suppose que bash est dans /bin/bash. La seconde cherche bash dans le PATH, ce qui marche sur plus de systèmes (notamment macOS ou bash peut etre ailleurs).
bash# Rendre le script executable
chmod +x mon-script.sh
# L'executer
./mon-script.sh
Variables et quoting
Le quoting en bash est une source infinie de bugs. La regle est simple : mets toujours tes variables entre guillemets doubles.
bash#!/usr/bin/env bash
# Assigner une variable (pas d'espace autour du =)
nom="Nicolas"
repertoire="/opt/mon app"
# Guillemets doubles : la variable est remplacee par sa valeur
echo "Bonjour $nom" # Bonjour Nicolas
# Guillemets simples : tout est litteral
echo 'Bonjour $nom' # Bonjour $nom
# Sans guillemets : DANGER avec les espaces
cd $repertoire # Erreur ! bash voit: cd /opt/mon app (deux arguments)
cd "$repertoire" # Correct : cd "/opt/mon app"
# Substitution de commande
date_du_jour=$(date +%Y-%m-%d)
echo "On est le $date_du_jour"
# Variables avec valeur par defaut
port="${PORT:-3000}" # utilise PORT si defini, sinon 3000
La regle d'or : "$variable" avec des guillemets doubles, toujours. Les seuls cas ou tu peux t'en passer sont les arithmetiques et les comparaisons a l'intérieur de [[ ]].
Conditions
bash#!/usr/bin/env bash
# Syntaxe moderne avec [[ ]] (prefere a [ ])
if [[ -f "/etc/nginx/nginx.conf" ]]; then
echo "Nginx est installe"
fi
# Tests sur les fichiers
[[ -f chemin ]] # le fichier existe
[[ -d chemin ]] # le repertoire existe
[[ -x chemin ]] # le fichier est executable
[[ -r chemin ]] # le fichier est lisible
# Tests sur les chaines
[[ -z "$var" ]] # la chaine est vide
[[ -n "$var" ]] # la chaine est non vide
[[ "$a" == "$b" ]] # egalite
# Tests numeriques
[[ "$count" -gt 10 ]] # greater than
[[ "$count" -lt 5 ]] # less than
[[ "$count" -eq 0 ]] # equal
# Combinaisons
if [[ -f ".env" ]] && [[ -n "$DB_HOST" ]]; then
echo "Config OK"
elif [[ -f ".env.example" ]]; then
echo "Copie .env.example vers .env"
cp .env.example .env
else
echo "Pas de fichier de config"
exit 1
fi
Boucles
bash#!/usr/bin/env bash
# For classique
for fichier in *.log; do
echo "Traitement de $fichier"
gzip "$fichier"
done
# For avec une sequence
for i in {1..5}; do
echo "Iteration $i"
done
# For sur la sortie d'une commande
for serveur in $(cat serveurs.txt); do
echo "Ping de $serveur"
ping -c 1 "$serveur"
done
# While : lire un fichier ligne par ligne (la bonne methode)
while IFS= read -r ligne; do
echo "Ligne: $ligne"
done < fichier.txt
# Until : repeter tant que la condition est fausse
until curl -s http://localhost:3000/health > /dev/null; do
echo "En attente du serveur..."
sleep 2
done
echo "Le serveur est pret"
Fonctions
bash#!/usr/bin/env bash
# Definir une fonction
log() {
echo "[$(date '+%H:%M:%S')] $*"
}
die() {
echo "ERREUR: $*" >&2
exit 1
}
check_command() {
command -v "$1" > /dev/null 2>&1 || die "$1 n'est pas installe"
}
# Utiliser
log "Demarrage du script"
check_command "node"
check_command "rsync"
log "Toutes les dependances sont presentes"
Les fonctions en bash n'ont pas de typage, pas de paramètres nommes. $1, $2, etc. sont les arguments. $* ou $@ represente tous les arguments. C'est rustique mais ca fait le travail.
Exit codes et gestion d'erreurs
Chaque commande retourne un code de sortie. 0 = succes, tout le reste = erreur :
bash# Verifier le code de sortie
grep "pattern" fichier.txt
echo $? # 0 si trouve, 1 si pas trouve
# Utiliser dans une condition
if grep -q "error" /var/log/app.log; then
echo "Des erreurs trouvees"
fi
set -euo pipefail : le filet de sécurité
C'est la ligne la plus utile que tu puisses ajouter a tes scripts :
bash#!/usr/bin/env bash
set -euo pipefail
# -e : arrete le script a la premiere erreur
# -u : erreur si une variable n'est pas definie
# -o pipefail : un pipe echoue si n'importe quelle commande du pipe echoue
Sans set -e, un script continue apres une erreur. C'est comme ca que tu te retrouves a déployer du code qui n'a pas compile. Sans set -u, rm -rf "$DOSSIER/" devient rm -rf / si DOSSIER n'est pas défini. Oui, ca arrive.
Exemple réel : script de déploiement
Voici un script que j'utilise sur paltemps.fr (simplifie pour l'article) :
bash#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/opt/paltemps"
BACKUP_DIR="/opt/backups"
BRANCH="main"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
die() {
log "ERREUR: $*" >&2
exit 1
}
# Verifier qu'on est root ou deploy
[[ "$(whoami)" == "deploy" ]] || die "Lance ce script en tant que deploy"
log "Debut du deploiement"
# Backup de la version actuelle
log "Backup de la version actuelle"
timestamp=$(date +%Y%m%d_%H%M%S)
tar -czf "$BACKUP_DIR/backup_$timestamp.tar.gz" -C "$APP_DIR" . \
|| die "Echec du backup"
# Pull des derniers changements
log "Pull depuis $BRANCH"
cd "$APP_DIR"
git fetch origin
git reset --hard "origin/$BRANCH"
# Installation des dependances
log "Installation des dependances"
bun install --frozen-lockfile || die "Echec de bun install"
# Build
log "Build de l'application"
bun run build || die "Echec du build"
# Redemarrage du service
log "Redemarrage du service"
sudo systemctl restart paltemps
# Verification
sleep 3
if curl -sf http://localhost:3000/health > /dev/null; then
log "Deploiement reussi"
else
log "Le health check a echoue, rollback"
tar -xzf "$BACKUP_DIR/backup_$timestamp.tar.gz" -C "$APP_DIR"
sudo systemctl restart paltemps
die "Rollback effectue"
fi
# Nettoyage des vieux backups (garder les 5 derniers)
ls -t "$BACKUP_DIR"/backup_*.tar.gz | tail -n +6 | xargs -r rm
log "Termine"
Ce script n'est pas parfait, mais il a les bases : gestion d'erreurs, backup, rollback, vérification. C'est dix fois mieux que git pull && bun run build && systemctl restart app.
Résumé
- Toujours
#!/usr/bin/env bashetset -euo pipefail - Guillemets doubles autour de toutes les variables :
"$var" [[ ]]pour les conditions (plus sur que[ ])- Fonctions
logetdiedans tous tes scripts - Les exit codes sont le mecanisme de communication entre commandes
Article précédent : Les variables d'environnement Article suivant : cron et les taches planifiees