Retour au cours

infra / linux-bash

Scripting bash avancé (arrays, fonctions, trap, set -euo pipefail)

Leçon 221 exercice

Explication

Par défaut, bash continue d'exécuter un script même après qu'une commande a échoué : si cd /dossier/inexistant échoue, les commandes suivantes s'exécutent quand même, dans le mauvais dossier. Sur un script lancé une fois à la main, ce n'est pas grave ; sur un script de déploiement automatique, ça peut casser un serveur en production.

Ce que vous allez apprendre

  • Comprendre ce que change exactement set -euo pipefail en tête de script
  • Garantir un nettoyage systématique avec trap ... EXIT, même en cas d'erreur
  • Structurer un script en fonctions réutilisables avec validation d'arguments
  • Manipuler des tableaux indexés et des tableaux associatifs (declare -A)
  • Utiliser l'expansion de paramètres (${var#...}, ${var:-défaut}) pour éviter sed/cut

Dans quel contexte ?

Une équipe DevOps écrit un script de déploiement qui doit gérer plusieurs services (api, worker, scheduler) avec des ports différents, tourner sans surveillance humaine via une pipeline CI, et ne jamais laisser de fichier temporaire orphelin sur le disque même s'il échoue en cours de route. Un script bash "simple" écrit sans ces techniques échouerait silencieusement ou laisserait le serveur dans un état incohérent.

Voici comment on corrige ce comportement dès la première ligne

set -euo pipefail change trois choses d'un coup :

OptionEffet
-eArrête le script à la première commande qui échoue
-uErreur si une variable non définie est utilisée
-o pipefailUn pipe échoue si N'IMPORTE QUELLE commande de la chaîne échoue, pas juste la dernière

C'est devenu le standard de fait en tête de tout script bash sérieux.

Bonne pratique

Commence systématiquement tes scripts de production par #!/usr/bin/env bash suivi de set -euo pipefail. C'est la garantie la plus simple et la plus efficace qu'une erreur ne passera jamais inaperçue en silence.

Une fois cette base posée, il reste un problème : le nettoyage en cas d'échec

Si un script crée un fichier temporaire avec mktemp puis plante avant la fin, ce fichier reste sur le disque indéfiniment. trap cleanup EXIT associe une fonction de nettoyage à la fin du script, qu'il réussisse, échoue, ou soit interrompu par un Ctrl+C — un peu comme un finally dans d'autres langages.

Maintenant que les erreurs sont gérées, structurons le script lui-même

Une fonction comme deployer() avec local environnement="${1:?Usage: deployer <env>}" rend le script lisible et échoue avec un message clair si l'argument attendu manque. Un tableau comme services=("api" "worker" "scheduler") permet de boucler avec for service in "${services[@]}" plutôt que de dupliquer du code trois fois.

declare -A ports puis ports[api]=8000 associe un nom à une valeur, comme un dictionnaire, ce qui remplace avantageusement plusieurs variables séparées (port_api, port_worker...).

Piège fréquent

Oublier les guillemets autour d'une variable dans for service in "${services[@]}" peut casser le script si un élément contient un espace : les guillemets ne sont jamais optionnels avec les tableaux, contrairement à ce qu'on pourrait croire pour une simple boucle.

Maintenant que tu écris des scripts robustes, la prochaine leçon montre comment déboguer ceux qui ne se comportent pas comme prévu.

Commandes & code

Scripting bash avancé

bash
#!/usr/bin/env bash
set -euo pipefail
# -e : arrête le script à la première commande qui échoue
# -u : erreur si une variable non définie est utilisée
# -o pipefail : un pipe échoue si N'IMPORTE QUELLE commande de la chaîne échoue
IFS=$'\n\t'    # évite les surprises de word-splitting sur les espaces

# --- Fonctions ---
log() {
    local niveau="$1"; shift
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$niveau] $*"
}

deployer() {
    local environnement="${1:?Usage: deployer <env>}"   # erreur explicite si $1 manquant
    log INFO "Déploiement sur $environnement"
    # ...
    return 0
}

log INFO "Démarrage du script"
deployer "production"

# --- Tableaux (arrays) ---
services=("api" "worker" "scheduler")
echo "Nombre de services : ${#services[@]}"
echo "Premier service : ${services[0]}"
for service in "${services[@]}"; do
    echo "Redémarrage de $service..."
done

# Tableau associatif (bash 4+)
declare -A ports
ports[api]=8000
ports[worker]=8001
for cle in "${!ports[@]}"; do
    echo "$cle écoute sur le port ${ports[$cle]}"
done

# --- trap : réagir à un signal ou à la sortie du script (nettoyage garanti) ---
tmpfile=$(mktemp)
cleanup() {
    log INFO "Nettoyage..."
    rm -f "$tmpfile"
}
trap cleanup EXIT             # s'exécute quoi qu'il arrive, même en cas d'erreur
trap 'log WARN "Interrompu par Ctrl+C"; exit 130' INT

# --- Gestion d'erreurs robuste ---
if ! curl -fsS https://api.exemple.com/health > "$tmpfile"; then
    log ERROR "Health check échoué"
    exit 1
fi

# --- Manipulation de chaînes ---
url="https://api.exemple.com/v1/users"
echo "${url#*://}"          # retire le plus court préfixe matchant : api.exemple.com/v1/users
echo "${url##*/}"             # retire le plus long préfixe : users
echo "${url%/*}"                # retire le plus court suffixe : https://api.exemple.com/v1
nom_fichier="rapport.tar.gz"
echo "${nom_fichier%%.*}"         # retire tout après le premier point : rapport

# Valeur par défaut si une variable est vide/non définie
port="${PORT:-8000}"
echo "Utilisation du port $port"

# --- Expansion de paramètres avancée pour valider des entrées ---
: "${DEPLOY_ENV:?Erreur : DEPLOY_ENV doit être défini}"

# --- getopts : parser proprement des options de ligne de commande ---
while getopts "e:v" option; do
    case "$option" in
        e) environnement="$OPTARG" ;;
        v) verbose=true ;;
        *) echo "Usage: $0 -e <env> [-v]"; exit 1 ;;
    esac
done

Résumé

  • set -euo pipefail doit être le premier réflexe de tout script bash sérieux.
  • trap ... EXIT garantit un nettoyage même en cas d'erreur ou d'interruption (Ctrl+C).
  • Les tableaux associatifs et l'expansion de paramètres (${var#...}, ${var:-défaut}) remplacent avantageusement sed/cut pour du texte simple.

Exercices pratiques

1 disponible
1

Mission : fiabiliser un script de déploiement multi-services

Objectif : Combiner set -euo pipefail, trap et tableaux pour rendre un script de déploiement robuste.

Contexte

Une équipe DevOps te confie un script de déploiement qui gère plusieurs services (api, worker, scheduler) avec des ports différents, destiné à tourner sans surveillance via une pipeline CI. Le script actuel ne commence par aucune protection particulière.

Résoudre l’exercice →