Exercice 10 : Bash avancé : fonctions, tableaux et gestion d'erreurs
🎯 Objectifs
À la fin de cet exercice, vous serez capable de :
- ✅ Définir et appeler des fonctions Bash avec arguments et valeurs de retour
- ✅ Utiliser les tableaux indexés et associatifs (
declare -aetdeclare -A) - ✅ Capturer les erreurs avec
trap ERRet nettoyer avectrap EXIT - ✅ Utiliser les sous-shells
$()et les redirections avancées - ✅ Écrire un script de déploiement structuré avec rollback automatique
Durée estimée : 35 minutes
Difficulté : ⭐⭐⭐☆☆ (Avancé)
Prérequis : Exercice 09 complété - variables, conditions, boucles
📖 Contexte
Les fonctions transforment un script de 200 lignes en code lisible et maintenable. Elles permettent la réutilisation, le test unitaire de chaque partie, et la décomposition du problème. Le trap garantit qu'un script ne laisse jamais de fichiers temporaires ou de ressources verrouillées derrière lui, même en cas d'erreur.
📋 Énoncé
Construisez un script de déploiement avec fonctions, gestion d'erreurs et rollback automatique.
🧭 Déroulement de l'exercice
Tâche 1 : Fonctions de base avec retour de valeurs
cat > 01-fonctions.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
# Fonction avec arguments ($1, $2...) et return (seulement 0-255)
# Pour retourner une chaîne, on utilise echo + sous-shell $()
verifier_port() {
local HOST="${1:?verifier_port: host manquant}"
local PORT="${2:?verifier_port: port manquant}"
local TIMEOUT="${3:-3}" # Valeur par défaut : 3 secondes
if timeout "$TIMEOUT" bash -c "echo > /dev/tcp/$HOST/$PORT" 2>/dev/null; then
echo "ouvert"
return 0
else
echo "fermé"
return 1
fi
}
# Appel avec capture de la sortie
ETAT_HTTP=$(verifier_port "google.com" 80)
echo "Port 80 google.com : $ETAT_HTTP"
ETAT_LOCAL=$(verifier_port "localhost" 22 || true)
echo "Port 22 localhost : $ETAT_LOCAL"
# Fonction qui retourne une valeur calculée
calculer_taille_repertoire() {
local CHEMIN="${1:?chemin requis}"
[[ -d "$CHEMIN" ]] || { echo "0"; return 1; }
du -sh "$CHEMIN" 2>/dev/null | cut -f1
}
echo "Taille /etc : $(calculer_taille_repertoire /etc)"
echo "Taille /tmp : $(calculer_taille_repertoire /tmp)"
EOF
chmod +x 01-fonctions.sh && ./01-fonctions.shIndice :
localest indispensable dans les fonctions. Sanslocal, une variable créée dans une fonction est globale et peut écraser une variable du même nom dans le script principal.
Tâche 2 : Tableaux indexés et associatifs
cat > 02-tableaux.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
# --- Tableau indexé ---
echo "=== Tableau indexé ==="
SERVEURS=("web-01" "web-02" "db-01" "cache-01")
echo "Nombre de serveurs : ${#SERVEURS[@]}"
echo "Premier : ${SERVEURS[0]}"
echo "Dernier : ${SERVEURS[-1]}"
# Itérer
for SERVEUR in "${SERVEURS[@]}"; do
echo " Ping $SERVEUR..."
done
# Ajouter
SERVEURS+=("backup-01")
echo "Après ajout : ${SERVEURS[*]}"
# Slice (éléments 1 à 3)
echo "Slice [1..3] : ${SERVEURS[@]:1:3}"
# --- Tableau associatif (dictionnaire) ---
echo ""
echo "=== Tableau associatif ==="
declare -A CONFIG=(
["env"]="production"
["region"]="eu-west-3"
["replicas"]="3"
["debug"]="false"
)
# Lire une valeur
echo "Environnement : ${CONFIG[env]}"
echo "Région : ${CONFIG[region]}"
# Toutes les clés
echo "Clés : ${!CONFIG[@]}"
# Toutes les valeurs
echo "Valeurs : ${CONFIG[@]}"
# Itérer sur clés et valeurs
for CLE in "${!CONFIG[@]}"; do
echo " $CLE = ${CONFIG[$CLE]}"
done
# Vérifier si une clé existe
if [[ -v CONFIG["debug"] ]]; then
echo "Debug mode : ${CONFIG[debug]}"
fi
EOF
chmod +x 02-tableaux.sh && ./02-tableaux.shTâche 3 : Gestion d'erreurs avec trap
cat > 03-trap.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
# Fichiers temporaires - doivent être nettoyés même en cas d'erreur
TMP_DIR=$(mktemp -d)
TMP_FILE=$(mktemp)
LOCK_FILE="/tmp/mon-script.lock"
# --- NETTOYAGE AUTOMATIQUE ---
# trap s'exécute à la sortie du script (normale ou erreur)
cleanup() {
local EXIT_CODE="$?"
echo "Nettoyage en cours..."
rm -rf "$TMP_DIR" "$TMP_FILE" 2>/dev/null || true
rm -f "$LOCK_FILE" 2>/dev/null || true
if [[ $EXIT_CODE -ne 0 ]]; then
echo "Script terminé avec erreur (code: $EXIT_CODE)" >&2
fi
}
trap cleanup EXIT
# --- GESTIONNAIRE D'ERREUR ---
erreur_handler() {
local LINE="${BASH_LINENO[0]}"
local CMD="${BASH_COMMAND}"
echo "❌ Erreur ligne $LINE : '$CMD'" >&2
}
trap erreur_handler ERR
# --- GESTION DU VERROU (éviter les exécutions parallèles) ---
if [[ -f "$LOCK_FILE" ]]; then
echo "Erreur : le script est déjà en cours (lock: $LOCK_FILE)" >&2
exit 1
fi
touch "$LOCK_FILE"
# Corps du script
echo "Travail dans $TMP_DIR"
echo "données importantes" > "$TMP_DIR/output.txt"
echo "autres données" > "$TMP_FILE"
# Simuler un traitement
sleep 0.5
echo "Traitement terminé"
cat "$TMP_DIR/output.txt"
# Le cleanup() s'exécutera automatiquement en quittant
EOF
chmod +x 03-trap.sh && ./03-trap.shVérification : Après l'exécution, ls /tmp/tmp* ne doit plus montrer les fichiers temporaires créés.
Tâche 4 : Script de déploiement avec rollback
cat > 04-deploy.sh << 'EOF'
#!/usr/bin/env bash
# Script de déploiement avec rollback automatique
set -euo pipefail
APPLI="${1:-mon-app}"
VERSION="${2:-latest}"
DEPLOY_DIR="/opt/deploiements"
CURRENT_LINK="${DEPLOY_DIR}/current"
# Logging structuré
log() {
local NIVEAU="$1"; shift
echo "$(date '+%H:%M:%S') [$NIVEAU] $*"
}
log_info() { log "INFO " "$@"; }
log_ok() { log "OK " "$@"; }
log_erreur(){ log "ERREUR" "$@" >&2; }
# Récupérer la version précédente pour le rollback
recuperer_version_precedente() {
if [[ -L "$CURRENT_LINK" ]]; then
basename "$(readlink "$CURRENT_LINK")"
else
echo ""
fi
}
rollback() {
local VERSION_PRECEDENTE
VERSION_PRECEDENTE=$(recuperer_version_precedente)
if [[ -z "$VERSION_PRECEDENTE" ]]; then
log_erreur "Pas de version précédente pour rollback"
return 1
fi
log_info "Rollback vers $VERSION_PRECEDENTE..."
ln -sfn "${DEPLOY_DIR}/${VERSION_PRECEDENTE}" "$CURRENT_LINK"
log_ok "Rollback effectué vers $VERSION_PRECEDENTE"
}
# En cas d'erreur, déclencher le rollback
trap 'rollback; exit 1' ERR
# --- DÉPLOIEMENT ---
log_info "Déploiement de $APPLI version $VERSION"
NOUVELLE_VERSION="${APPLI}-${VERSION}-$(date +%Y%m%d%H%M%S)"
CIBLE="${DEPLOY_DIR}/${NOUVELLE_VERSION}"
# Simuler la création du déploiement
mkdir -p "$CIBLE"
echo "$VERSION" > "$CIBLE/VERSION"
echo "{\"app\":\"$APPLI\",\"version\":\"$VERSION\"}" > "$CIBLE/config.json"
log_ok "Artefacts copiés dans $CIBLE"
# Simuler un healthcheck
log_info "Healthcheck..."
if [[ "$VERSION" == "broken" ]]; then
log_erreur "Healthcheck échoué - rollback automatique !"
exit 1 # Le trap ERR déclenche le rollback
fi
# Basculer le lien symbolique
ln -sfn "$CIBLE" "$CURRENT_LINK"
log_ok "Déploiement réussi : $CIBLE"
log_info "Version active : $(readlink -f "$CURRENT_LINK")"
EOF
chmod +x 04-deploy.sh
mkdir -p /opt/deploiements
# Premier déploiement
sudo ./04-deploy.sh mon-app 1.0.0
# Second déploiement (réussi)
sudo ./04-deploy.sh mon-app 1.1.0
# Déploiement cassé (doit rollback automatiquement vers 1.1.0)
sudo ./04-deploy.sh mon-app broken || true
sudo ls -la /opt/deploiements/current✅ Vérification du résultat
01-fonctions.shaffiche l'état du port 80 de google.com02-tableaux.shaffiche correctement les clés et valeurs du tableau associatif03-trap.shnettoie les fichiers temporaires après exécution04-deploy.sh mon-app brokenrollback automatiquement vers la version précédente
💡 À retenir
Fonctions Bash :
- Toujours
localpour les variables internes - Retourner une chaîne :
echo "valeur"+ capturer avec$(ma_fonction) - Retourner un code :
return 0(succès) oureturn 1(échec)
trap :
trap 'code' EXIT: s'exécute toujours en quittant (nettoyage)trap 'code' ERR: s'exécute à chaque commande qui échouetrap 'code' SIGINT SIGTERM: capture Ctrl+C et kill
✨ Solution Complète
#!/usr/bin/env bash
set -euo pipefail
# Nettoyage automatique
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# Fonction avec valeur de retour
get_info() {
local FICHIER="${1:?fichier requis}"
[[ -f "$FICHIER" ]] || return 1
echo "$(wc -l < "$FICHIER") lignes"
}
echo "Taille: $(get_info /etc/passwd)"