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) :
1.2. Le Mode Strict (set -euo pipefail)
Placez ces options au tout début de chaque script :
| 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 :
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é :
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
readonlyen MAJUSCULES. - Variables locales dans les fonctions : Toujours déclarées avec
localpour éviter d'écraser des variables globales. - Variables d'environnement : Exportées avec
exportuniquement 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 :
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