Fonctions et scripts#
Écrire une commande ou deux dans le terminal est une chose. Écrire un script Bash que des humains pourront lire, maintenir et exécuter en production est une discipline à part entière. Ce chapitre porte sur l’art de structurer le code Bash : le shebang, les options de robustesse, la définition et l’organisation des fonctions, la gestion de la portée des variables, le passage d’arguments, les conventions de retour de valeur, et enfin la gestion des arguments en ligne de commande avec getopts. À la fin de ce chapitre, vous disposerez de tous les éléments pour écrire des scripts professionnels, lisibles et robustes.
Le shebang : première ligne du script#
La première ligne d’un script exécutable indique au noyau quel interpréteur utiliser pour l’exécuter. Cette ligne s’appelle le shebang (ou hashbang).
Définition 28 (Le shebang)
Le shebang est une séquence de deux caractères — #! — placée en tout début de fichier, immédiatement suivie du chemin vers l’interpréteur. Quand on exécute un fichier avec ./script.sh, le noyau Linux lit les deux premiers octets : s’il trouve #!, il extrait le chemin de l’interpréteur sur la même ligne et lance cet interpréteur en lui passant le script comme argument.
La forme recommandée pour les scripts Bash est :
#!/usr/bin/env bash
Plutôt que le chemin absolu `#!/bin/bash`. La commande `env` recherche `bash` dans le `PATH`, ce qui rend le script portable sur les systèmes où Bash n'est pas dans `/bin` (macOS avec Homebrew, Nix, certains BSD).
# À éviter (non portable)
#!/bin/bash
# Recommandé (portable)
#!/usr/bin/env bash
# Pour Python (même logique)
#!/usr/bin/env python3
# Pour un script POSIX sh (le plus portable, mais pas Bash)
#!/bin/sh
Remarque 26
Sur macOS, Bash est installé par défaut en version 3.2 dans /bin/bash (pour des raisons de licence GPLv2). Si l’on veut Bash 5.x (installé via Homebrew dans /opt/homebrew/bin/bash ou /usr/local/bin/bash), l’utilisation de #!/usr/bin/env bash avec un PATH correctement configuré garantit l’utilisation de la bonne version. Cette distinction devient importante quand le script utilise des fonctionnalités de Bash 4+ comme declare -A (tableaux associatifs) ou la normalisation de casse ${var,,}.
En-tête d’un script professionnel#
Un script destiné à être partagé, maintenu ou utilisé en production doit commencer par un bloc d’en-tête structuré. Cet en-tête documente le script et configure l’environnement d’exécution pour la robustesse.
#!/usr/bin/env bash
# ==============================================================================
# nom_script.sh — Description concise de ce que fait le script
#
# Usage :
# ./nom_script.sh [OPTIONS] <argument_obligatoire>
#
# Options :
# -h, --help Afficher cette aide et quitter
# -v, --verbose Mode verbeux
# -o, --output DIR Répertoire de sortie (défaut : .)
#
# Description :
# Explication plus longue du script, de ses dépendances, de ses effets
# de bord éventuels et de son comportement en cas d'erreur.
#
# Dépendances :
# - bash >= 4.0
# - curl, jq (pour les appels API)
#
# Auteur : Prénom Nom <email@exemple.com>
# Date : 2024-03-15
# Version : 1.2.0
# Licence : MIT
# ==============================================================================
# --- Options de robustesse ---
set -euo pipefail
IFS=$'\n\t'
Les options de robustesse : set -euo pipefail#
Ces trois options, combinées, transforment Bash en un environnement beaucoup moins permissif — et beaucoup plus sûr pour la production.
Définition 29 (Options de robustesse Bash)
Les options de robustesse les plus importantes sont :
set -e(ouset -o errexit) : arrêt immédiat si une commande retourne un code non nul. Sans cette option, Bash ignore silencieusement les erreurs et continue l’exécution — comportement dangereux dans un script automatisé.set -u(ouset -o nounset) : erreur sur les variables non définies. Sans cette option, une variable non définie est silencieusement traitée comme une chaîne vide, ce qui peut produire des effets désastreux (rm -rf "$répertoire/"si$répertoireest vide).set -o pipefail: le code de retour d’un pipeline est celui de la dernière commande qui a échoué, et non celui de la dernière commande. Sans cette option,commande_échouée | grep motifretourne 0 sigrepréussit, masquant l’échec.IFS=$'\n\t': redéfinir l”Internal Field Separator pour exclure l’espace. Évite le word splitting involontaire sur les noms de fichiers contenant des espaces.
# Démonstration de l'importance de set -euo pipefail
# SANS ces options (comportement par défaut dangereux)
bash << 'EOF'
variable_non_définie=""
rm -rf $répertoire/ # Si $répertoire est vide → rm -rf / (CATASTROPHIQUE)
EOF
# AVEC set -euo pipefail
bash << 'EOF'
set -euo pipefail
echo $répertoire_non_défini # ERREUR : variable non définie → arrêt immédiat
EOF
# Gérer les exceptions à set -e
set -e
# Méthode 1 : || true (ignorer l'erreur d'une commande spécifique)
grep "motif" fichier.txt || true # Ne pas arrêter si grep ne trouve rien
# Méthode 2 : vérifier manuellement
if grep -q "motif" fichier.txt; then
echo "Trouvé"
fi # grep peut retourner 1 (non trouvé) sans provoquer l'arrêt
# Méthode 3 : désactiver et réactiver localement
set +e
commande_pouvant_échouer
résultat=$?
set -e
Définir une fonction#
Bash reconnaît deux syntaxes équivalentes pour définir une fonction :
Définition 30 (Syntaxes de définition de fonction)
Les deux syntaxes équivalentes pour définir une fonction en Bash sont :
# Syntaxe 1 : avec le mot-clé function (non POSIX mais lisible)
function nom_fonction() {
commandes
}
# Syntaxe 2 : sans le mot-clé function (POSIX)
nom_fonction() {
commandes
}
Les deux formes sont équivalentes en Bash. La syntaxe avec function permet d’omettre les parenthèses (function nom { ... }), mais la forme avec parenthèses est recommandée pour la clarté. Contrairement à Python ou JavaScript, les fonctions Bash n’ont pas de paramètres formels dans leur définition : les arguments sont accédés via les variables positionnelles ($1, $2, etc.) exactement comme dans le script principal.
### Fonctions simples
```bash
#!/usr/bin/env bash
set -euo pipefail
# Fonction sans argument
afficher_date() {
echo "Nous sommes le $(date '+%A %d %B %Y à %H:%M')"
}
# Appel de la fonction
afficher_date
# Fonction avec arguments
saluer() {
local prénom="$1"
local politesse="${2:-Bonjour}" # Valeur par défaut
echo "$politesse, $prénom !"
}
saluer "Alice" # Bonjour, Alice !
saluer "Bob" "Bonsoir" # Bonsoir, Bob !
# Fonction utilitaire : afficher un message d'erreur sur stderr et quitter
die() {
local message="$1"
local code="${2:-1}" # Code de sortie, 1 par défaut
echo "ERREUR : $message" >&2
exit "$code"
}
# Utilisation
[[ -f "$1" ]] || die "Fichier non trouvé : $1" 2
# Fonction de journalisation avec horodatage
log() {
local niveau="${1:-INFO}"
local message="$2"
printf "[%s] [%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$niveau" "$message"
}
log "INFO" "Démarrage du script"
log "WARN" "Espace disque faible"
log "ERREUR" "Connexion refusée"
Arguments des fonctions#
Les fonctions Bash reçoivent leurs arguments de la même manière que les scripts : via les variables positionnelles $1, $2, etc. Ces variables sont locales à la fonction : elles masquent temporairement les arguments du script parent pendant l’exécution de la fonction.
Remarque 27
À l’intérieur d’une fonction, $1, $2, $#, $@ et $* font référence aux arguments de la fonction, pas aux arguments du script. En revanche, $0 conserve le nom du script (pas de la fonction). Pour accéder aux arguments du script depuis une fonction, il faut les avoir sauvegardés dans des variables avant l’appel, ou les passer explicitement comme arguments.
# Démonstration des arguments de fonction
traiter_fichiers() {
echo "Nombre d'arguments : $#"
echo "Premier argument : $1"
echo "Tous les arguments :"
for arg in "$@"; do
echo " - '$arg'"
done
}
traiter_fichiers "fichier 1.txt" "fichier2.txt" "répertoire/fichier3.log"
# Nombre d'arguments : 3
# Premier argument : fichier 1.txt
# Tous les arguments :
# - 'fichier 1.txt'
# - 'fichier2.txt'
# - 'répertoire/fichier3.log'
# Transmission des arguments du script à une fonction
trouver_et_traiter() {
local motif="$1"
local répertoire="${2:-.}" # Défaut : répertoire courant
find "$répertoire" -name "$motif" -type f | while IFS= read -r fichier; do
echo "Trouvé : $fichier"
done
}
trouver_et_traiter "*.log" "/var/log"
trouver_et_traiter "*.py" # Cherche dans le répertoire courant
Variables locales et portée#
La gestion de la portée des variables est cruciale en Bash. Par défaut, toutes les variables sont globales : une variable définie dans une fonction est visible dans le shell parent et vice versa.
Définition 31 (Portée des variables en Bash)
En Bash, la portée des variables est contrôlée par le mot-clé local :
Sans
local: la variable est globale. Toute modification dans une fonction affecte la variable du même nom dans le script parent.Avec
local: la variable est locale à la fonction courante et à ses appelées. Elle masque toute variable globale du même nom et disparaît quand la fonction retourne.
La forme local variable=valeur déclare et affecte en une seule instruction. local accepte les mêmes options que declare : local -r (readonly), local -i (entier), local -a (tableau).
# Démonstration de la portée des variables
variable_globale="globale"
fonction_démonstration() {
# Sans local : modifie la variable globale
variable_globale="modifiée par la fonction"
# Avec local : variable distincte, locale à la fonction
local variable_locale="je suis locale"
local -i compteur=0
echo "Dans la fonction :"
echo " variable_globale = $variable_globale"
echo " variable_locale = $variable_locale"
}
echo "Avant l'appel : variable_globale = $variable_globale"
fonction_démonstration
echo "Après l'appel : variable_globale = $variable_globale" # Modifiée !
# echo "$variable_locale" # Erreur ou vide : variable_locale n'existe plus
# BONNE PRATIQUE : toujours utiliser local pour les variables internes
calculer_somme() {
local -i total=0
local nombre
for nombre in "$@"; do
(( total += nombre ))
done
echo "$total" # Retourner la valeur via stdout
}
résultat=$(calculer_somme 10 20 30 40)
echo "Somme : $résultat" # 100
Variables globales dans les fonctions : declare -g#
# Déclarer une variable globale depuis une fonction (Bash 4.2+)
initialiser_config() {
declare -g CHEMIN_LOG="/var/log/mon_script.log"
declare -g NIVEAU_LOG="INFO"
declare -g VERSION="1.0.0"
declare -gA CONFIG # Tableau associatif global
CONFIG[timeout]=30
CONFIG[retries]=3
}
# Après l'appel, CHEMIN_LOG, NIVEAU_LOG, VERSION et CONFIG sont globaux
initialiser_config
echo "$CHEMIN_LOG"
echo "${CONFIG[timeout]}"
Valeurs de retour des fonctions#
Les fonctions Bash peuvent « retourner » des valeurs de deux manières fondamentalement différentes :
Définition 32 (Valeurs de retour en Bash)
En Bash, il existe deux mécanismes de retour de valeur pour les fonctions :
Code de retour (via
return N) : un entier entre 0 et 255. Conventionnellement, 0 signifie succès et toute valeur non nulle signifie échec. Ce code est accessible via$?après l’appel. C’est le mécanisme utilisé pour indiquer le succès ou l’échec d’une opération.Retour par stdout : la fonction écrit sa valeur sur stdout avec
echoouprintf, et l’appelant capture cette sortie avec la substitution de commandes$(). C’est la seule façon de retourner une chaîne arbitraire ou un nombre complexe.
Ces deux mécanismes sont complémentaires : une fonction peut retourner un code de statut (0/1) ET écrire un résultat sur stdout. L’appelant utilise $? pour le statut et $() pour la valeur.
# Retour d'un code de statut seulement
est_entier() {
local valeur="$1"
[[ "$valeur" =~ ^-?[0-9]+$ ]]
# [[ ]] retourne 0 (vrai) ou 1 (faux) comme code de sortie
# La fonction retourne automatiquement ce code
}
if est_entier "42"; then
echo "42 est un entier"
fi
if est_entier "abc"; then
echo "abc est un entier"
else
echo "abc n'est pas un entier"
fi
# Retour d'une valeur par stdout
obtenir_nom_utilisateur() {
local uid="$1"
getent passwd "$uid" | cut -d':' -f1
# ou : awk -F: -v uid="$uid" '$3 == uid {print $1}' /etc/passwd
}
nom=$(obtenir_nom_utilisateur 1000)
echo "Utilisateur 1000 : $nom"
# Combiner code de retour et valeur stdout
trouver_fichier() {
local nom="$1"
local résultat
résultat=$(find / -name "$nom" -type f 2>/dev/null | head -1)
if [[ -n "$résultat" ]]; then
echo "$résultat"
return 0 # Succès
else
return 1 # Échec : fichier non trouvé
fi
}
if chemin=$(trouver_fichier "passwd"); then
echo "Trouvé : $chemin"
else
echo "Fichier non trouvé"
fi
# Retour de plusieurs valeurs avec des variables globales
# (moins élégant mais parfois nécessaire)
analyser_fichier() {
local fichier="$1"
# Utiliser declare -g ou des variables globales conventionnelles
RÉSULTAT_LIGNES=$(wc -l < "$fichier")
RÉSULTAT_MOTS=$(wc -w < "$fichier")
RÉSULTAT_TAILLE=$(stat -c%s "$fichier")
}
analyser_fichier "/etc/passwd"
echo "Lignes : $RÉSULTAT_LIGNES, Mots : $RÉSULTAT_MOTS, Taille : $RÉSULTAT_TAILLE"
Rendre un script exécutable#
Un script Bash est un fichier texte ordinaire. Pour pouvoir l’exécuter directement (avec ./script.sh), il faut deux conditions : que le shebang soit correct et que les permissions d’exécution soient définies.
# Rendre un script exécutable
chmod +x mon_script.sh
# Permissions recommandées pour un script privé
chmod 700 mon_script.sh # Lecture, écriture, exécution pour le propriétaire
# Permissions pour un script partagé
chmod 755 mon_script.sh # Exécution pour tous, modification pour le propriétaire
# Vérifier les permissions
ls -la mon_script.sh
# -rwxr-xr-x 1 alice alice 1234 mars 15 14:30 mon_script.sh
# Exécuter directement (nécessite le shebang et chmod +x)
./mon_script.sh arg1 arg2
# Exécuter sans chmod +x (pas besoin du shebang)
bash mon_script.sh arg1 arg2
# Exécuter depuis n'importe où si dans le PATH
# (copier ou créer un lien symbolique dans ~/bin ou /usr/local/bin)
cp mon_script.sh ~/bin/mon_script
# ou :
ln -s "$(pwd)/mon_script.sh" ~/bin/mon_script
Ajouter ~/bin au PATH#
# Dans ~/.bashrc ou ~/.bash_profile
if [[ -d "$HOME/bin" ]]; then
export PATH="$HOME/bin:$PATH"
fi
# Ou plus complètement
mkdir -p "$HOME/bin"
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
source vs exécution dans un sous-shell#
Il existe deux façons fondamentalement différentes d”« exécuter » un script, avec des implications importantes sur les variables et l’environnement.
```{prf:definition} source vs sous-shell
:label: definition-10-06
Exécution dans un sous-shell (
./script.sh,bash script.sh) : le script s’exécute dans un nouveau processus enfant. Il hérite des variables exportées du shell parent, mais toutes les modifications qu’il effectue (nouvelles variables,cd,export) restent confinées au sous-shell. Quand le script se termine, ces modifications disparaissent.Source (
source script.shou. script.sh) : le script est exécuté dans le shell courant, sans créer de processus enfant. Toutes les modifications — définition de variables, changement de répertoire, définition de fonctions, modifications duPATH— affectent directement le shell courant. C’est le mécanisme utilisé pour charger des fonctions ou des configurations (~/.bashrc,~/.bash_profile).
```bash
# Démonstration de la différence
# script_test.sh :
# #!/usr/bin/env bash
# MA_VARIABLE="définie dans le script"
# cd /tmp
# function ma_fonction() { echo "je suis une fonction"; }
# Exécution normale : les changements ne persistent pas
bash script_test.sh
echo $MA_VARIABLE # Vide : variable non visible dans le shell courant
ma_fonction # ERREUR : fonction non définie
pwd # Toujours le répertoire original
# Source : les changements persistent dans le shell courant
source script_test.sh
echo $MA_VARIABLE # "définie dans le script"
ma_fonction # "je suis une fonction"
pwd # /tmp (le cd a pris effet)
# Utilisation typique : charger des fonctions utilitaires
# Dans le script principal :
source ./lib/fonctions_communes.sh
source ./lib/fonctions_réseau.sh
# Les fonctions définies dans ces fichiers sont maintenant disponibles
Organisation en bibliothèques de fonctions#
# lib/log.sh — Bibliothèque de journalisation
# À charger avec : source lib/log.sh
LOG_NIVEAU="${LOG_NIVEAU:-INFO}"
LOG_FICHIER="${LOG_FICHIER:-/dev/stderr}"
log_debug() {
[[ "$LOG_NIVEAU" == "DEBUG" ]] || return 0
printf "[%s] [DEBUG] %s\n" "$(date '+%H:%M:%S')" "$*" >> "$LOG_FICHIER"
}
log_info() {
printf "[%s] [INFO] %s\n" "$(date '+%H:%M:%S')" "$*" >> "$LOG_FICHIER"
}
log_warn() {
printf "[%s] [WARN] %s\n" "$(date '+%H:%M:%S')" "$*" >&2
}
log_error() {
printf "[%s] [ERROR] %s\n" "$(date '+%H:%M:%S')" "$*" >&2
}
# Dans le script principal
source "$(dirname "$0")/lib/log.sh"
log_info "Démarrage"
log_warn "Espace disque faible"
Gestion des arguments avec getopts#
Pour les scripts acceptant des options en ligne de commande (-v, -o fichier, --help), Bash fournit la commande intégrée getopts qui analyse les arguments selon la convention POSIX.
```{prf:definition} La commande getopts
:label: definition-10-07
getopts OPTSTRING VARIABLE analyse les arguments positionnels ($@) à la recherche d’options. À chaque appel (dans une boucle while), elle :
Lit la prochaine option dans
$@.Stocke la lettre de l’option dans
VARIABLE.Si l’option attend un argument (marqué par
:après la lettre dansOPTSTRING), stocke l’argument dans$OPTARG.Retourne 0 tant qu’il reste des options, 1 quand toutes ont été traitées.
$OPTINDpointe vers le premier argument non-option après le traitement.
La forme OPTSTRING est une chaîne de lettres. Chaque lettre est une option valide. Un : après une lettre indique que l’option attend un argument. Un : initial dans OPTSTRING active le mode silencieux (gestion des erreurs manuelle).
### Patron complet de gestion des arguments
```bash
#!/usr/bin/env bash
set -euo pipefail
# --- Variables par défaut ---
VERBEUX=false
RÉPERTOIRE_SORTIE="."
NOMBRE_MAX=10
FICHIER_CONFIG=""
# --- Fonction d'aide ---
afficher_aide() {
cat <<EOF
Usage : $(basename "$0") [OPTIONS] <fichier_entrée>
OPTIONS :
-h Afficher cette aide et quitter
-v Mode verbeux
-o DIR Répertoire de sortie (défaut : répertoire courant)
-n NOMBRE Nombre maximum de lignes (défaut : 10)
-c FICHIER Fichier de configuration
ARGUMENTS :
fichier_entrée Fichier à traiter (obligatoire)
EXEMPLES :
$(basename "$0") données.csv
$(basename "$0") -v -o /tmp/résultats -n 50 données.csv
$(basename "$0") -c config.yaml -v données.csv
EOF
}
# --- Analyse des options ---
while getopts ":hvo:n:c:" option; do
case "$option" in
h)
afficher_aide
exit 0
;;
v)
VERBEUX=true
;;
o)
RÉPERTOIRE_SORTIE="$OPTARG"
;;
n)
if ! [[ "$OPTARG" =~ ^[0-9]+$ ]]; then
echo "ERREUR : -n attend un entier positif, reçu : '$OPTARG'" >&2
exit 1
fi
NOMBRE_MAX="$OPTARG"
;;
c)
FICHIER_CONFIG="$OPTARG"
;;
:)
echo "ERREUR : l'option -$OPTARG attend un argument" >&2
afficher_aide >&2
exit 1
;;
\?)
echo "ERREUR : option inconnue : -$OPTARG" >&2
afficher_aide >&2
exit 1
;;
esac
done
# Déplacer OPTIND pour accéder aux arguments non-options
shift $((OPTIND - 1))
# --- Vérification des arguments obligatoires ---
if [[ $# -eq 0 ]]; then
echo "ERREUR : le fichier d'entrée est obligatoire" >&2
afficher_aide >&2
exit 1
fi
FICHIER_ENTRÉE="$1"
# --- Vérifications ---
[[ -f "$FICHIER_ENTRÉE" ]] || { echo "ERREUR : '$FICHIER_ENTRÉE' n'existe pas" >&2; exit 2; }
[[ -r "$FICHIER_ENTRÉE" ]] || { echo "ERREUR : '$FICHIER_ENTRÉE' non lisible" >&2; exit 2; }
[[ -d "$RÉPERTOIRE_SORTIE" ]] || mkdir -p "$RÉPERTOIRE_SORTIE"
# --- Traitement ---
$VERBEUX && echo "Traitement de : $FICHIER_ENTRÉE"
$VERBEUX && echo "Sortie vers : $RÉPERTOIRE_SORTIE"
$VERBEUX && echo "Nombre max : $NOMBRE_MAX"
head -n "$NOMBRE_MAX" "$FICHIER_ENTRÉE" > "$RÉPERTOIRE_SORTIE/résultat.txt"
echo "Traitement terminé : $RÉPERTOIRE_SORTIE/résultat.txt"
Gestion des options longues avec getopt (GNU)#
getopts (built-in POSIX) ne supporte pas les options longues (--help, --output). Pour cela, on utilise la commande externe getopt (GNU coreutils) :
#!/usr/bin/env bash
set -euo pipefail
# Analyser les options avec getopt (GNU)
OPTS=$(getopt \
--options hvo:n: \
--longoptions help,verbose,output:,nombre: \
--name "$(basename "$0")" \
-- "$@") || { afficher_aide >&2; exit 1; }
eval set -- "$OPTS"
VERBEUX=false
SORTIE="."
NOMBRE=10
while true; do
case "$1" in
-h | --help)
afficher_aide
exit 0
;;
-v | --verbose)
VERBEUX=true
shift
;;
-o | --output)
SORTIE="$2"
shift 2
;;
-n | --nombre)
NOMBRE="$2"
shift 2
;;
--)
shift
break
;;
*)
echo "Option interne non gérée : $1" >&2
exit 1
;;
esac
done
Structurer un script complet#
Voici un exemple de script bien structuré incorporant toutes les bonnes pratiques :
#!/usr/bin/env bash
# ==============================================================================
# sauvegarder.sh — Sauvegarde incrémentale d'un répertoire
# ==============================================================================
set -euo pipefail
IFS=$'\n\t'
# --- Constantes ---
readonly NOM_SCRIPT="$(basename "$0")"
readonly VERSION="2.1.0"
readonly HORODATAGE="$(date +%Y%m%d_%H%M%S)"
# --- Valeurs par défaut ---
VERBEUX=false
COMPRESSION=true
DESTINATION="/backup"
RÉTENTION_JOURS=30
# --- Fonctions utilitaires ---
log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"; }
warn() { printf '[%s] WARN: %s\n' "$(date '+%H:%M:%S')" "$*" >&2; }
erreur() {
printf '[%s] ERREUR: %s\n' "$(date '+%H:%M:%S')" "$*" >&2
exit "${2:-1}"
}
afficher_aide() {
cat <<EOF
$NOM_SCRIPT v$VERSION — Sauvegarde incrémentale
Usage : $NOM_SCRIPT [OPTIONS] <source>
OPTIONS :
-d DIR Destination (défaut : $DESTINATION)
-j N Rétention en jours (défaut : $RÉTENTION_JOURS)
-c Désactiver la compression
-v Mode verbeux
-h Aide
EOF
}
vérifier_prérequis() {
local manquants=()
for outil in rsync tar gzip; do
command -v "$outil" &>/dev/null || manquants+=("$outil")
done
if (( ${#manquants[@]} > 0 )); then
erreur "Outils manquants : ${manquants[*]}"
fi
}
nettoyer_anciennes_sauvegardes() {
local destination="$1"
local jours="$2"
log "Nettoyage des sauvegardes de plus de $jours jours..."
find "$destination" -name "*.tar.gz" -mtime +"$jours" -delete
$VERBEUX && log "Nettoyage terminé"
}
effectuer_sauvegarde() {
local source="$1"
local destination="$2"
local archive="${destination}/sauvegarde_${HORODATAGE}"
mkdir -p "$destination"
log "Sauvegarde de '$source' vers '$archive'..."
if $COMPRESSION; then
tar czf "${archive}.tar.gz" -C "$(dirname "$source")" "$(basename "$source")"
log "Archive créée : ${archive}.tar.gz ($(du -sh "${archive}.tar.gz" | cut -f1))"
else
tar cf "${archive}.tar" -C "$(dirname "$source")" "$(basename "$source")"
log "Archive créée : ${archive}.tar"
fi
}
# --- Analyse des options ---
while getopts ":d:j:cvh" opt; do
case "$opt" in
d) DESTINATION="$OPTARG" ;;
j) RÉTENTION_JOURS="$OPTARG" ;;
c) COMPRESSION=false ;;
v) VERBEUX=true ;;
h) afficher_aide; exit 0 ;;
:) erreur "L'option -$OPTARG nécessite un argument" ;;
\?) erreur "Option inconnue : -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
# --- Validation ---
[[ $# -ge 1 ]] || erreur "Le répertoire source est obligatoire"
SOURCE="$1"
[[ -d "$SOURCE" ]] || erreur "'$SOURCE' n'est pas un répertoire"
# --- Exécution ---
vérifier_prérequis
effectuer_sauvegarde "$SOURCE" "$DESTINATION"
nettoyer_anciennes_sauvegardes "$DESTINATION" "$RÉTENTION_JOURS"
log "Sauvegarde terminée avec succès"
Visualisation : cycle de vie d’un script Bash#
Résumé#
Dans ce chapitre, nous avons acquis les compétences nécessaires pour écrire des scripts Bash professionnels et robustes :
Le shebang
#!/usr/bin/env bashest préférable à#!/bin/bashpour la portabilité. Il indique au noyau quel interpréteur utiliser pour exécuter le script.L”en-tête structuré documente l’usage, les options, les dépendances et l’auteur. Les options
set -euo pipefailetIFS=$'\n\t'rendent le script robuste face aux erreurs silencieuses.Les fonctions se définissent avec
nom() { ... }oufunction nom() { ... }. Les arguments sont accessibles via$1,$@,$#— exactement comme dans le script principal.Le mot-clé
localest fondamental : sans lui, toutes les variables de la fonction sont globales et peuvent corrompre l’état du script. La règle est simple : toujours déclarerlocaltoutes les variables internes à une fonction.Les fonctions retournent soit un code de statut (0-255) via
return, soit une chaîne viaechocapturée avec$(). Les deux mécanismes sont complémentaires.chmod +xrend un script exécutable. L’ajout de~/binauPATHpermet de lancer les scripts depuis n’importe où.sourceexécute un script dans le shell courant (les modifications persistent) ; l’exécution normale crée un sous-shell (rien ne persiste).sourceest utilisé pour charger des bibliothèques de fonctions.getoptsanalyse les options POSIX (-v,-o valeur) de manière robuste. Pour les options longues (--verbose,--output), on utilise la commande externegetopt(GNU).
Dans le chapitre suivant, nous approfondirons les tableaux et la manipulation de chaînes : tableaux indexés, tableaux associatifs, et toute la richesse des opérateurs d’expansion de variables Bash pour transformer les chaînes sans outils externes.