Les briques de base pour écrire des scripts shell solides : variables, substitutions, tests, structures de contrôle, fonctions et options de robustesse. Les exemples ciblent Bash 4+ ; quand une construction n'est pas POSIX, c'est signalé.
Variables
Une variable shell n'a pas de type : tout est chaîne. Les règles à connaître concernent la déclaration (sans espaces), l'expansion (avec quotes pour la sécurité) et les nombreux modificateurs ${...}.
$VAR
Déclarer, lire et exporter une variable
Une affectation s'écrit sans espaces autour du =. Pour lire la valeur on préfixe par $, et pour les cas ambigus (concaténation avec du texte) on entoure le nom d'accolades.
Syntaxe
nom=valeur # affectation simple
nom="texte avec espaces" # toujours quoter si espaces ou caracteres speciaux
echo "$nom" # lecture
echo "${nom}_suffixe" # accolades obligatoires pour deslimiter le nom
export NOM=valeur # variable d'environnement (heritee par les enfants)
local nom=valeur # portee limitee a la fonction
readonly nom=valeur # interdit toute modification ulterieure
$ nom="Alice"
echo "Bonjour $nom"
Bonjour Alice
Affectation puis lecture. Les " double-quotes permettent l'expansion de $nom.
$ base=/var/log
echo "${base}_archive"
/var/log_archive
Sans accolades, $base_archive serait interprété comme la variable $base_archive.
$ export PATH="$HOME/bin:$PATH"
Ajoute ~/bin en tête du PATH et propage aux processus enfants.
Toujours quoter à l'expansion.echo $fichier casse si fichier contient un espace ou un joker. Préférer echo "$fichier". La règle d'or : quote everything unless you have a reason not to.
Les accolades autour d'un nom de variable acceptent des modificateurs très puissants : valeur par défaut si vide, longueur, sous-chaîne, suppression de préfixe ou suffixe, remplacement.
Valeur par défaut et obligatoire
${var:-def}
Renvoie def si var est vide ou non définie. Ne modifie pas var.
${var:=def}
Idem, mais affecte aussi def à var.
${var:?msg}
Affiche msg sur stderr et termine le script si var est vide.
${var:+x}
Renvoie x si var est définie et non vide, sinon vide. L'inverse de :-.
Longueur, sous-chaîne, manipulation
${#var}
Longueur en caractères.
${var:offset:length}
Sous-chaîne (offset 0-indexé). length est optionnel.
${var#prefix}
Supprime le plus court préfixe matchant. ## pour le plus long.
${var%suffix}
Supprime le plus court suffixe matchant. %% pour le plus long.
${var/pat/rep}
Remplace la première occurrence. ${var//pat/rep} pour toutes.
${var^^}
Met en MAJUSCULES. ${var,,} en minuscules. (Bash 4+)
$ port=${PORT:-8080}
echo "Ecoute sur $port"
Ecoute sur 8080
Utilise PORT si elle est définie et non vide ; sinon prend 8080. Le deux-points de ${PORT:-8080} traite également la chaîne vide comme une absence.
Insérer le résultat d'une commande au milieu d'une autre, ou faire un calcul entier sans appeler de programme externe.
$(cmd)
Capturer la sortie d'une commande dans une expression
$(cmd) remplace la substitution par la sortie standard de la commande en retirant tous les sauts de ligne finaux. Quoter l’expansion préserve les espaces et sauts de ligne internes. Les backticks sont une ancienne syntaxe, moins pratique à imbriquer.
Calcul entier sans expr ni bc. Les variables sont utilisables sans $ à l'intérieur. Ne gère que des entiers ; pour des décimales, utiliser bc -l ou awk.
Calcul entier sur deux variables. À l'intérieur de $(()), les variables s'utilisent sans $ (le contexte arithmétique sait que a est une variable). On affiche les quatre opérations : addition, multiplication, division entière (pas 2.33), modulo (le reste de 7÷3).
$ i=0
while (( i < 5 )); do echo $i; i=$((i + 1)); done
Compteur classique. (( i < 5 )) est une condition arithmétique : la boucle continue tant qu'elle est vraie. i=$((i + 1)) incrémente i sans déclencher set -e lorsque la valeur initiale vaut zéro, contrairement à ((i++)). Plus lisible que la version POSIX [ "$i" -lt 5 ] dès qu'on manipule des entiers.
Évaluer une condition pour if, while ou en ligne avec && / ||. Deux constructions au choix : la forme POSIX [ ... ] universelle, ou la forme bash [[ ... ]] plus sûre et plus expressive.
[ ... ]
Test POSIX (alias de la commande test)
[ est généralement un builtin du shell ; un exécutable externe existe aussi. Il reprend les tests de test mais exige un argument final ]. Il renvoie 0 si le test est vrai, 1 sinon et un autre statut en cas d’erreur de syntaxe. Les espaces séparant les arguments sont obligatoires.
Conditions sur les fichiers
-e fichier
Existe ; suit les liens symboliques, donc faux pour un lien cassé. -L teste le lien lui-même.
-f fichier
est un fichier régulier.
-d fichier
est un répertoire.
-L fichier
est un lien symbolique.
-r / -w / -x
lisible / modifiable / exécutable.
-s fichier
existe et est non vide.
fichier1 -nt fichier2
Teste si fichier1 est plus récent que fichier2 dans Bash et plusieurs autres shells. Vérifier les opérateurs disponibles si le script doit cibler d’anciennes implémentations de sh.
Comparaisons de chaînes et d'entiers
"$a" = "$b"
égalité de chaînes (POSIX). != pour différent.
-z "$a"
chaîne vide. -n pour non vide.
"$a" -eq "$b"
égalité numérique. -ne -lt -le -gt -ge pour les autres.
$ if [ -f "$config" ]; then
. "$config"
fi
Pattern « charger une config si elle est présente, ignorer sinon ». . (équivalent POSIX de source) exécute le fichier dans le shell courant : les variables qu'il définit deviennent disponibles. Sans le test -f en garde, le script aurait planté si le fichier n'existait pas.
$ [ -z "$nom" ] && nom="anonyme"
Forme courte : si $nom est vide, lui donner une valeur.
Toujours quoter les variables
dans [ ... ]. [ -z $var ] casse si $var contient un espace ; [ -z "$var" ] non. [[ ... ]] n'a pas ce piège.
Construction Bash, plus sûre que [ ] : pas de découpage de mots, pas de globbing intempestif, et opérateurs supplémentaires pour le pattern matching et les regex.
Opérateurs en plus de [ ]
"$a" == motif
Pattern matching (jokers * ? [...]). Le motif n'est pas quoted.
"$a" =~ regex
Regex étendue. Captures dans ${BASH_REMATCH[1..N]}.
cond1 && cond2
ET (au lieu de -a).
cond1 || cond2
OU (au lieu de -o).
$ if [[ "$nom" == *.log ]]; then echo "fichier log"; fi
Pattern matching : équivalent du globbing. Note que *.log n'est pas entre quotes.
$ if [[ "$ip" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "premier octet : ${BASH_REMATCH[1]}"
fi
Capture quatre groupes de chiffres séparés par des points dans BASH_REMATCH[1] à [4]. Cette regex ne vérifie pas que chaque nombre est compris entre 0 et 255 : elle ne valide donc pas une adresse IPv4 complète. Elle illustre la capture d’un format textuel.
Les blocs if, for, while, case. Bash utilise un mot-clé de fermeture miroir : fi, done, esac.
if
Branchement conditionnel
if teste une commande (souvent [[ ]] ou [ ]) et s'exécute selon son code de retour : 0 = succès = vrai, ≠ 0 = faux. Le bloc se ferme par fi.
Syntaxe
if condition; then
instructions
elif autre_condition; then
instructions
else
instructions
fi
$ if [[ -f "$fichier" ]]; then
echo "fichier present"
elif [[ -d "$fichier" ]]; then
echo "c'est un repertoire"
else
echo "rien a cet endroit"
fi
Test en cascade : on vérifie d'abord si $fichier est un fichier régulier (-f), sinon on regarde s'il s'agit d'un répertoire (-d). Sans match, on tombe dans le else. elif permet d'enchaîner plusieurs conditions sans imbriquer plusieurs if.
Itère sur les éléments d'une liste (mots séparés par espaces, fichiers issus d'un glob, lignes d'une commande, plage numérique). Bash propose aussi une forme C-like avec trois expressions.
Syntaxe
for var in liste; do
instructions
done
for ((i=0; i<10; i++)); do # forme C
instructions
done
$ for f in *.log; do
[ -f "$f" ] || continue
gzip -- "$f"
done
Compresse les fichiers réguliers .log. Le test évite de traiter le motif littéral *.log si aucun fichier ne correspond ; -- protège les noms commençant par un tiret.
$ for i in {1..5}; do echo "tentative $i"; done
{1..5} est une brace expansion : le shell la transforme en 1 2 3 4 5avant que for ne l'utilise. Variantes utiles : {a..z} pour les lettres, {1..10..2} pour un pas (1, 3, 5, 7, 9), {2024..2026} pour des années. Extension Bash, pas POSIX.
$ while IFS= read -r ligne || [ -n "$ligne" ]; do
printf 'lu : %s
' "$ligne"
done < fichier.txt
Parcourt les lignes sans découper les espaces ni interpréter les antislashs. Le test après read traite aussi une dernière ligne non terminée par un saut de ligne.
Anti-pattern classique :for f in $(ls) casse sur les noms à espaces et n'apporte rien de plus que for f in *. Toujours for f in * pour les fichiers, jamais via ls.
Boucler tant qu'une condition est vraie (et son inverse until)
while cond; do ...; done exécute le bloc tant que cond renvoie 0. until est l'inverse : tant qu'elle renvoie ≠ 0. Couplé à read, c'est l'idiome pour lire un flux ligne par ligne.
$ i=0
while (( i < 3 )); do
echo "$i"
i=$((i + 1))
done
0
1
2
Boucle classique avec compteur. (( )) pour évaluer arithmétiquement.
$ while IFS=: read -r user _ uid _ _ home _; do
[ "$uid" -ge 1000 ] && echo "$user -> $home"
done < /etc/passwd
Parcourt /etc/passwd ligne par ligne. IFS=: dit à read de découper sur :. read -r reçoit les 7 champs (utilisateur, mot de passe, uid, gid, commentaire, home, shell) — on n'utilise que user, uid et home, le _ sert pour les autres. Le filtre [ "$uid" -ge 1000 ] garde les UID supérieurs ou égaux à 1000, un seuil courant mais dépendant de la distribution. Cela peut inclure nobody et des comptes techniques : ce filtre ne garantit pas des comptes humains.
$ until ping -c1 -W1 host >/dev/null 2>&1; do
sleep 2
done
Boucle d'attente classique pour synchroniser un script sur un service distant. Tant que ping retourne une erreur (hôte injoignable), on dort 2 secondes et on retente. -c1 envoie un seul paquet, -W1 plafonne l'attente à 1 s par essai (au lieu des 5 s par défaut). Utile pour patienter qu'un conteneur Docker ou une VM démarre avant la suite du script.
L'équivalent de switch dans d'autres langages, en plus puissant car les branches sont des motifs glob (*, ?, [abc]) et peuvent être combinées avec |. Plus lisible qu'une cascade d'elif quand on teste la même variable.
Syntaxe
case "$var" in
motif1) instructions ;;
motif2|motif3) instructions ;;
*) instructions_par_defaut ;;
esac
Squelette d'un script type wrapper de service. $1 est le premier argument passé au script. La branche *) attrape tout ce qui ne correspond pas (y compris l'absence d'argument), affiche un message d'usage sur stderr et sort avec le code 1. C'est le pattern à reproduire pour tout script qui prend une commande en argument.
$ case "$fichier" in
*.tar.gz|*.tgz) tar xzf "$fichier" ;;
*.tar.bz2) tar xjf "$fichier" ;;
*.zip) unzip "$fichier" ;;
*) echo "format inconnu" >&2; exit 1 ;;
esac
Détecte le format d'archive d'après l'extension et appelle la commande de décompression adéquate. Avantage sur une cascade de if [[ ... == *.tar.gz ]] : lecture plus claire et ordre des branches immédiat. Le | liste plusieurs motifs équivalents (*.tar.gz et *.tgz) sur la même branche.
Découper un script en blocs réutilisables. Les fonctions bash n'ont pas de paramètres nommés, juste les variables positionnelles classiques $1, $2, $@.
function
Déclarer et utiliser une fonction
La déclaration nom() { ...; } est POSIX. Le mot-clé function et la commande local sont des extensions prises en charge par Bash, pas des garanties POSIX. "$@" préserve les arguments séparés ; $# donne leur nombre. return N termine la fonction avec un statut, pas une valeur arbitraire.
Syntaxe
nom() {
local var=valeur # local : reste dans la fonction
echo "premier arg : $1"
echo "tous : $@"
echo "nombre : $#"
return 0 # code de sortie (defaut : celui de la derniere instruction)
}
# Appel
nom arg1 arg2 "arg trois"
Variables positionnelles
$0
Nom du script (pas de la fonction).
$1 … $9
Arguments. Au-delà : ${10}.
"$@"
Tous les arguments comme liste de mots distincts (le bon choix dans 99 % des cas).
"$*"
Tous concaténés en une seule chaîne, séparés par le premier caractère d'IFS.
$#
Nombre d'arguments.
shift [N]
Décale les arguments de N (defaut 1) : $2 devient $1, etc.
$ log() {
local niveau=$1; shift
echo "[$(date +%H:%M:%S)] [$niveau] $*"
}
log INFO "demarrage"
log ERROR "echec lecture" "$fichier"
Wrapper de log standard. Le premier argument est le niveau, on le sauvegarde dans niveau puis shift le retire — $@ ne contient plus que le message. $* concatène tous les arguments restants en une seule chaîne séparée par des espaces, donc log ERROR "echec lecture" "$fichier" produit une ligne du genre [16:20:01] [ERROR] echec lecture /etc/passwd.
Bash est très permissif par défaut : un script continue après une commande qui échoue, utilise une variable vide comme si de rien n'était, et ignore les erreurs au milieu d'un pipe. Ces options changent ce comportement pour des scripts qui plantent vite et fort plutôt que silencieusement.
set -e
Sortir au premier code d'erreur non nul
Active errexit : provoque la sortie sur certaines erreurs non testées. Ses exceptions dépendent du contexte (conditions, listes logiques, fonctions appelées dans ces contextes, pipelines). Il ne remplace pas une gestion explicite des erreurs ; vérifier le comportement attendu avant de l’activer dans un script existant.
$ #!/usr/bin/env bash
set -e
cd /tmp/build
./configure
make
sudo make install
Si ./configure échoue, make ne sera pas lancé. Sans set -e, le script continuait au risque d'exécuter sudo make install sur un build invalide.
Limites :set -e ne s'applique pas aux commandes en condition de if, while ou until, ni aux commandes non finales des listes cmd1 || cmd2 et cmd1 && cmd2. La dernière commande reste concernée : true && false termine le script sous set -e. Pour autoriser un échec ponctuel : cmd || true.
Active nounset : référencer une variable jamais affectée provoque une erreur et termine un shell non interactif, même sans set -e. Évite les bugs liés aux fautes de frappe ou aux variables d'environnement absentes.
$ set -u
echo "$NON_DEFINIE"
bash: NON_DEFINIE: unbound variable
Avec set -u, tout accès à une variable jamais affectée fait sortir le script en erreur. Sans cette option, echo "$NON_DEFINIE" aurait silencieusement imprimé une ligne vide — bug classique quand on tape $user au lieu de $User. La détection précoce vaut largement l'obligation d'initialiser les optionnelles avec ${VAR:-}.
Échappatoire propre :
pour les variables vraiment optionnelles, utiliser la syntaxe ${VAR:-} qui renvoie une chaîne vide sans déclencher l'erreur.
Le code de sortie d'un pipe est celui du dernier maillon en échec à droite
Par défaut, cmd1 | cmd2 retourne le code de cmd2. Si cmd1 échoue mais cmd2 réussit, le pipe passe pour un succès. Avec pipefail, le statut du pipeline est celui de la commande la plus à droite dont le code est non nul, ou zéro si toutes réussissent. Par exemple, (exit 7) | (exit 3) renvoie 3. Le tableau Bash PIPESTATUS contient les statuts individuels ; le lire immédiatement après le pipeline.
$ set -o pipefail
curl -fsS http://example.test/data | jq .users
Si curl échoue (DNS, 404), jq recevrait juste un flux vide et renverrait un succès. Avec pipefail, le script voit l'échec de curl.
Combo standardset -euo pipefail combine trois options utiles mais ne constitue pas une garantie de robustesse. Par exemple, un producteur interrompu par head peut renvoyer SIGPIPE et faire échouer le pipeline. Tester les erreurs attendues explicitement ; set -x affiche les commandes après expansion et peut révéler des secrets.
Définir une action à l'arrivée d'un signal ou à la sortie
trap associe une action à un signal ou à un événement du shell. EXIT s’exécute lors de la sortie normale du shell et des erreurs qui le font sortir. Ne pas supposer qu’il couvre tous les signaux : installer aussi les gestionnaires INT, TERM et HUP nécessaires. SIGKILL et SIGSTOP ne peuvent pas être interceptés.
Syntaxe
trap 'instruction' SIGNAL [SIGNAL...]
trap - SIGNAL # restaurer le comportement par defaut
trap '' SIGNAL # ignorer le signal
mktemp crée un répertoire temporaire et son échec est traité avant de continuer. EXIT le nettoie à la sortie ; les gestionnaires INT, TERM et HUP provoquent une sortie avec le statut conventionnel adapté. SIGKILL, un crash système ou une coupure peuvent empêcher le nettoyage.
$ trap 'echo "interrompu" >&2; exit 130' INT
trap 'echo "termine" >&2; exit 143' TERM
Installe deux gestionnaires distincts : 130 = 128 + SIGINT (2), 143 = 128 + SIGTERM (15). Les deux affichent un message puis terminent le script.
Suit tous les liens symboliques et renvoie le chemin absolu réel. Idiome essentiel quand un script doit connaître son propre emplacement, par exemple pour charger un fichier de config voisin.
$ readlink -f /usr/bin/sh
/usr/bin/dash
Démontre que readlink -f suit toute la chaîne des liens symboliques jusqu'au fichier final. Sur Debian/Ubuntu, /usr/bin/sh pointe en fait vers dash — d'où le piège des scripts #!/bin/sh qui se comportent différemment d'un Bash. Sans le drapeau -f, readlink n'afficherait que la cible directe sans résoudre récursivement.
Pattern « répertoire du script ». Combine readlink -f (résout les liens), dirname (extrait le répertoire), cd && pwd (chemin canonique). Marche même si le script est appelé via un lien symbolique.
Portabilité :
Ces exemples ciblent GNU readlink sur Linux. La disponibilité et les options de readlink diffèrent selon le système et sa version ; sur macOS, vérifier le manuel local ou installer GNU coreutils et utiliser greadlink -f.
Lire l'entrée utilisateur, formater la sortie de manière prévisible, et parser les arguments d'un script en respectant les conventions POSIX.
read
Lire une ligne depuis stdin (ou afficher un prompt)
Builtin shell, toujours à utiliser avec -r (sinon les backslashes sont interprétés). Coupe sur $IFS par défaut (espaces/tabs/\n) : passer IFS= pour préserver les espaces.
Options
-r
Mode brut (raw) : ne pas interpréter les \. À mettre presque toujours.
-p "PROMPT"
Affiche un prompt avant de lire.
-s
Mode silencieux (caractères non échoés) — pour les mots de passe.
-t SEC
Timeout en secondes : sort en erreur si rien n'arrive.
-n N
Lit au maximum N caractères, ou moins si le délimiteur de ligne est rencontré ; -N N a un comportement différent dans Bash.
Confirmation interactive avec valeur par défaut sûre (vide = abandon). -p intègre le prompt dans read au lieu d'un echo -n séparé.
$ read -rsp "Mot de passe : " pass
echo
echo "Mot de passe saisi : ${#pass} caractères"
-s n'affiche pas ce que l'utilisateur tape. Le echo seul rajoute le saut de ligne manquant. Pattern standard pour collecter une passphrase sans la stocker dans ~/.bash_history.
$ while IFS=: read -r user _ uid _ _ home _; do
[ "$uid" -ge 1000 ] && echo "$user $home"
done < /etc/passwd
Lecture ligne-à-ligne avec champs nommés. IFS=: coupe sur :, les _ servent pour les colonnes ignorées. Pattern POSIX pour traverser un fichier structuré.
$ if read -rt 5 -p "Démarrer ? " rep; then
echo "Réponse : $rep"
else
echo "Timeout — démarrage par défaut"
fi
read échoue sur expiration, fin de fichier ou erreur ; la branche else regroupe ici ces cas. Le message Timeout est donc une simplification à adapter si l’on doit les distinguer.
Plus prévisible et plus portable qu'echo : pas d'arguments interprétés différemment selon l'OS, contrôle exact des largeurs, des alignements et des conversions. À privilégier dès qu'on dépasse echo "$x".
Tableau aligné. %-15s = chaîne alignée à gauche dans 15 caractères. Plus simple qu'printf '%-15s %s\n' … + column.
$ printf '%(%Y-%m-%dT%H:%M:%S%z)T\n' -1
2026-05-07T14:32:11+0200
Date ISO 8601 sans appeler date. -1 = maintenant. Plus rapide quand on en a besoin dans une boucle (pas de fork-exec à chaque fois).
$ printf 'cd %q && ./run.sh\n' "$dossier"
%q échappe la chaîne pour qu'elle puisse être ré-évaluée par le shell sans risque. Si $dossier contient des espaces ou des caractères spéciaux, ils sont protégés correctement — utile pour générer un script à exécuter ailleurs.
$ printf '%s\n' ligne1 ligne2 ligne3
ligne1
ligne2
ligne3
Quand le format est plus court que la liste d'arguments, il est réutilisé. Idiomatique pour générer N lignes à partir d'un tableau.
Builtin POSIX (à ne pas confondre avec getopt, qui est un binaire externe et accepte les options longues). Conçu pour les flags de type -v, -h, -f FILE. Le format de la chaîne d'options est : une lettre = un flag sans argument, une lettre suivie de : = flag avec argument obligatoire.
Variables peuplées par getopts
$opt (ou le nom choisi)
La lettre de l'option en cours, ou ? si inconnue.
$OPTARG
L'argument fourni à l'option (pour les options en :).
$OPTIND
Index de l'argument à traiter ensuite. Utile pour shift $((OPTIND-1)) à la fin.
Squelette complet. Le : initial dans ":vho:" active le mode silencieux (les erreurs passent par les cas \? et : au lieu de getopts qui imprime à stderr). shift $((OPTIND-1)) retire les options traitées, laissant $@ avec uniquement les arguments positionnels.
Pas d'options longues :getopts ne sait pas gérer --verbose. Pour les options longues et combinables (style GNU), utiliser le binairegetopt (paquet util-linux) qui produit une chaîne normalisée à reparser, ou ré-écrire un parser manuel avec un case sur $1.