Scripting Bash avancé — pièges, bonnes pratiques et optimisation

07 juin 2026 — par admin_libra

Bash est le shell par défaut sur Debian et Ubuntu, et demeure l’outil incontournable pour automatiser l’administration système. Pourtant, entre un script qui « fonctionne » et un script robuste, maintenable et performant, la distance est souvent considérable. Nombreux sont les administrateurs qui ont vu un script de production silencieusement avaler une erreur critique, propageant un problème sans le signaler.

Les pièges de Bash sont nombreux et subtils : variables non protégées, pipelines masquant des échecs, sous-shells isolant les modifications, boucles multipliant les processus externes inutilement. Ce guide explore les techniques avancées pour écrire des scripts réellement fiables, en couvrant le mode strict, la gestion des erreurs, l’organisation du code par fonctions, l’optimisation des performances et les outils de débogage.

Les exemples présentés ici sont testés sur Debian 12 (Bookworm) et Ubuntu 24.04 LTS avec Bash 5.2. L’objectif est de vous donner des réflexes applicables immédiatement dans vos scripts de production.

1. Le mode strict : la base de tout script robuste

Tout script destiné à la production devrait commencer par l’activation du mode strict. Ces trois options transforment Bash en un environnement qui détecte et signale les erreurs au lieu de les ignorer silencieusement.

#!/usr/bin/env bash
set -o errexit   # Quitter immédiatement sur erreur (équivalent : set -e)
set -o nounset   # Erreur sur variable non définie (équivalent : set -u)
set -o pipefail  # Propager les erreurs dans les pipelines
IFS=$'nt'      # Séparateur interne : saut de ligne et tabulation uniquement

set -o errexit : arrêter sur la première erreur

Sans set -e, un script continue son exécution même si une commande échoue. C’est l’une des causes les plus fréquentes de comportements inattendus en production. Avec errexit, dès qu’une commande retourne un code de sortie non nul, le script s’interrompt immédiatement.

Piège courant : set -e est ignoré dans les structures conditionnelles (if, while, ||, &&). Pour une commande qui peut légitimement échouer, utilisez || true :

# Cette commande ne provoque pas l'arrêt si elle échoue
rm /tmp/fichier_optionnel.tmp || true

# Pattern recommandé avec vérification explicite
if ! commande_risquée; then
    echo "Échec de commande_risquée — traitement de l'erreur" >&2
    exit 1
fi

set -o nounset : interdire les variables non définies

L’utilisation d’une variable non définie retourne une chaîne vide par défaut — comportement silencieux pouvant causer des catastrophes (imaginez rm -rf "${PREFIX}/" avec PREFIX vide). L’option nounset lève une erreur dans ce cas.

# Valeur par défaut si la variable est absente
LOG_DIR="${LOG_DIR:-/var/log/monapp}"

# Vérification explicite
: "${REQUIRED_VAR:?La variable REQUIRED_VAR doit être définie}"

set -o pipefail : ne pas masquer les erreurs dans les pipelines

Sans pipefail, le code de retour d’un pipeline est celui de la dernière commande uniquement. Un pipeline comme failing_cmd | grep pattern retourne 0 si grep réussit, même si failing_cmd a échoué.

# Sans pipefail : cette ligne peut passer en silence même si tar échoue
tar czf - /données | ssh serveur-backup "cat > backup.tar.gz"

# Avec set -o pipefail : l'échec de tar arrête le script

2. Pièges classiques et comment les éviter

La gestion des espaces et des noms de fichiers

Le piège le plus classique de Bash : oublier de quoter les variables. Un nom de fichier contenant des espaces, des astérisques ou des sauts de ligne peut provoquer une expansion inattendue.

# DANGEREUX : expansion sur les espaces et les globs
for fichier in $(ls /data/*.log); do
    traiter $fichier
done

# CORRECT : utilisation de glob natif et double quotes
for fichier in /data/*.log; do
    [[ -f "$fichier" ]] || continue
    traiter "$fichier"
done

# Pour les listes de fichiers avec find
while IFS= read -r -d '' fichier; do
    traiter "$fichier"
done < <(find /data -name "*.log" -print0)

Variables dans les sous-shells : l’isolation invisibleg

Un sous-shell (créé par ( ), la substitution de commandes ou le côté droit d’un pipe) ne peut pas modifier les variables du shell parent. Ce comportement surprend régulièrement.

compteur=0

# PIÈGE : la boucle s'exécute dans un sous-shell via le pipe
cat liste.txt | while read -r ligne; do
    (( compteur++ ))
done
echo "Compteur : $compteur"  # Affiche 0 !

# CORRECT : Process Substitution (pas de sous-shell pour la boucle)
while IFS= read -r ligne; do
    (( compteur++ ))
done < <(cat liste.txt)
echo "Compteur : $compteur"  # Affiche la valeur correcte

Backticks vs $() : préférer la substitution moderne

La syntaxe backtick (`cmd`) est l’ancienne forme de substitution de commandes. La syntaxe $(cmd) est préférable : elle est imbriquable, plus lisible et gère mieux les caractères spéciaux.

# Ancien style (à éviter)
resultat=`echo "Date : $(date)"`

# Style moderne recommandé
resultat=$(echo "Date : $(date)")

# Imbrication facile avec $()
version=$(dpkg-query --show --showformat='${Version}' "$(basename "$0")")

Le piège de l’arithmétique avec set -e

En Bash, une expression arithmétique qui s’évalue à 0 retourne le code de sortie 1 (faux), ce qui déclenche errexit.

set -e
compteur=0

# PIÈGE : (( compteur++ )) retourne 1 quand compteur vaut 0
(( compteur++ ))  # Tue le script !

# CORRECT : forcer un code de sortie 0
(( compteur++ )) || true
# Ou utiliser la forme let avec || true
(( ++compteur )) || true
# Ou encore : tester avant d'incrémenter
compteur=$(( compteur + 1 ))

3. Fonctions : organisation et bonnes pratiques

Structurer ses scripts en fonctions améliore la lisibilité, la testabilité et la réutilisabilité. Chaque fonction devrait suivre le principe de responsabilité unique.

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

# Constantes globales en majuscules (readonly)
readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"

# Fonctions de logging
log_info()  { printf '[INFO]  %sn' "$*" | tee -a "$LOG_FILE"; }
log_error() { printf '[ERROR] %sn' "$*" | tee -a "$LOG_FILE" >&2; }

# Nettoyage automatique à la sortie
cleanup() {
    local exit_code=$?
    rm -f /tmp/${SCRIPT_NAME}_*.tmp
    [[ $exit_code -ne 0 ]] && log_error "Script terminé avec le code $exit_code"
    exit "$exit_code"
}
trap cleanup EXIT ERR INT TERM

# Fonction avec variables locales et documentation
# Arguments: $1=chemin_source, $2=chemin_destination
# Retour: 0 si succès, 1 si échec
sauvegarder_repertoire() {
    local source="$1"
    local destination="$2"
    local archive="${destination}/backup_$(date +%Y%m%d_%H%M%S).tar.gz"

    [[ -d "$source" ]] || { log_error "Source introuvable : $source"; return 1; }
    [[ -d "$destination" ]] || mkdir -p "$destination"

    tar czf "$archive" --exclude='*.tmp' -C "$(dirname "$source")" 
        "$(basename "$source")" 
        && log_info "Sauvegarde créée : $archive" 
        || { log_error "Échec de la sauvegarde de $source"; return 1; }
}

4. Optimisation des performances

Éviter les fork inutiles : privilégier les builtins

Chaque appel à une commande externe (grep, awk, sed, wc…) crée un processus fils. Dans une boucle itérée des milliers de fois, ces forks représentent un coût significatif. Les builtins Bash sont jusqu’à 100× plus rapides.

# Lent : appel externe à wc et echo
longueur=$(echo "$chaine" | wc -c)

# Rapide : builtin Bash
longueur=${#chaine}

# Lent : appel externe à grep
if echo "$chaine" | grep -q "^[0-9]"; then ...

# Rapide : [[]] avec pattern matching (pas de fork)
if [[ "$chaine" =~ ^[0-9] ]]; then ...

# Lent : appel externe à sed pour substitution simple
chaine_modifiee=$(echo "$chaine" | sed 's/foo/bar/g')

# Rapide : expansion de paramètres Bash
chaine_modifiee="${chaine//foo/bar}"

Tableaux et lecture en masse avec mapfile

Pour lire un fichier ligne par ligne dans un tableau, mapfile (alias readarray) est beaucoup plus efficace qu’une boucle avec read.

# Lent : boucle read ligne par ligne
declare -a lignes=()
while IFS= read -r ligne; do
    lignes+=("$ligne")
done < fichier.txt

# Rapide : mapfile charge tout en une opération
mapfile -t lignes < fichier.txt

# Traitement de toutes les lignes en une passe
printf '%sn' "${lignes[@]}" | awk '...'

# Tableau associatif pour des lookups O(1)
declare -A index_services
while IFS='=' read -r cle valeur; do
    index_services["$cle"]="$valeur"
done < /etc/services_map.conf

# Accès instantané sans grep/awk
echo "${index_services[nginx]}"

Parallélisation avec & et wait

Pour les tâches indépendantes (sauvegardes de plusieurs serveurs, compilation de fichiers), la parallélisation peut réduire drastiquement le temps d’exécution.

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

readonly MAX_JOBS=4  # Limiter aux cœurs disponibles
declare -a pids=()

traiter_serveur() {
    local serveur="$1"
    # Traitement du serveur...
    ssh "$serveur" "df -h" > "/tmp/rapport_${serveur}.txt"
}

for serveur in srv1 srv2 srv3 srv4 srv5 srv6; do
    # Limiter la concurrence
    while (( ${#pids[@]} >= MAX_JOBS )); do
        for i in "${!pids[@]}"; do
            if ! kill -0 "${pids[$i]}" 2>/dev/null; then
                unset 'pids[$i]'
            fi
        done
        pids=("${pids[@]}")
        sleep 0.1
    done

    traiter_serveur "$serveur" &
    pids+=($!)
done

# Attendre tous les jobs et collecter les codes de retour
for pid in "${pids[@]}"; do
    wait "$pid" || echo "Job $pid a échoué" >&2
done

Définir LC_ALL=C pour les opérations locale-agnostiques

Les opérations de tri, comparaison et traitement de texte peuvent être accélérées de 30 à 40 % en forçant la locale C, qui évite le traitement des collations Unicode.

export LC_ALL=C

# Tri ultra-rapide d'un grand fichier
LC_ALL=C sort -u fichier_volumineux.txt > fichier_trie.txt

# grep sans surcharge Unicode
LC_ALL=C grep -c "pattern" fichier.log

5. Débogage et validation

Un bon workflow de développement Bash inclut systématiquement une phase de validation statique et une phase de débogage ciblé.

# Vérification syntaxique sans exécution
bash -n mon_script.sh

# Trace d'exécution complète (mode debug)
bash -x mon_script.sh

# Débogage d'une section spécifique
set -x
section_à_déboguer
set +x

# Installation de ShellCheck sur Debian/Ubuntu
apt install shellcheck

# Analyse statique complète
shellcheck -S warning mon_script.sh

# Mesure des performances
time mon_script.sh

# Profiling ligne par ligne avec PS4
export PS4='+ $(date "+%s%N") ${BASH_SOURCE}:${LINENO}: '
set -x

ShellCheck est particulièrement précieux : il détecte les problèmes de quoting, les variables non protégées, les patterns dangereux et propose des corrections avec des explications détaillées. Il s’intègre également dans la plupart des éditeurs de code (VS Code, Vim, Emacs).

À lire également

Références

Index complet

Tous les articles (41)

Date Article Tags
07/06/2026 Docker : comment récupérer de l'espace disque cache conteneurs debian 07/06/2026 Graylog 7 — Centralisation et analyse de logs : l'alternative à ELK sur Debian/Ubuntu centralisation debian elk 07/06/2026 OpenZFS : tiering avec L2ARC et SLOG pour les workloads mixtes cache l2arc nvme 07/06/2026 Scripting Bash avancé — pièges, bonnes pratiques et optimisation automatisation bash bonnes-pratiques 07/06/2026 AppArmor sur Debian/Ubuntu : profils, modes et confinement applicatif apparmor audit confinement 07/06/2026 Durcissement SSH — au-delà des clés publiques 2fa authentification cryptographie 27/05/2026 LXD 6.x : orchestration de conteneurs Linux avec profils et clustering administration clustering conteneurs 27/05/2026 Keepalived — VIP flottante et load balancing sans matériel dédié debian failover haute-disponibilité 27/05/2026 Btrfs sur Linux — snapshots, sous-volumes et compression en pratique administration btrfs compression 21/05/2026 CVE-2026-42945 (NGINX Rift) : analyse et remédiation sur Debian/Ubuntu cve debian heap-overflow 21/05/2026 Tuning kernel Linux — paramètres sysctl essentiels pour la production debian kernel mémoire 21/05/2026 DRBD : réplication de blocs entre deux serveurs en temps réel cluster debian drbd 15/05/2026 CVE-2026-23918 — vulnérabilité Apache 2.4.66 : analyse et correctifs sur Debian/Ubuntu (hors Debian 11) apache cve debian 15/05/2026 CVE-2026-31431 (Copy Fail) — Analyse et remédiation sur Debian/Ubuntu algif_aead copy-fail cve 12/05/2026 Pacemaker et Corosync — cluster haute disponibilité Linux cluster corosync debian 12/05/2026 WireGuard : monter un VPN mesh entre plusieurs serveurs Linux chiffrement linux mesh 12/05/2026 Netdata — monitoring temps réel sans configuration complexe alertes dashboard linux 12/05/2026 nftables en pratique — remplacer iptables sur Debian/Ubuntu debian firewall iptables 12/05/2026 Podman : alternative rootless à Docker — installation et migration conteneurs docker kubernetes 02/05/2026 Prometheus et Grafana sur Debian — installation, configuration et dashboards pratiques alertmanager dashboard debian 02/05/2026 Ansible : automatiser la gestion de serveurs Linux avec des playbooks administration ansible automation 28/04/2026 ZFS sur Linux : snapshots, clones et RAID-Z en pratique administration compression filesystem 28/04/2026 eBPF sur Linux : observabilité et traçage kernel avec bpftrace et BCC bcc bpftrace diagnostic 23/04/2026 Analyse de la mémoire sur Linux — vmstat, free, smem diagnostic mémoire monitoring 23/04/2026 Sécurité Linux — Firewall iptables et nftables firewall iptables nftables 23/04/2026 ZFS sur Linux — Installation et gestion avancée administration filesystem stockage 23/04/2026 Gestion des services avec systemd sur Debian et Ubuntu administration debian services 23/04/2026 Gestion des ressources cgroups v1/v2 avec LXC cgroups conteneurs lxc 23/04/2026 Centralisation logs avec ELK Stack — Elasticsearch, Kibana, Filebeat elasticsearch elk filebeat 23/04/2026 Supervision avec Zabbix 7.0 LTS sur Debian/Ubuntu debian monitoring supervision 23/04/2026 Plusieurs versions PHP-FPM sur Apache Debian/Ubuntu apache debian php-fpm 23/04/2026 Sécurisation avancée PHP-FPM — Multi-VirtualHosts Apache/Nginx apache nginx php-fpm 23/04/2026 Optimisation PHP-FPM — Guide de tuning d'un pool optimisation performance php-fpm 29/07/2025 Docker sur Debian/Ubuntu : Installation, Configuration et Utilisation conteneurs debian docker 03/07/2025 Serveur VPN WireGuard sous linux réseau sécurité vpn 03/07/2025 Authentification par clé publique sur un serveur SSH authentification cryptographie sécurité 27/06/2025 Surveillance et diagnostic d’un serveur Linux avec vmstat, iotop et htop diagnostic htop monitoring 27/06/2025 Mémoire : Utilisation des Huge Pages et implémentation hugepages mémoire noyau 27/06/2025 Mémoire Swap et paramétrage swappiness mémoire noyau performance 18/06/2025 Installation et Configuration des Conteneurs LXC sur Linux administration conteneurs lxc 18/06/2025 Gestion des journaux avec syslog et journalctl administration journalctl logs