Skip to content

Bash & Shell Scripting

Tip

Outils Indispensables - ShellCheck : Analyseur statique pour scripts shell (incontournable pour détecter les bugs et failles). - Explainshell : Visualiseur interactif pour décortiquer n'importe quelle commande complexe. - DevHints Bash : Aide-mémoire rapide et complet.


1. Anatomie d'un Script Bash Robuste (De A à Z)

Un script bash en production ne doit pas échouer silencieusement ni adopter un comportement imprévisible. Voici les éléments structurels indispensables :

1.1. Le Shebang

Préférez toujours l'appel via /usr/bin/env qui trouve automatiquement le binaire bash dans le PATH de l'environnement (portable entre distributions, macOS, BSD, NixOS) :

#!/usr/bin/env bash

1.2. Le Mode Strict (set -euo pipefail)

Placez ces options au tout début de chaque script :

set -euo pipefail
IFS=$'\n\t'
Option Rôle
set -e Arrête immédiatement l'exécution si une commande renvoie un code de retour non nul.
set -u Traite toute variable non définie comme une erreur et interrompt le script.
set -o pipefail Fait échouer un pipeline (cmd1 \| cmd2) dès qu'une des commandes échoue (au lieu de ne considérer que la dernière).
IFS=$'\n\t' Définit le séparateur interne de champs sur saut de ligne et tabulation uniquement (évite les bugs liés aux espaces dans les noms de fichiers).

Note

Si une commande est censée échouer sans bloquer le script, neutralisez temporairement l'erreur avec || true :

grep "motif" fichier.txt || true

1.3. Localiser le Répertoire du Script

Pour référencer des fichiers relatifs au script (fichiers de conf, helpers, .env) quel que soit l'endroit depuis lequel le script est exécuté :

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR

1.4. Gestion des Nettoyages et Interruptions (trap)

Le mécanisme trap permet d'exécuter une fonction de nettoyage automatique lors de la sortie normale du script ou en cas d'interruption (SIGINT, SIGTERM, EXIT).

# Création d'un dossier temporaire sécurisé
TMP_DIR="$(mktemp -d -t my_script_XXXXXX)"

cleanup() {
    local exit_code=$?
    # Supprime les fichiers temporaires
    rm -rf "$TMP_DIR"
    # Actions supplémentaires (déverrouillage de lockfile, déconnexion...)
    if [[ $exit_code -ne 0 ]]; then
        echo "Le script s'est terminé avec une erreur (Code: $exit_code)" >&2
    fi
}

# Capture la sortie normale (EXIT), Ctrl+C (INT) et l'arrêt système (TERM)
trap cleanup EXIT INT TERM

2. Gestion des Variables & Variables d'Environnement

2.1. Portée et Déclaration des Variables

  • Constantes globales : Déclarées avec readonly en MAJUSCULES.
  • Variables locales dans les fonctions : Toujours déclarées avec local pour éviter d'écraser des variables globales.
  • Variables d'environnement : Exportées avec export uniquement si les sous-processus doivent y avoir accès.
readonly APP_NAME="MonScript"
readonly VERSION="1.0.0"

ma_fonction() {
    local user_input="$1"
    local result=""
    # ...
    echo "$result"
}

2.2. Substitutions et Valeurs par Défaut

Bash fournit des mécanismes natifs puissants pour manipuler les variables sans recourir à des sous-commandes comme sed ou awk :

# 1. Valeur par défaut si non définie ou vide
DB_PORT="${DB_PORT:-5432}"

# 2. Assigner la valeur par défaut si non définie
DB_HOST="${DB_HOST:=localhost}"

# 3. Erreur obligatoire si variable manquante
API_KEY="${API_KEY:?Erreur : La variable API_KEY est obligatoire !}"

# 4. Longueur d'une chaîne
len="${#DB_HOST}"

# 5. Remplacement / Suppression de motif
filename="archive_backup_2026.tar.gz"
echo "${filename%.tar.gz}"    # Supprime le suffixe -> archive_backup_2026
echo "${filename#archive_}"   # Supprime le préfixe -> backup_2026.tar.gz
echo "${filename//_/-}"       # Remplace tous les '_' par '-' -> archive-backup-2026.tar.gz

# 6. Conversion de casse (Bash 4+)
nom="serveur-medusa"
echo "${nom^^}"  # Majuscule -> SERVEUR-MEDUSA
echo "${nom,,}"  # Minuscule -> serveur-medusa

2.3. Chargement Sécurisé d'un Fichier .env

Pour charger des variables d'environnement depuis un fichier .env ou un fichier de configuration sans exécuter de code arbitraire :

load_env() {
    local env_file="${1:-${SCRIPT_DIR}/.env}"

    if [[ -f "$env_file" ]]; then
        # Charge le fichier en exportant automatiquement les variables
        set -a
        # shellcheck disable=SC1090
        source "$env_file"
        set +a
    else
        echo "Avertissement : Fichier d'environnement '$env_file' introuvable." >&2
    fi
}

3. Création de Helpers et Fonctions Utilitaires

Centraliser les fonctions récurrentes dans une section du script ou dans un module séparé (lib/utils.sh).

3.1. Gestion des Sorties Standard et d'Erreur (Helpers de Log)

Une bonne pratique Unix consiste à envoyer les messages d'état, avertissements et erreurs sur la sortie d'erreur standard (stderr via >&2), afin de ne pas corrompre la sortie standard (stdout) si le script est utilisé dans un pipeline (|) :

log_info() {
    echo "[INFO] $*"
}

log_warn() {
    echo "[WARN] $*" >&2
}

log_error() {
    echo "[ERROR] $*" >&2
}

log_debug() {
    if [[ "${VERBOSE:-false}" == "true" ]]; then
        echo "[DEBUG] $*" >&2
    fi
}

3.2. Helpers de Validation et de Dépendances

# Vérifie que les dépendances système sont installées
check_dependencies() {
    local missing_deps=()
    for cmd in "$@"; do
        if ! command -v "$cmd" &>/dev/null; then
            missing_deps+=("$cmd")
        fi
    done

    if [[ ${#missing_deps[@]} -gt 0 ]]; then
        log_error "Les dépendances suivantes sont requises mais manquantes : ${missing_deps[*]}"
        exit 1
    fi
}

# Vérifie si le script est exécuté en root (ou non)
require_root() {
    if [[ $EUID -ne 0 ]]; then
        log_error "Ce script doit être exécuté avec les privilèges root (sudo)."
        exit 1
    fi
}

# Demande confirmation utilisateur (O/N)
confirm() {
    local prompt="${1:-Êtes-vous sûr ?}"
    local response
    read -r -p "$prompt [o/N] " response
    case "$response" in
        [oO][uU][iI]|[oO]) return 0 ;;
        *) return 1 ;;
    esac
}

3.3. Architecture Modulaire (Importer un fichier Helper externe)

Si vous souhaitez partager vos helpers entre plusieurs scripts :

mon-projet/
├── bin/
│   └── deploy.sh
├── lib/
│   └── utils.sh
└── config/
    └── .env

Dans deploy.sh :

#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="${SCRIPT_DIR}/../lib"

# Importation du helper
if [[ -f "${LIB_DIR}/utils.sh" ]]; then
    # shellcheck source=../lib/utils.sh
    source "${LIB_DIR}/utils.sh"
else
    echo "Fichier de helpers ${LIB_DIR}/utils.sh introuvable !" >&2
    exit 1
fi

log_info "Helper chargé avec succès."

4. Parsing des Arguments et Options CLI

Pour rendre vos scripts conviviaux, supportez les flags courts (-h, -v, -f) et longs (--help, --verbose, --file).

4.1. Fonction d'Aide (usage)

usage() {
    cat <<EOF
Usage: $(basename "$0") [OPTIONS] <COMMAND>

Description :
  Script d'automatisation pour le déploiement et la maintenance.

Commandes :
  start            Démarre le service
  stop             Arrête le service
  status           Vérifie l'état

Options :
  -e, --env FILE   Spécifie le fichier .env à charger (défaut : .env)
  -v, --verbose    Active les journaux de débogage
  -f, --force      Force l'exécution sans demander de confirmation
  -h, --help       Affiche cette aide et quitte

Exemples :
  $(basename "$0") --env /etc/myapp.env start
  $(basename "$0") --verbose --force stop
EOF
    exit 0
}

4.2. Boucle de Parsing (while / case)

# Variables par défaut
ENV_FILE="${SCRIPT_DIR}/.env"
VERBOSE=false
FORCE=false
COMMAND=""

parse_arguments() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help)
                usage
                ;;
            -v|--verbose)
                VERBOSE=true
                shift
                ;;
            -f|--force)
                FORCE=true
                shift
                ;;
            -e|--env)
                if [[ -z "${2:-}" ]]; then
                    log_error "L'option '$1' nécessite un argument (fichier)."
                    exit 1
                fi
                ENV_FILE="$2"
                shift 2
                ;;
            --env=*)
                ENV_FILE="${1#*=}"
                shift
                ;;
            start|stop|status)
                COMMAND="$1"
                shift
                ;;
            -*)
                log_error "Option inconnue : $1"
                usage
                ;;
            *)
                # Argument positionnel restant
                log_error "Argument inattendu : $1"
                exit 1
                ;;
        esac
    done

    if [[ -z "$COMMAND" ]]; then
        log_error "Aucune commande spécifiée."
        usage
    fi
}

5. Template de Script Production-Ready (Clé en Main)

Voici un template complet, réutilisable pour tout nouveau script shell professionnel :

#!/usr/bin/env bash
# ==============================================================================
# Nom du script  : template_script.sh
# Description    : Template de script Bash moderne, sécurisé et modulaire
# Auteur         : Antoine
# ==============================================================================

set -euo pipefail
IFS=$'\n\t'

# --- 1. Variables globales & chemins ---
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly VERSION="1.0.0"

# Variables configurables via arguments / environnement
ENV_FILE="${SCRIPT_DIR}/.env"
VERBOSE=false
FORCE=false
ACTION=""

# Création du dossier temporaire
TMP_DIR="$(mktemp -d -t "${SCRIPT_NAME}_XXXXXX")"

# --- 2. Messages & Sorties d'erreur ---
log_info()  { echo "[INFO] $*"; }
log_warn()  { echo "[WARN] $*" >&2; }
log_error() { echo "[ERROR] $*" >&2; }
log_debug() { [[ "$VERBOSE" == "true" ]] && echo "[DEBUG] $*" >&2 || true; }

# --- 3. Nettoyage & Signaux (Trap) ---
cleanup() {
    local exit_code=$?
    rm -rf "$TMP_DIR"
    log_debug "Nettoyage du répertoire temporaire : $TMP_DIR"
    if [[ $exit_code -ne 0 ]]; then
        log_error "Le script s'est arrêté de façon inattendue (code: $exit_code)"
    fi
}
trap cleanup EXIT INT TERM

# --- 4. Helpers & Validations ---
usage() {
    cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <ACTION>

Actions :
  run              Exécute le traitement principal
  check            Vérifie la configuration et l'environnement

Options :
  -e, --env FILE   Fichier d'environnement (défaut : $ENV_FILE)
  -v, --verbose    Mode verbeux (logs debug)
  -f, --force      Force l'exécution sans demander de confirmation
  -h, --help       Affiche cette aide
  --version        Affiche la version du script
EOF
    exit 0
}

check_dependencies() {
    local deps=("curl" "jq")
    for cmd in "${deps[@]}"; do
        if ! command -v "$cmd" &>/dev/null; then
            log_error "Outil requis manquant : $cmd"
            exit 1
        fi
    done
}

load_env() {
    if [[ -f "$ENV_FILE" ]]; then
        log_debug "Chargement du fichier d'environnement : $ENV_FILE"
        set -a
        # shellcheck disable=SC1090
        source "$ENV_FILE"
        set +a
    else
        log_warn "Fichier d'environnement non trouvé : $ENV_FILE (valeurs par défaut utilisées)"
    fi
}

# --- 5. Parsing des arguments ---
parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help) usage ;;
            --version) echo "$SCRIPT_NAME version $VERSION"; exit 0 ;;
            -v|--verbose) VERBOSE=true; shift ;;
            -f|--force) FORCE=true; shift ;;
            -e|--env)
                [[ -z "${2:-}" ]] && { log_error "Option '$1' attend un argument."; exit 1; }
                ENV_FILE="$2"
                shift 2
                ;;
            run|check)
                ACTION="$1"
                shift
                ;;
            *)
                log_error "Argument ou option non reconnue : $1"
                usage
                ;;
        esac
    done

    [[ -z "$ACTION" ]] && { log_error "Aucune action spécifiée."; usage; }
}

# --- 6. Logique Métier ---
do_check() {
    log_info "Vérification des prérequis..."
    check_dependencies
    log_info "Tous les prérequis sont validés."
}

do_run() {
    log_info "Démarrage de l'action principale..."

    # Exemple de variable d'environnement avec fallback
    local target_host="${APP_TARGET_HOST:-127.0.0.1}"
    log_info "Cible : $target_host"

    if [[ "$FORCE" != "true" ]]; then
        read -r -p "Confirmer l'opération sur $target_host ? [o/N] " answer
        [[ "$answer" =~ ^[oOyY]$ ]] || { log_warn "Opération annulée par l'utilisateur."; exit 0; }
    fi

    # Traitement métier ici...
    log_debug "Écriture dans le dossier temporaire : $TMP_DIR"
    echo "payload test" > "${TMP_DIR}/payload.tmp"

    log_info "Traitement terminé avec succès !"
}

# --- 7. Point d'Entrée Principal ---
main() {
    parse_args "$@"
    load_env

    case "$ACTION" in
        check) do_check ;;
        run)   do_run ;;
    esac
}

main "$@"

6. Bonnes Pratiques & Pièges Courants

6.1. Toujours Quoter ses Variables ("$VAR")

Ne laissez jamais une variable sans guillemets doubles sauf besoin explicite de word splitting :

# MAUVAIS (échoue si $path contient des espaces ou des caractères spéciaux)
rm -rf $path
if [ $name == "root" ]; then ... fi

# BON
rm -rf "$path"
if [[ "$name" == "root" ]]; then ... fi

6.2. Préférer [[ ... ]] à [ ... ]

Dans Bash, les doubles crochets [[ ... ]] sont plus sûrs et offrent des fonctionnalités avancées : - Support des opérateurs logiques && et || directs. - Support des expressions régulières avec =~. - Pas de plantage si la variable testée est vide.

# Vérifier si une variable est un entier
if [[ "$PORT" =~ ^[0-9]+$ ]]; then
    log_info "Port valide : $PORT"
fi

# Tests multiples
if [[ -f "$CONFIG_FILE" && -r "$CONFIG_FILE" ]]; then
    log_info "Fichier lisible."
fi

6.3. Tableaux (Arrays) Indexés et Associatifs

# Tableau indexé
serveurs=("medusa" "chimera" "backup")
serveurs+=("desktop")

for srv in "${serveurs[@]}"; do
    echo "Serveur : $srv"
done

echo "Nombre d'éléments : ${#serveurs[@]}"

# Tableau associatif (Clé / Valeur - Bash 4+)
declare -A ip_table
ip_table["medusa"]="10.2.0.1"
ip_table["chimera"]="10.2.0.2"

for host in "${!ip_table[@]}"; do
    echo "$host a pour IP ${ip_table[$host]}"
done

7. Détection des Arguments Système (Ex: systemd-suspend)

Certains services système appellent des scripts avec des arguments positionnels prédéfinis. Par exemple, l'exécution de scripts via /usr/lib/systemd/system-sleep/ fournit deux arguments : - $1 : la phase (pre avant la suspension, post après le réveil). - $2 : le mode (suspend, hibernate, hybrid-sleep).

Exemple de structure de script pour system-sleep :

#!/usr/bin/env bash
set -euo pipefail

# $1 = phase (pre/post)
# $2 = mode (suspend/hibernate/hybrid-sleep)

readonly PHASE="${1:-}"
readonly MODE="${2:-}"

case "$PHASE" in
    pre)
        logger -t "system-sleep" "Mise en veille ($MODE)..."
        # Commandes AVANT la mise en veille (ex: arrêter un service sensible, synchroniser les disques)
        ;;
    post)
        logger -t "system-sleep" "Sortie de veille ($MODE)..."
        # Commandes APRÈS le réveil (ex: redémarrer les interfaces réseau, relancer un VPN)
        ;;
    *)
        echo "Usage: $(basename "$0") {pre|post} {suspend|hibernate|hybrid-sleep}" >&2
        exit 1
        ;;
esac