bash-defensive-patterns

Par wshobson · agents

Maîtrisez les techniques de programmation Bash défensive pour des scripts de niveau production. À utiliser lors de l'écriture de scripts shell robustes, de pipelines CI/CD ou d'utilitaires système nécessitant tolérance aux pannes et sécurité.

npx skills add https://github.com/wshobson/agents --skill bash-defensive-patterns

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 fonctions
  • set -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éfinie
  • set -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

  1. Toujours utiliser le mode strict - set -Eeuo pipefail
  2. Citer toutes les variables - "$variable" prévient la division de mots
  3. Utiliser les conditionnelles [[]] - Plus robuste que [ ]
  4. Implémenter le piégeage des erreurs - Capturer et gérer les erreurs avec élégance
  5. Valider toutes les entrées - Vérifier l'existence des fichiers, les permissions, les formats
  6. Utiliser les fonctions pour la réutilisabilité - Préfixer avec des noms significatifs
  7. Implémenter une journalisation structurée - Inclure les horodatages et les niveaux
  8. Supporter le mode simulation - Permettre aux utilisateurs de prévisualiser les modifications
  9. Gérer les fichiers temporaires de manière sûre - Utiliser mktemp, nettoyer avec trap
  10. Concevoir pour l'idempotence - Les scripts doivent être sûrs de réexécution
  11. Documenter les exigences - Lister les dépendances et les versions minimales
  12. Tester les chemins d'erreur - Assurer que la gestion des erreurs fonctionne correctement
  13. Utiliser command -v - Plus sûr que which pour vérifier les exécutables
  14. Préférer printf à echo - Plus prévisible entre les systèmes

Skills similaires