Motifs défensifs Bash
Guidance complète pour écrire des scripts Bash prêts pour la production en utilisant des techniques de programmation défensive, la gestion des erreurs et les meilleures pratiques de sécurité pour prévenir les pièges courants et assurer la fiabilité.
Quand utiliser cette compétence
- Écrire des scripts d'automatisation de production
- Construire des scripts de pipeline CI/CD
- Créer des utilitaires d'administration système
- Développer une automatisation de déploiement résiliente aux erreurs
- Écrire des scripts qui doivent gérer les cas limites de manière sûre
- Construire des bibliothèques de scripts shell maintenables
- Implémenter une journalisation et une surveillance complètes
- Créer des scripts qui doivent fonctionner sur différentes plateformes
Principes défensifs fondamentaux
1. Mode strict
Activez le mode strict bash au début de chaque script pour détecter les erreurs rapidement.
#!/bin/bash
set -Eeuo pipefail # Quitter en cas d'erreur, variables non définis, défaillance de pipe
Drapeaux clés :
set -E: Hériter du trap ERR dans les fonctionsset -e: Quitter en cas d'erreur (la commande retourne non-zéro)set -u: Quitter lors de la référence d'une variable non définieset -o pipefail: Pipe échoue si une commande échoue (pas seulement la dernière)
2. Piégeage des erreurs et nettoyage
Implémentez un nettoyage approprié à la sortie ou en cas d'erreur du script.
#!/bin/bash
set -Eeuo pipefail
trap 'echo "Erreur à la ligne $LINENO"' ERR
trap 'echo "Nettoyage..."; rm -rf "$TMPDIR"' EXIT
TMPDIR=$(mktemp -d)
# Code du script ici
3. Sécurité des variables
Citez toujours les variables pour éviter les problèmes de division de mots et de globbing.
# Mauvais - non sûr
cp $source $dest
# Correct - sûr
cp "$source" "$dest"
# Variables requises - échouent avec un message si non définis
: "${REQUIRED_VAR:?REQUIRED_VAR n'est pas défini}"
4. Gestion des tableaux
Utilisez les tableaux de manière sûre pour la gestion de données complexes.
# Itération sûre d'un tableau
declare -a items=("item 1" "item 2" "item 3")
for item in "${items[@]}"; do
echo "Traitement : $item"
done
# Lecture de la sortie dans un tableau de manière sûre
mapfile -t lines < <(some_command)
readarray -t numbers < <(seq 1 10)
5. Sécurité des conditionnelles
Utilisez [[ ]] pour les fonctionnalités spécifiques à Bash, [ ] pour POSIX.
# Bash - plus sûr
if [[ -f "$file" && -r "$file" ]]; then
content=$(<"$file")
fi
# POSIX - portable
if [ -f "$file" ] && [ -r "$file" ]; then
content=$(cat "$file")
fi
# Tester l'existence avant les opérations
if [[ -z "${VAR:-}" ]]; then
echo "VAR n'est pas défini ou est vide"
fi
Motifs fondamentaux
Motif 1 : Détection sûre du répertoire du script
#!/bin/bash
set -Eeuo pipefail
# Déterminer correctement le répertoire du script
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"
echo "Localisation du script : $SCRIPT_DIR/$SCRIPT_NAME"
Motif 2 : Modèle de fonction complète
#!/bin/bash
set -Eeuo pipefail
# Préfixe pour les fonctions : handle_*, process_*, check_*, validate_*
# Inclure la documentation et la gestion des erreurs
validate_file() {
local -r file="$1"
local -r message="${2:-Fichier non trouvé : $file}"
if [[ ! -f "$file" ]]; then
echo "ERREUR : $message" >&2
return 1
fi
return 0
}
process_files() {
local -r input_dir="$1"
local -r output_dir="$2"
# Valider les entrées
[[ -d "$input_dir" ]] || { echo "ERREUR : input_dir n'est pas un répertoire" >&2; return 1; }
# Créer le répertoire de sortie si nécessaire
mkdir -p "$output_dir" || { echo "ERREUR : Impossible de créer output_dir" >&2; return 1; }
# Traiter les fichiers de manière sûre
while IFS= read -r -d '' file; do
echo "Traitement : $file"
# Effectuer le travail
done < <(find "$input_dir" -maxdepth 1 -type f -print0)
return 0
}
Motif 3 : Gestion sûre des fichiers temporaires
#!/bin/bash
set -Eeuo pipefail
trap 'rm -rf -- "$TMPDIR"' EXIT
# Créer un répertoire temporaire
TMPDIR=$(mktemp -d) || { echo "ERREUR : Impossible de créer le répertoire temporaire" >&2; exit 1; }
# Créer des fichiers temporaires dans le répertoire
TMPFILE1="$TMPDIR/temp1.txt"
TMPFILE2="$TMPDIR/temp2.txt"
# Utiliser les fichiers temporaires
touch "$TMPFILE1" "$TMPFILE2"
echo "Fichiers temporaires créés dans : $TMPDIR"
Motif 4 : Analyse robuste des arguments
#!/bin/bash
set -Eeuo pipefail
# Valeurs par défaut
VERBOSE=false
DRY_RUN=false
OUTPUT_FILE=""
THREADS=4
usage() {
cat <<EOF
Utilisation : $0 [OPTIONS]
Options :
-v, --verbose Activer la sortie détaillée
-d, --dry-run Exécuter sans apporter de modifications
-o, --output FILE Chemin du fichier de sortie
-j, --jobs NUM Nombre de tâches parallèles
-h, --help Afficher ce message d'aide
EOF
exit "${1:-0}"
}
# Analyser les arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose)
VERBOSE=true
shift
;;
-d|--dry-run)
DRY_RUN=true
shift
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
-j|--jobs)
THREADS="$2"
shift 2
;;
-h|--help)
usage 0
;;
--)
shift
break
;;
*)
echo "ERREUR : Option inconnue : $1" >&2
usage 1
;;
esac
done
# Valider les arguments requuis
[[ -n "$OUTPUT_FILE" ]] || { echo "ERREUR : -o/--output est requis" >&2; usage 1; }
Motif 5 : Journalisation structurée
#!/bin/bash
set -Eeuo pipefail
# Fonctions de journalisation
log_info() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] INFO : $*" >&2
}
log_warn() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARN : $*" >&2
}
log_error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR : $*" >&2
}
log_debug() {
if [[ "${DEBUG:-0}" == "1" ]]; then
echo "[$(date +'%Y-%m-%d %H:%M:%S')] DEBUG : $*" >&2
fi
}
# Utilisation
log_info "Démarrage du script"
log_debug "Informations de débogage"
log_warn "Message d'avertissement"
log_error "Une erreur s'est produite"
Motif 6 : Orchestration de processus avec signaux
#!/bin/bash
set -Eeuo pipefail
# Suivre les processus en arrière-plan
PIDS=()
cleanup() {
log_info "Arrêt..."
# Terminer tous les processus en arrière-plan
for pid in "${PIDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null || true
fi
done
# Attendre l'arrêt gracieux
for pid in "${PIDS[@]}"; do
wait "$pid" 2>/dev/null || true
done
}
trap cleanup SIGTERM SIGINT
# Démarrer les tâches en arrière-plan
background_task &
PIDS+=($!)
another_task &
PIDS+=($!)
# Attendre tous les processus en arrière-plan
wait
Motif 7 : Opérations sûres sur les fichiers
#!/bin/bash
set -Eeuo pipefail
# Utiliser le drapeau -i pour déplacer de manière sûre sans écrasement
safe_move() {
local -r source="$1"
local -r dest="$2"
if [[ ! -e "$source" ]]; then
echo "ERREUR : La source n'existe pas : $source" >&2
return 1
fi
if [[ -e "$dest" ]]; then
echo "ERREUR : La destination existe déjà : $dest" >&2
return 1
fi
mv "$source" "$dest"
}
# Nettoyage sûr du répertoire
safe_rmdir() {
local -r dir="$1"
if [[ ! -d "$dir" ]]; then
echo "ERREUR : N'est pas un répertoire : $dir" >&2
return 1
fi
# Utiliser le drapeau -I pour demander confirmation avant rm (compatible BSD/GNU)
rm -rI -- "$dir"
}
# Écritures atomiques de fichiers
atomic_write() {
local -r target="$1"
local -r tmpfile
tmpfile=$(mktemp) || return 1
# Écrire dans le fichier temporaire d'abord
cat > "$tmpfile"
# Renommage atomique
mv "$tmpfile" "$target"
}
Motif 8 : Conception idempotente du script
#!/bin/bash
set -Eeuo pipefail
# Vérifier si la ressource existe déjà
ensure_directory() {
local -r dir="$1"
if [[ -d "$dir" ]]; then
log_info "Le répertoire existe déjà : $dir"
return 0
fi
mkdir -p "$dir" || {
log_error "Impossible de créer le répertoire : $dir"
return 1
}
log_info "Répertoire créé : $dir"
}
# Assurer l'état de la configuration
ensure_config() {
local -r config_file="$1"
local -r default_value="$2"
if [[ ! -f "$config_file" ]]; then
echo "$default_value" > "$config_file"
log_info "Configuration créée : $config_file"
fi
}
# Réexécuter le script plusieurs fois doit être sûr
ensure_directory "/var/cache/myapp"
ensure_config "/etc/myapp/config" "DEBUG=false"
Motif 9 : Substitution de commande sûre
#!/bin/bash
set -Eeuo pipefail
# Utiliser $() à la place des backticks
name=$(<"$file") # Attribution de variable moderne et sûre depuis le fichier
output=$(command -v python3) # Obtenir l'emplacement de la commande de manière sûre
# Gérer la substitution de commande avec vérification des erreurs
result=$(command -v node) || {
log_error "Commande node non trouvée"
return 1
}
# Pour plusieurs lignes
mapfile -t lines < <(grep "pattern" "$file")
# Itération sûre pour NUL
while IFS= read -r -d '' file; do
echo "Traitement : $file"
done < <(find /path -type f -print0)
Motif 10 : Support du mode simulation
#!/bin/bash
set -Eeuo pipefail
DRY_RUN="${DRY_RUN:-false}"
run_cmd() {
if [[ "$DRY_RUN" == "true" ]]; then
echo "[SIMULATION] Exécuterait : $*"
return 0
fi
"$@"
}
# Utilisation
run_cmd cp "$source" "$dest"
run_cmd rm "$file"
run_cmd chown "$owner" "$target"
Techniques défensives avancées
Motif de paramètres nommés
#!/bin/bash
set -Eeuo pipefail
process_data() {
local input_file=""
local output_dir=""
local format="json"
# Analyser les paramètres nommés
while [[ $# -gt 0 ]]; do
case "$1" in
--input=*)
input_file="${1#*=}"
;;
--output=*)
output_dir="${1#*=}"
;;
--format=*)
format="${1#*=}"
;;
*)
echo "ERREUR : Paramètre inconnu : $1" >&2
return 1
;;
esac
shift
done
# Valider les paramètres requis
[[ -n "$input_file" ]] || { echo "ERREUR : --input est requis" >&2; return 1; }
[[ -n "$output_dir" ]] || { echo "ERREUR : --output est requis" >&2; return 1; }
}
Vérification des dépendances
#!/bin/bash
set -Eeuo pipefail
check_dependencies() {
local -a missing_deps=()
local -a required=("jq" "curl" "git")
for cmd in "${required[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
missing_deps+=("$cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
echo "ERREUR : Commandes requises manquantes : ${missing_deps[*]}" >&2
return 1
fi
}
check_dependencies
Résumé des meilleures pratiques
- Toujours utiliser le mode strict -
set -Eeuo pipefail - Citer toutes les variables -
"$variable"prévient la division de mots - Utiliser les conditionnelles [[]] - Plus robuste que [ ]
- Implémenter le piégeage des erreurs - Capturer et gérer les erreurs avec élégance
- Valider toutes les entrées - Vérifier l'existence des fichiers, les permissions, les formats
- Utiliser les fonctions pour la réutilisabilité - Préfixer avec des noms significatifs
- Implémenter une journalisation structurée - Inclure les horodatages et les niveaux
- Supporter le mode simulation - Permettre aux utilisateurs de prévisualiser les modifications
- Gérer les fichiers temporaires de manière sûre - Utiliser mktemp, nettoyer avec trap
- Concevoir pour l'idempotence - Les scripts doivent être sûrs de réexécution
- Documenter les exigences - Lister les dépendances et les versions minimales
- Tester les chemins d'erreur - Assurer que la gestion des erreurs fonctionne correctement
- Utiliser
command -v- Plus sûr quewhichpour vérifier les exécutables - Préférer printf à echo - Plus prévisible entre les systèmes