Un script Bash capable d’exécuter des commandes est utile.
Un script capable de décider quelles commandes exécuter devient nettement plus intéressant.
C’est le rôle des tests conditionnels :
le fichier existe ?
le service fonctionne ?
l'utilisateur est root ?
la variable est vide ?
le nombre dépasse une limite ?
la commande précédente a réussi ?
Selon la réponse, le script peut :
- continuer ;
- choisir une autre action ;
- afficher une erreur ;
- quitter ;
- retenter une opération ;
- ou décider prudemment de ne pas supprimer la moitié du serveur.
En Bash, une condition n’est fondamentalement pas « vraie » ou « fausse » comme une valeur booléenne abstraite : elle repose très souvent sur le statut de sortie d’une commande.
Le principe fondamental : 0 signifie succès
Sous Unix et dans Bash, une commande retourne un :
exit status
code de retour
statut de sortie
Par convention :
0
→ succès / vrai dans un contexte conditionnel
et :
valeur non nulle
→ échec / faux dans un contexte conditionnel
Cela peut sembler inversé à quelqu’un venant d’un langage dans lequel :
0 = false
1 = true
Mais le shell utilise une logique différente :
un seul code représente le succès
plusieurs codes peuvent décrire différents échecs
Exemple simple
La commande :
true
retourne :
0
La commande :
false
retourne un statut non nul.
On peut donc écrire :
if true; then
printf '%s\n' "Cette branche sera exécutée."
fi
Et :
if false; then
printf '%s\n' "Vous ne verrez normalement pas ceci."
fi
La vraie syntaxe de if
On présente souvent :
if [ condition ]; then
commandes
fi
comme si :
[ ... ]
faisait partie obligatoirement de la syntaxe de :
if
Ce n’est pas le cas.
La structure conceptuelle est plutôt :
if commandes_de_test; then
commandes_si_succès
fi
Bash exécute les commandes de test.
Si leur statut final vaut :
0
la branche :
then
est exécutée.
Tester directement une commande
Par exemple :
if systemctl is-active --quiet ssh; then
printf '%s\n' "SSH est actif."
fi
Il n’y a :
aucun crochet
et pourtant il s’agit d’un test conditionnel parfaitement valide.
Structure de base
if commande; then
action
fi
Avec else
if commande; then
action_si_succès
else
action_si_échec
fi
Avec elif
if condition1; then
action1
elif condition2; then
action2
else
action_par_defaut
fi
Bash teste les conditions dans l’ordre.
Dès qu’une condition réussit :
sa branche est exécutée
↓
les elif suivants sont ignorés
Exemple concret
charge=75
if (( charge >= 90 )); then
printf '%s\n' "Charge critique."
elif (( charge >= 70 )); then
printf '%s\n' "Charge élevée."
elif (( charge >= 40 )); then
printf '%s\n' "Charge normale."
else
printf '%s\n' "Charge faible."
fi
L’ordre des conditions compte
Ce code serait incorrect :
if (( charge >= 40 )); then
printf '%s\n' "Charge normale."
elif (( charge >= 90 )); then
printf '%s\n' "Charge critique."
fi
Avec :
charge=95
la première condition :
95 >= 40
réussit déjà.
La condition :
95 >= 90
ne sera jamais examinée.
Les cas les plus spécifiques ou restrictifs doivent donc souvent apparaître avant les cas plus généraux.
Trois grandes manières de tester en Bash
Dans un script Bash moderne, vous rencontrerez principalement :
| Construction | Usage |
|---|---|
[ ... ] / test |
Tests traditionnels, largement portables |
[[ ... ]] |
Tests conditionnels étendus propres à Bash et shells compatibles |
(( ... )) |
Tests et calculs arithmétiques |
Il faut ajouter à cela une quatrième possibilité extrêmement importante :
tester directement une commande
La commande test
Bash possède un builtin :
test
Par exemple :
if test -f "/etc/passwd"; then
printf '%s\n' "/etc/passwd existe."
fi
La commande [
Cette syntaxe :
if [ -f "/etc/passwd" ]; then
printf '%s\n' "Fichier trouvé."
fi
est essentiellement une autre forme de :
test -f "/etc/passwd"
Dans Bash, :
[
est un builtin.
Le :
]
final est un argument requis pour terminer cette forme de la commande.
Pourquoi les espaces autour des crochets sont obligatoires
Correct :
if [ "$nom" = "Alice" ]; then
...
fi
Incorrect :
if ["$nom" = "Alice"]; then
...
fi
Pourquoi ?
Parce que Bash doit analyser des mots séparés :
[
"$nom"
=
"Alice"
]
Sans espaces, il peut chercher une commande portant un nom ressemblant à :
[Alice
ce qui ne correspond généralement pas au projet initial.
[ ] ou [[ ]] ?
Dans un script explicitement Bash :
#!/bin/bash
[[ ... ]] est généralement très confortable.
Exemple :
if [[ "$nom" == "Alice" ]]; then
printf 'Bonjour %s.\n' "$nom"
fi
Avantages de [[ ]]
À l’intérieur de :
[[ ... ]]
Bash n’effectue pas le :
- word splitting ;
- filename expansion classique ;
sur les mots de l’expression.
Cette construction prend également en charge directement :
&&;||;!;- comparaison par motifs ;
- expressions régulières avec
=~.
Mais [[ ]] n’est pas POSIX
Un script annoncé :
#!/bin/sh
ne devrait pas supposer que :
[[ ... ]]
existe.
Si vous utilisez cette construction :
écrivez réellement un script Bash
et utilisez un shebang cohérent.
Faut-il citer les variables dans [[ ]] ?
Cette construction Bash est sûre contre le word splitting :
[[ -n $nom ]]
même si :
$nom
contient des espaces.
Vous verrez néanmoins souvent :
[[ -n "$nom" ]]
Les deux fonctionnent dans ce cas.
Conserver les guillemets sur les expansions simples peut améliorer la cohérence visuelle du script.
Mais avec :
[[ ... ]]
le placement des guillemets peut parfois modifier volontairement la sémantique, notamment pour :
- les patterns ;
- les regex.
Il faut donc comprendre ce que l’on cite.
Tests sur les fichiers
Bash possède de nombreux opérateurs permettant d’interroger un chemin.
| Test | Signification |
|---|---|
-e chemin |
Le chemin existe selon le test effectué |
-f chemin |
Fichier régulier |
-d chemin |
Répertoire |
-L chemin |
Lien symbolique |
-r chemin |
Lisible pour le processus |
-w chemin |
Inscriptible pour le processus |
-x chemin |
Exécutable ou traversable selon le type |
-s chemin |
Existe et possède une taille supérieure à zéro |
-b chemin |
Périphérique bloc |
-c chemin |
Périphérique caractère |
-p chemin |
FIFO / tube nommé |
-S chemin |
Socket |
Tester un fichier régulier
fichier="/etc/passwd"
if [[ -f "$fichier" ]]; then
printf '%s est un fichier régulier.\n' "$fichier"
else
printf '%s est absent ou n’est pas un fichier régulier.\n' \
"$fichier"
fi
Tester un répertoire
repertoire="/var/log"
if [[ -d "$repertoire" ]]; then
printf 'Répertoire trouvé : %s\n' "$repertoire"
fi
Tester simplement l’existence
if [[ -e "$chemin" ]]; then
printf '%s existe.\n' "$chemin"
fi
Mais :
existe
ne signifie pas :
est un fichier régulier
Le chemin peut représenter :
- un répertoire ;
- un périphérique ;
- un socket ;
- un autre type d’objet.
Liens symboliques : subtilité importante
La plupart des tests de fichiers suivent le lien symbolique et examinent sa cible.
Supposons :
lien
→ fichier.txt
Alors :
[[ -f lien ]]
peut être vrai si :
fichier.txt
est un fichier régulier.
Pour savoir si :
lien
est lui-même un lien symbolique :
if [[ -L "$chemin" ]]; then
printf 'Lien symbolique.\n'
fi
Le lien symbolique cassé
Supposons :
lien
→ /fichier/qui/n'existe/plus
Le lien existe comme entrée de système de fichiers, mais sa cible est absente.
Un test :
[[ -e "$lien" ]]
peut être faux parce que la cible ne peut plus être résolue.
Pour détecter spécifiquement le lien :
if [[ -L "$lien" ]]; then
printf 'Le lien symbolique existe.\n'
fi
Tester les permissions apparentes d’accès
if [[ -r "$fichier" ]]; then
printf 'Le processus peut lire le fichier.\n'
fi
if [[ -w "$fichier" ]]; then
printf 'Le processus peut écrire dans le fichier.\n'
fi
if [[ -x "$fichier" ]]; then
printf 'Le test d’exécution réussit.\n'
fi
-r, -w et -x ne lisent pas simplement les trois caractères de ls
Ces tests cherchent à savoir si l’accès correspondant est disponible dans le contexte du processus.
Ils ne doivent donc pas être compris comme :
« Le bit propriétaire
r,wouxest-il présent ? »
Le résultat peut dépendre notamment :
- de l’utilisateur effectif ;
- des groupes ;
- des permissions ;
- des ACL ;
- du contexte du système.
Écrire dans un fichier dépend aussi du répertoire
Même si :
[[ -w fichier ]]
est vrai, certaines opérations comme :
supprimer
renommer
dépendent largement des permissions du :
répertoire parent
et non du seul fichier.
Ne transformez donc pas :
-w fichier
en test universel répondant à :
« Puis-je faire absolument ce que je veux avec ce fichier ? »
Tester un fichier non vide
if [[ -s "$fichier" ]]; then
printf 'Le fichier contient au moins un octet.\n'
fi
Attention :
-s
ne signifie pas :
« Ce fichier contient des données valides. »
Un fichier de 1 octet contenant :
X
est non vide.
Sa valeur documentaire reste discutable.
Autres tests de fichiers utiles
| Test | Utilité |
|---|---|
-O fichier |
Le fichier appartient à l’UID effectif |
-G fichier |
Le groupe du fichier correspond à un groupe effectif |
-N fichier |
Le fichier a été modifié depuis sa dernière lecture selon les informations disponibles |
f1 -nt f2 |
f1 est plus récent que f2 |
f1 -ot f2 |
f1 est plus ancien que f2 |
f1 -ef f2 |
Les deux noms désignent le même fichier |
Comparer les dates de fichiers
if [[ "$source" -nt "$destination" ]]; then
printf 'La source est plus récente.\n'
fi
Cette construction peut être utile pour certains petits outils.
Pour une vraie synchronisation complexe :
rsync
possède heureusement déjà quelques décennies de travail que vous n’êtes pas obligé de réimplémenter un vendredi soir.
Tester si deux chemins désignent le même fichier
if [[ "$fichier1" -ef "$fichier2" ]]; then
printf 'Même fichier sous-jacent.\n'
fi
Cela peut notamment être vrai avec :
- deux chemins différents ;
- des liens physiques ;
- certaines résolutions de chemins.
Tests sur les chaînes
Les chaînes représentent une grande partie des conditions Bash.
Les tests fondamentaux sont :
| Test | Signification |
|---|---|
-z chaine |
Chaîne vide |
-n chaine |
Chaîne non vide |
s1 = s2 |
Égalité selon la construction utilisée |
s1 == s2 |
Égalité Bash / pattern dans certains contextes |
s1 != s2 |
Différence |
Chaîne vide
read -r -p "Quel est votre nom ? " nom
if [[ -z "$nom" ]]; then
printf '%s\n' "Le nom est vide."
else
printf 'Bonjour, %s.\n' "$nom"
fi
Chaîne non vide
if [[ -n "$nom" ]]; then
printf 'Nom : %s\n' "$nom"
fi
Dans [ ], utilisez = pour une portabilité POSIX
Dans Bash, cette forme fonctionne :
[ "$nom" == "Alice" ]
Mais pour du shell traditionnel portable, la forme recommandée est :
[ "$nom" = "Alice" ]
Dans un script Bash utilisant :
[[ ... ]]
on écrit très couramment :
[[ "$nom" == "Alice" ]]
Pourquoi les guillemets sont particulièrement importants avec [ ]
Évitez :
[ $nom = "root" ]
Si :
nom=""
la commande peut recevoir un nombre d’arguments différent de celui attendu.
Préférez :
[ "$nom" = "root" ]
Dans :
[[ ... ]]
le word splitting ne pose pas le même problème, mais citer les expansions simples reste souvent lisible :
[[ "$nom" == "root" ]]
Comparaison littérale avec [[ ]]
Si vous voulez tester précisément :
admin
vous pouvez écrire :
if [[ "$nom" == "admin" ]]; then
printf '%s\n' "Nom exact."
fi
Le membre droit non cité peut être un pattern
Avec :
[[ ... == ... ]]
la partie droite non citée peut être interprétée comme un motif.
Par exemple :
fichier="rapport-2026.txt"
if [[ "$fichier" == *.txt ]]; then
printf '%s\n' "Extension .txt"
fi
Ici :
*.txt
est volontairement un pattern.
Quoter le pattern le rend littéral
Comparez :
[[ "$fichier" == *.txt ]]
avec :
[[ "$fichier" == "*.txt" ]]
Dans le premier cas :
*.txt
est un motif.
Dans le second :
Bash cherche littéralement la chaîne :
*.txt
Patterns utiles
[[ "$fichier" == *.log ]]
[[ "$nom" == admin* ]]
[[ "$code" == ??? ]]
[[ "$lettre" == [A-Z] ]]
Ces motifs utilisent les règles de pattern matching du shell.
Ce ne sont pas des expressions régulières.
Pattern et regex ne sont pas la même chose
Le motif :
*.txt
signifie grossièrement :
« zéro ou plusieurs caractères, suivis de .txt »
Une expression régulière équivalente serait plutôt :
.*[.]txt$
Les syntaxes sont différentes.
Expressions régulières avec =~
Bash fournit :
=~
dans :
[[ ... ]]
pour tester une expression régulière étendue POSIX.
Exemple :
valeur="12345"
if [[ "$valeur" =~ ^[0-9]+$ ]]; then
printf '%s\n' "Uniquement des chiffres."
fi
Valider un entier positif
read -r -p "Entrez un entier positif : " valeur
if [[ "$valeur" =~ ^[0-9]+$ ]]; then
printf 'Valeur valide : %s\n' "$valeur"
else
printf 'Valeur invalide.\n' >&2
exit 2
fi
Valider un entier signé
if [[ "$valeur" =~ ^-?[0-9]+$ ]]; then
printf '%s\n' "Entier syntaxiquement valide."
fi
Attention aux guillemets autour d’une regex
Cette construction :
[[ "$valeur" =~ ^[0-9]+$ ]]
utilise réellement une regex.
Si vous écrivez :
[[ "$valeur" =~ "^[0-9]+$" ]]
les caractères cités perdent leur fonction spéciale de regex et sont traités littéralement.
C’est un piège très fréquent.
Stocker une regex dans une variable
Pour une expression plus complexe :
regex='^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
if [[ "$date" =~ $regex ]]; then
printf 'Format plausible : %s\n' "$date"
fi
Remarquez :
$regex
n’est volontairement pas cité dans :
=~
afin que son contenu conserve sa signification d’expression régulière.
BASH_REMATCH
Lorsqu’un :
=~
réussit, Bash remplit le tableau :
BASH_REMATCH
Exemple :
version="app-12.34"
regex='^app-([0-9]+)[.]([0-9]+)$'
if [[ "$version" =~ $regex ]]; then
printf 'Version complète : %s\n' "${BASH_REMATCH[0]}"
printf 'Majeure : %s\n' "${BASH_REMATCH[1]}"
printf 'Mineure : %s\n' "${BASH_REMATCH[2]}"
fi
Ici :
BASH_REMATCH[0]
contient le match complet.
Les indices suivants contiennent les groupes capturés.
Une regex valide la syntaxe, pas la réalité
Cette regex :
^[0-9]{4}-[0-9]{2}-[0-9]{2}$
acceptera par exemple :
2026-99-88
Le format est conforme.
La date, légèrement moins.
Validation syntaxique et validation métier restent deux opérations différentes.
Comparaisons numériques traditionnelles
Avec :
[ ... ]
ou :
[[ ... ]]
on peut utiliser :
| Opérateur | Signification |
|---|---|
-eq |
Égal |
-ne |
Différent |
-lt |
Inférieur |
-le |
Inférieur ou égal |
-gt |
Supérieur |
-ge |
Supérieur ou égal |
Exemple classique
x=15
if [ "$x" -gt 10 ]; then
printf '%s\n' "Supérieur à 10."
fi
En Bash, préférez souvent (( )) pour les nombres
La même condition devient :
x=15
if (( x > 10 )); then
printf '%s\n' "Supérieur à 10."
fi
Cette syntaxe ressemble davantage à une expression arithmétique traditionnelle.
Opérateurs arithmétiques
Dans :
(( ... ))
on peut notamment utiliser :
==
!=
<
<=
>
>=
&&
||
!
Par exemple :
age=25
if (( age >= 18 && age <= 120 )); then
printf '%s\n' "Âge dans la plage attendue."
fi
La logique de (( ))
La commande arithmétique :
(( expression ))
retourne un succès si la valeur calculée est :
non nulle
et un échec si elle vaut :
0
Exemple :
if (( 42 )); then
printf '%s\n' "Vrai."
fi
Mais :
if (( 0 )); then
printf '%s\n' "Cette branche ne sera pas exécutée."
fi
Les variables n’ont pas besoin de $ dans (( ))
Correct :
if (( compteur > limite )); then
...
fi
Il n’est pas nécessaire d’écrire :
if (( $compteur > $limite )); then
...
fi
Les noms sont interprétés comme variables arithmétiques dans ce contexte.
Validez une saisie avant l’arithmétique
L’exemple :
read -r -p "Entrez un chiffre : " x
if [ "$x" -gt 10 ]; then
...
fi
suppose que l’utilisateur tape réellement :
un entier
S’il saisit :
banane
le test numérique n’a plus beaucoup de matière mathématique sur laquelle travailler.
Version plus robuste
read -r -p "Entrez un entier positif : " x
if [[ ! "$x" =~ ^[0-9]+$ ]]; then
printf 'Entier invalide : %s\n' "$x" >&2
exit 2
fi
if (( 10#$x > 10 )); then
printf '%s\n' "C’est supérieur à 10."
else
printf '%s\n' "10 ou moins."
fi
Pourquoi 10# ?
L’arithmétique Bash prend en charge plusieurs bases.
Notamment :
10
→ décimal
010
→ octal
0x10
→ hexadécimal
Ainsi :
08
peut poser problème dans un contexte où Bash interprète le zéro initial comme une indication d’octal :
8 n'existe pas en base 8
La syntaxe :
10#$x
indique explicitement :
interpréter en base 10
dans cet exemple d’entier positif préalablement validé.
Ne généralisez pas 10#$x aux nombres négatifs sans réfléchir
Si :
x=-08
une construction naïve :
10#$x
n’est pas la forme adaptée.
Pour des entrées signées arbitraires, gérez explicitement :
- le signe ;
- la validation ;
- la conversion.
Le point important reste :
ne supposez pas qu’une chaîne saisie par un utilisateur est automatiquement un entier Bash propre.
Attention aux décimaux
L’arithmétique native Bash fonctionne essentiellement avec des :
entiers
Cette condition :
(( 3.14 > 2.5 ))
n’est pas un calcul flottant valide comme en Python.
Pour des nombres décimaux, utilisez selon le besoin :
awk;bc;- Python ;
- un langage adapté.
Tester plusieurs conditions avec [[ ]]
AND :
if [[ -f "$fichier" && -r "$fichier" ]]; then
printf '%s\n' "Fichier régulier et lisible."
fi
OR :
if [[ "$nom" == "admin" || "$nom" == "root" ]]; then
printf '%s\n' "Nom privilégié."
fi
Négation :
if [[ ! -d "$repertoire" ]]; then
printf 'Répertoire absent : %s\n' "$repertoire" >&2
fi
Parenthèses logiques dans [[ ]]
Pour une expression complexe :
if [[
( "$mode" == "prod" || "$mode" == "staging" )
&& -n "$serveur"
]]; then
printf '%s\n' "Configuration acceptée."
fi
Mais une condition qui demande quinze parenthèses mérite parfois d’être divisée en variables ou fonctions intermédiaires.
Évitez -a et -o dans [ ] pour les nouveaux scripts
Vous rencontrerez parfois :
[ condition1 -a condition2 ]
ou :
[ condition1 -o condition2 ]
Ces constructions historiques existent, mais deviennent rapidement ambiguës et pénibles à lire.
Dans Bash, préférez :
[[ condition1 && condition2 ]]
ou utilisez des commandes séparées :
if [ condition1 ] && [ condition2 ]; then
...
fi
&& et || entre commandes
Les opérateurs :
&&
||
ne sont pas réservés à :
[[ ... ]]
Ils permettent aussi de chaîner des commandes.
AND list
commande1 && commande2
commande2 est exécutée seulement si :
commande1
réussit.
Exemple :
mkdir -p -- "$repertoire" &&
printf '%s\n' "Répertoire disponible."
OR list
commande1 || commande2
commande2 est exécutée seulement si :
commande1
échoue.
Exemple :
cd -- "$repertoire" ||
{
printf 'Impossible d’entrer dans %s\n' "$repertoire" >&2
exit 1
}
Le faux ternaire Bash
On rencontre souvent :
[ -f fichier.txt ] &&
printf '%s\n' "Présent" ||
printf '%s\n' "Absent"
Cela ressemble à :
if condition; then
vrai
else
faux
fi
Mais ce n’est pas strictement équivalent.
Pourquoi ?
Considérons :
condition && action_vraie || action_fausse
Si :
condition
réussit mais que :
action_vraie
échoue, alors :
action_fausse
est exécutée.
Exemple
true && false || printf '%s\n' "Alternative exécutée"
La condition initiale :
true
a pourtant réussi.
Mais :
false
a échoué, déclenchant la branche après :
||
Pour une vraie décision, utilisez if
Si la logique signifie réellement :
si A
alors B
sinon
C
écrivez :
if A; then
B
else
C
fi
C’est plus long de trois lignes.
Le budget du projet devrait survivre.
Tester directement une commande : souvent la meilleure solution
Supposons que vous vouliez savoir si :
apache2
est actif.
Évitez une logique comme :
etat="$(systemctl is-active apache2)"
if [[ "$etat" == "active" ]]; then
...
fi
si votre seul besoin est le succès ou l’échec.
Préférez :
if systemctl is-active --quiet apache2; then
printf '%s\n' "Apache est actif."
else
printf '%s\n' "Apache n’est pas actif."
fi
Pourquoi c’est meilleur ?
Vous utilisez directement :
l'interface prévue par la commande
→ son statut de sortie
Vous n’avez pas besoin de :
- capturer du texte ;
- le comparer ;
- dépendre de son format d’affichage.
Tester grep directement
Au lieu de :
resultat="$(grep "root" /etc/passwd)"
if [[ -n "$resultat" ]]; then
...
fi
utilisez :
if grep -qF "root" /etc/passwd; then
printf '%s\n' "Motif trouvé."
fi
Mais grep possède plusieurs statuts
Pour :
grep
les statuts ont notamment une signification :
0 → correspondance trouvée
1 → aucune correspondance
>1 → erreur
Ainsi, ce code :
if grep -qF "$motif" "$fichier"; then
printf '%s\n' "Trouvé."
else
printf '%s\n' "Non trouvé."
fi
regroupe deux situations :
aucune correspondance
ET
erreur de lecture
Si la différence compte, examinez le statut
grep -qF "$motif" "$fichier"
status=$?
case "$status" in
0)
printf '%s\n' "Motif trouvé."
;;
1)
printf '%s\n' "Motif absent."
;;
*)
printf 'Erreur grep, statut %d.\n' "$status" >&2
exit "$status"
;;
esac
Tester directement une commande est excellent.
Mais il faut toujours comprendre :
ce que signifient ses codes de sortie
Tester une commande avec !
Le :
!
inverse le statut logique.
Exemple :
if ! systemctl is-active --quiet ssh; then
printf '%s\n' "SSH n’est pas actif."
fi
La construction signifie :
« Exécute la branche si la commande ne réussit pas. »
Tester la présence d’une commande
Utilisez :
command -v
Par exemple :
if command -v rsync >/dev/null 2>&1; then
printf '%s\n' "rsync est disponible."
else
printf '%s\n' "rsync est absent."
fi
C’est généralement préférable à :
which rsync
dans un script.
Tester si le script tourne comme root
En Bash :
if (( EUID != 0 )); then
printf '%s\n' \
"Ce script nécessite des privilèges root." >&2
exit 1
fi
EUID contient l’identifiant utilisateur effectif du shell.
Pour root :
EUID = 0
Root n’est pas automatiquement un objectif
Ne transformez pas tous vos scripts administratifs en :
if pas root
→ refuse de fonctionner
si seules deux opérations nécessitent des privilèges élevés.
Le moindre privilège reste préférable.
Plus un script possède de droits, plus ses bugs ont un budget opérationnel généreux.
Alternative portable pour connaître l’UID
Dans un shell plus générique :
if [ "$(id -u)" -ne 0 ]; then
printf '%s\n' "Root requis." >&2
exit 1
fi
EUID est une fonctionnalité Bash pratique.
Tester le nombre d’arguments
$# contient le nombre de paramètres positionnels.
if (( $# == 0 )); then
printf 'Usage : %s fichier\n' "$0" >&2
exit 2
fi
Exiger exactement un argument
if (( $# != 1 )); then
printf 'Usage : %s fichier\n' "$0" >&2
exit 2
fi
Exiger au moins deux arguments
if (( $# < 2 )); then
printf 'Usage : %s SOURCE DESTINATION\n' "$0" >&2
exit 2
fi
Vérifier ensuite le contenu de l’argument
Avoir :
1 argument
ne signifie pas que cet argument désigne :
un fichier valide
On peut combiner :
if (( $# != 1 )); then
printf 'Usage : %s fichier\n' "$0" >&2
exit 2
fi
fichier="$1"
if [[ ! -f "$fichier" ]]; then
printf 'Fichier invalide : %s\n' "$fichier" >&2
exit 1
fi
Tester l’existence d’une variable
Attention à la différence entre :
variable absente
et :
variable définie mais vide
Exemple :
nom=""
Ici, :
nom
est définie.
Sa valeur est simplement vide.
Test -v
Bash fournit :
-v
pour tester si une variable est définie.
if [[ -v nom ]]; then
printf '%s\n' "La variable nom existe."
fi
Comparer -v, -z et -n
| Test | Question |
|---|---|
[[ -v variable ]] |
La variable est-elle définie ? |
[[ -z "$variable" ]] |
Sa valeur est-elle vide ? |
[[ -n "$variable" ]] |
Sa valeur est-elle non vide ? |
Exemple
nom=""
if [[ -v nom ]]; then
printf '%s\n' "nom est définie."
fi
if [[ -z "$nom" ]]; then
printf '%s\n' "Mais sa valeur est vide."
fi
Avec set -u
Dans un script utilisant :
set -u
une expansion directe d’une variable absente peut provoquer une erreur.
Par exemple :
if [[ -n "$DEBUG" ]]; then
...
fi
si :
DEBUG
n’est jamais définie.
Valeur par défaut sûre
On peut écrire :
if [[ -n "${DEBUG:-}" ]]; then
printf '%s\n' "Debug demandé."
fi
L’expansion :
${DEBUG:-}
fournit ici une chaîne vide si :
DEBUG
est absente ou vide.
Exiger une variable
On peut également utiliser :
: "${BACKUP_DIR:?BACKUP_DIR doit être définie}"
Le script échoue immédiatement si :
BACKUP_DIR
est absente ou vide.
Tester un descripteur de fichier
Le test :
-t
permet de savoir si un descripteur correspond à un terminal.
Par exemple :
if [[ -t 0 ]]; then
printf '%s\n' "stdin est relié à un terminal."
else
printf '%s\n' "stdin n’est pas interactif."
fi
Très utile pour distinguer :
- une exécution interactive ;
- cron ;
- pipeline ;
- redirection depuis un fichier.
Détecter si stdout est un terminal
if [[ -t 1 ]]; then
printf '%s\n' "Sortie interactive."
fi
Un script peut ainsi décider :
- d’utiliser des couleurs devant un humain ;
- de produire une sortie sobre lorsqu’elle est redirigée.
Comparaison lexicographique des chaînes
Dans :
[[ ... ]]
Bash prend en charge :
<
>
pour comparer l’ordre lexicographique selon la locale courante.
Par exemple :
if [[ "$a" < "$b" ]]; then
printf '%s vient avant %s.\n' "$a" "$b"
fi
Ce n’est pas une comparaison numérique
Avec :
a="10"
b="2"
une comparaison lexicographique raisonne comme sur du texte.
Pour des nombres :
(( a < b ))
est beaucoup plus approprié.
Locale et ordre des chaînes
Les opérateurs lexicographiques de :
[[ ... ]]
utilisent la locale courante.
L’ordre entre :
- majuscules ;
- minuscules ;
- accents ;
- autres caractères ;
peut donc dépendre du contexte linguistique configuré.
Si votre script exige un ordre reproductible au niveau technique, prenez explicitement en compte :
LC_ALL
LC_COLLATE
selon l’opération concernée.
Combiner tests et fonctions
Une fonction peut elle-même servir de condition.
est_valide() {
local fichier="$1"
[[ -f "$fichier" && -r "$fichier" ]]
}
Puis :
if est_valide "$fichier"; then
printf '%s\n' "Fichier valide."
fi
La dernière commande détermine le statut de la fonction
Dans la fonction précédente :
[[ -f "$fichier" && -r "$fichier" ]]
est la dernière commande exécutée.
Son statut devient donc celui de la fonction si aucun :
return
explicite ne le remplace.
Fonction conditionnelle plus explicite
est_configuration_valide() {
local fichier="$1"
[[ -f "$fichier" ]] || return 1
[[ -r "$fichier" ]] || return 1
grep -qF "enabled=true" "$fichier"
}
Utilisation :
if est_configuration_valide "$config"; then
printf '%s\n' "Configuration acceptable."
else
printf '%s\n' "Configuration invalide."
fi
return utilise lui aussi les statuts Unix
Dans une fonction :
return 0
signifie :
succès
et :
return 1
indique généralement un échec ou un résultat faux selon l’API de la fonction.
Ne retournez pas une chaîne avec return
Ceci :
return "oui"
n’est pas l’équivalent Bash de :
return "oui"
dans un langage généraliste.
return concerne un statut numérique.
Pour produire une valeur textuelle :
printf
peut être utilisé.
case : souvent meilleur qu’une série de elif
Supposons :
if [[ "$action" == "start" ]]; then
...
elif [[ "$action" == "stop" ]]; then
...
elif [[ "$action" == "restart" ]]; then
...
elif [[ "$action" == "status" ]]; then
...
else
...
fi
Un :
case
est souvent beaucoup plus naturel.
Exemple
case "$action" in
start)
demarrer
;;
stop)
arreter
;;
restart)
redemarrer
;;
status)
afficher_etat
;;
*)
printf 'Action inconnue : %s\n' "$action" >&2
exit 2
;;
esac
Case accepte des patterns
case "$fichier" in
*.jpg|*.jpeg|*.png)
printf '%s\n' "Image"
;;
*.txt|*.md)
printf '%s\n' "Texte"
;;
*)
printf '%s\n' "Autre"
;;
esac
Pour plusieurs valeurs discrètes ou motifs :
case
est souvent plus clair qu’une forêt de :
elif
Tester une réponse oui/non
read -r -p "Continuer ? [o/N] " reponse
case "$reponse" in
o|O|oui|OUI)
printf '%s\n' "Continuation."
;;
*)
printf '%s\n' "Annulation."
;;
esac
Le choix par défaut est ainsi :
ne rien faire de destructif
ce qui constitue généralement une excellente philosophie administrative.
Les conditions imbriquées
On peut écrire :
if [[ -f "$fichier" ]]; then
if [[ -r "$fichier" ]]; then
if [[ -s "$fichier" ]]; then
printf '%s\n' "Fichier exploitable."
fi
fi
fi
Mais trop d’imbrication rend le code difficile à lire.
Préférer les validations précoces
Dans une fonction ou un script :
[[ -f "$fichier" ]] || {
printf 'Pas un fichier : %s\n' "$fichier" >&2
exit 1
}
[[ -r "$fichier" ]] || {
printf 'Fichier illisible : %s\n' "$fichier" >&2
exit 1
}
[[ -s "$fichier" ]] || {
printf 'Fichier vide : %s\n' "$fichier" >&2
exit 1
}
printf '%s\n' "Traitement..."
Cette technique est parfois appelée :
guard clauses
validation précoce
Ou regrouper proprement la condition
if [[
-f "$fichier"
&& -r "$fichier"
&& -s "$fichier"
]]; then
printf '%s\n' "Fichier exploitable."
fi
Le choix dépend de :
- la complexité ;
- la qualité du diagnostic souhaité ;
- le besoin de distinguer les erreurs.
Une grosse condition peut cacher la cause
Avec :
if [[
-f "$fichier"
&& -r "$fichier"
&& -s "$fichier"
]]; then
...
else
printf '%s\n' "Échec."
fi
vous savez seulement qu’au moins une condition est fausse.
Vous ne savez pas laquelle.
Pour un script utilisateur, plusieurs validations séparées peuvent produire de meilleurs messages :
fichier absent
permission refusée
fichier vide
Tester une commande avant de l’utiliser
if ! command -v curl >/dev/null 2>&1; then
printf '%s\n' "curl est requis." >&2
exit 1
fi
Puis :
if curl -fsS "https://example.com/" >/dev/null; then
printf '%s\n' "Service HTTP accessible."
else
printf '%s\n' "Test HTTP en échec."
fi
Ping n’est pas un test de service
Cette condition :
if ping -c 1 "$serveur" >/dev/null 2>&1; then
printf '%s\n' "Serveur joignable."
fi
signifie seulement que :
le test ICMP choisi a réussi
Elle ne prouve pas que :
- SSH fonctionne ;
- Apache répond ;
- la base accepte des connexions ;
- l’application est saine.
Testez ce dont vous avez réellement besoin.
Tester un port TCP
Avec un outil approprié :
if nc -z -w 3 "$serveur" 443; then
printf '%s\n' "TCP/443 accepte une connexion."
fi
Cela ne prouve toujours pas que :
HTTPS fonctionne correctement
Un test applicatif :
curl
sera souvent plus pertinent.
Tester une commande et récupérer sa sortie
On peut faire les deux à la fois :
if sortie="$(commande)"; then
printf 'Résultat : %s\n' "$sortie"
else
printf '%s\n' "Commande en échec." >&2
fi
Cette construction :
- capture stdout ;
- teste le statut de l’affectation/commande.
Exemple pratique
if ip="$(hostname -I 2>/dev/null)"; then
printf 'Adresse(s) : %s\n' "$ip"
else
printf '%s\n' "Impossible de récupérer les adresses." >&2
fi
Ne testez pas $? plus tard que nécessaire
Ce code est fragile :
commande
printf '%s\n' "Commande exécutée"
if [[ $? -eq 0 ]]; then
printf '%s\n' "Succès."
fi
Vous testez maintenant le statut de :
printf
et non celui de :
commande
Si vous avez besoin de $?, sauvegardez-le immédiatement
commande
status=$?
if (( status == 0 )); then
printf '%s\n' "Succès."
fi
Mais préférez souvent if commande
if commande; then
printf '%s\n' "Succès."
else
printf '%s\n' "Échec."
fi
C’est généralement :
- plus lisible ;
- plus court ;
- moins fragile.
Statuts spéciaux fréquents
Bash utilise notamment :
0
→ succès
126
→ commande trouvée mais non exécutable
127
→ commande introuvable
128 + N
→ terminaison par certains signaux
Les programmes peuvent évidemment définir leurs propres codes non nuls.
Consultez leur documentation lorsque plusieurs résultats doivent être distingués.
Tester false n’est pas forcément une erreur
Dans :
if grep -q "motif" fichier; then
...
fi
un statut :
1
signifiant :
aucune correspondance
peut être un résultat métier parfaitement normal.
Le shell ne confond pas nécessairement :
condition fausse
avec :
catastrophe
Conditions et set -e
Cette distinction devient importante avec :
set -e
ou :
set -o errexit
Les commandes utilisées dans certains contextes conditionnels font partie des exceptions au comportement simplifié :
« Quitter dès qu’une commande retourne non zéro. »
Exemple légitime
set -e
if grep -qF "production" config.txt; then
printf '%s\n' "Mode production."
else
printf '%s\n' "Mode production absent."
fi
Le statut non nul de :
grep
dans la condition est précisément une information attendue par :
if
Ce comportement explique pourquoi :
set -e
ne doit jamais être résumé à :
« Tout non-zéro tue le script. »
Conditions et pipefail
Supposons :
if generer |
filtrer |
envoyer; then
printf '%s\n' "Pipeline réussie."
fi
Par défaut, le statut d’une pipeline dépend principalement de sa dernière commande.
Avec :
set -o pipefail
un échec intermédiaire peut influencer le statut global.
Dans un script où le succès de toute la chaîne est important :
pipefail
peut donc être essentiel.
Tester plusieurs commandes comme une condition
La partie conditionnelle de :
if
peut contenir une liste de commandes.
Par exemple :
if
printf '%s\n' "Vérification..."
test -f "$fichier"
then
printf '%s\n' "Le fichier existe."
fi
Le statut de la dernière commande de la liste de test détermine ici le résultat.
Cette syntaxe est valide, même si une condition courte sur une seule ligne reste souvent plus lisible.
Groupement de commandes
On peut regrouper des commandes avec :
{
commande1
commande2
}
Le groupe s’exécute dans le shell courant.
Par exemple :
if {
[[ -f "$fichier" ]]
grep -qF "enabled=true" "$fichier"
}; then
printf '%s\n' "Configuration active."
fi
Le statut du groupe est celui de la dernière commande exécutée.
Sous-shell comme condition
On peut également tester :
if (
cd -- "$repertoire" &&
commande
); then
printf '%s\n' "Opération réussie."
fi
Les parenthèses créent un sous-shell.
Le :
cd
ne modifie donc pas le répertoire du shell parent.
Conditions et commandes destructives
Une condition ne doit pas seulement décider :
est-ce possible ?
mais parfois :
est-ce bien la cible prévue ?
Exemple :
destination="/srv/app/cache"
if [[ "$destination" != "/srv/app/cache" ]]; then
printf 'Destination inattendue : %s\n' \
"$destination" >&2
exit 1
fi
if [[ ! -d "$destination" ]]; then
printf 'Répertoire absent : %s\n' \
"$destination" >&2
exit 1
fi
Puis seulement :
find "$destination" \
-type f \
-name '*.tmp' \
-delete
Un test n’élimine pas les race conditions
Considérez :
if [[ -f "$fichier" ]]; then
rm -- "$fichier"
fi
Entre :
le test
et :
rm
l’état du système peut changer.
Dans les environnements concurrents, on parle souvent de problèmes :
TOCTOU
Time Of Check To Time Of Use
Préférez parfois tenter directement l’opération
Au lieu de :
if [[ -d "$repertoire" ]]; then
cd "$repertoire"
else
...
fi
on peut écrire :
if cd -- "$repertoire"; then
printf '%s\n' "Répertoire accessible."
else
printf 'Impossible d’entrer dans %s.\n' \
"$repertoire" >&2
fi
Cette approche teste :
l'opération réelle
plutôt qu’une prédiction préalable de sa réussite.
Tester avant d’écrire n’est pas toujours suffisant
Par exemple :
if [[ -w "$fichier" ]]; then
printf '%s\n' "Nouvelle donnée" > "$fichier"
fi
entre le test et l’écriture :
- les permissions peuvent changer ;
- le fichier peut être remplacé ;
- le filesystem peut devenir readonly ;
- l’espace disque peut disparaître.
Il faut donc aussi vérifier :
si l'écriture elle-même réussit
Version plus directe
if printf '%s\n' "Nouvelle donnée" > "$fichier"; then
printf '%s\n' "Écriture réussie."
else
printf 'Écriture impossible : %s\n' "$fichier" >&2
fi
Conditions et commandes de validation
Un excellent pattern administratif est :
générer
↓
valider
↓
installer uniquement si valide
Exemple conceptuel
tmp="$(mktemp)"
if generer_configuration > "$tmp" &&
valider_configuration "$tmp"; then
install -m 0644 \
"$tmp" \
/etc/application/config.conf
else
printf '%s\n' \
"Configuration refusée." >&2
exit 1
fi
La condition empêche ici une mauvaise configuration d’être déployée simplement parce que :
« La génération n’avait pas affiché de message rouge. »
Tester les options Bash
Les expressions conditionnelles Bash possèdent également :
-o option
pour vérifier certaines options du shell.
Par exemple :
if [[ -o nounset ]]; then
printf '%s\n' "nounset est actif."
fi
ou :
if [[ -o errexit ]]; then
printf '%s\n' "errexit est actif."
fi
Tester une nameref
Bash dispose aussi du test :
-R variable
qui permet de déterminer si une variable est définie comme :
name reference
nameref
C’est une fonctionnalité avancée principalement utile dans des fonctions génériques.
Conditions et tableaux
On peut tester le nombre d’éléments :
if (( ${#serveurs[@]} == 0 )); then
printf '%s\n' "Aucun serveur."
fi
Tester si un élément de tableau est défini
Avec :
-v
on peut tester un élément précis :
if [[ -v 'serveurs[2]' ]]; then
printf 'Élément : %s\n' "${serveurs[2]}"
fi
La distinction :
élément absent
versus
élément existant mais vide
peut être importante.
Conditions et chaînes contenant des caractères spéciaux
Avec :
[[ ... ]]
une valeur comme :
rapport *.txt [test]
peut être comparée sans subir le filename expansion classique lorsqu’elle se trouve comme opérande.
C’est l’une des raisons pour lesquelles :
[[ ... ]]
est particulièrement agréable dans les scripts Bash.
Mais la syntaxe du membre droit compte toujours
Par exemple :
motif="*.txt"
if [[ "$fichier" == $motif ]]; then
printf '%s\n' "Correspondance."
fi
utilise le contenu de :
motif
comme pattern.
Alors que :
if [[ "$fichier" == "$motif" ]]; then
...
fi
teste l’égalité littérale avec :
*.txt
Comparaison insensible à la casse
Bash possède l’option :
nocasematch
qui affecte notamment certains matches de :
[[ ... ]]
et :
case
Exemple :
shopt -s nocasematch
if [[ "$reponse" == "oui" ]]; then
printf '%s\n' "Réponse positive."
fi
Ainsi :
OUI
Oui
oui
peuvent correspondre selon le pattern et la locale.
Une option shell modifie l’environnement logique du script
Si vous activez :
shopt -s nocasematch
pour une seule section, pensez éventuellement à :
shopt -u nocasematch
ensuite.
Sinon, les tests réalisés cent lignes plus bas peuvent soudainement devenir insensibles à la casse sans que leur auteur en ait conscience.
Conditions et valeurs booléennes
Bash ne possède pas un type booléen classique comme :
bool enabled = true
mais les commandes :
true
false
s’intègrent très bien aux conditions.
Exemple
dry_run=true
if "$dry_run"; then
printf '%s\n' "Mode simulation."
fi
La variable contient ici le nom d’une commande :
true
Bash l’exécute.
Son statut vaut :
0
et la condition réussit.
Cette approche demande de contrôler les valeurs
Si :
dry_run="rm"
le comportement devient nettement plus philosophique.
Pour des données externes, utilisez plutôt une représentation contrôlée et une comparaison explicite :
if [[ "$DRY_RUN" == "yes" ]]; then
...
fi
Tester une variable d’environnement booléenne proprement
case "${DEBUG:-false}" in
true|yes|1)
debug_enabled=true
;;
false|no|0|"")
debug_enabled=false
;;
*)
printf 'DEBUG invalide : %s\n' "$DEBUG" >&2
exit 2
;;
esac
Cette méthode distingue :
- les valeurs acceptées ;
- les erreurs de configuration.
Conditions et fichiers de configuration
Évitez de décider qu’un fichier est valide uniquement parce qu’il existe :
[[ -f config.conf ]]
→ fichier présent
Cela ne signifie pas :
syntaxe valide
configuration cohérente
secrets corrects
permissions adaptées
Utilisez lorsqu’ils existent les validateurs de l’application.
Exemple SSH
if sshd -t; then
printf '%s\n' "Configuration SSH valide."
else
printf '%s\n' "Configuration SSH invalide." >&2
fi
Exemple Samba
if testparm -s >/dev/null; then
printf '%s\n' "Configuration Samba valide."
fi
Exemple Apache
if apache2ctl configtest; then
printf '%s\n' "Configuration Apache valide."
fi
Tester la vraie syntaxe est généralement plus pertinent que vérifier :
[[ -f /etc/application/config ]]
puis espérer que le contenu raconte une histoire cohérente.
Validation d’une adresse IPv4 : attention aux fausses bonnes regex
Cette regex :
^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$
accepte :
999.999.999.999
Elle valide la :
forme générale
pas la plage réelle de chaque octet.
Comme pour les dates :
syntaxe
≠
validité métier
Exemple avec plusieurs niveaux de validation
ip="$1"
if [[ ! "$ip" =~ ^([0-9]{1,3}[.]){3}[0-9]{1,3}$ ]]; then
printf '%s\n' "Format IPv4 invalide." >&2
exit 2
fi
IFS=. read -r o1 o2 o3 o4 <<< "$ip"
for octet in "$o1" "$o2" "$o3" "$o4"; do
if (( 10#$octet > 255 )); then
printf '%s\n' "Octet IPv4 hors plage." >&2
exit 2
fi
done
Pour des besoins réseau complexes, utilisez néanmoins autant que possible une bibliothèque ou un outil spécialisé plutôt que reconstruire toute la validation IP à la main.
Tester une chaîne avec plusieurs valeurs
Au lieu de :
if [[ "$env" == "prod" ||
"$env" == "test" ||
"$env" == "dev" ]]; then
...
fi
case peut être plus lisible :
case "$env" in
prod|test|dev)
printf '%s\n' "Environnement valide."
;;
*)
printf 'Environnement inconnu : %s\n' "$env" >&2
exit 2
;;
esac
Conditions mutuellement exclusives ou indépendantes
Comparez :
if condition1; then
action1
elif condition2; then
action2
fi
avec :
if condition1; then
action1
fi
if condition2; then
action2
fi
Dans le premier cas :
une seule branche au maximum
est exécutée.
Dans le second :
les deux peuvent être exécutées
si les deux conditions réussissent.
Exemple
if [[ -f "$chemin" ]]; then
printf '%s\n' "C’est un fichier."
fi
if [[ -r "$chemin" ]]; then
printf '%s\n' "Il est lisible."
fi
Ces deux propriétés ne sont pas mutuellement exclusives.
Un :
elif
serait donc inadapté si vous voulez afficher les deux informations.
Erreurs classiques : oublier les espaces avec [ ]
Incorrect :
if ["$x" = "abc"]; then
...
fi
Correct :
if [ "$x" = "abc" ]; then
...
fi
Erreur classique : oublier l’espace avant ]
Incorrect :
[ "$x" = "abc"]
Correct :
[ "$x" = "abc" ]
Erreur classique : confondre chaînes et nombres
Chaînes :
[[ "$a" == "$b" ]]
Nombres :
(( a == b ))
Ou avec le test traditionnel :
[ "$a" -eq "$b" ]
Erreur classique : utiliser > pour des nombres dans [[ ]]
Cette expression :
[[ "$a" > "$b" ]]
effectue une comparaison :
lexicographique
et non numérique.
Pour des entiers :
(( a > b ))
Erreur classique : quoter une regex entière
Vous vouliez :
[[ "$x" =~ ^[0-9]+$ ]]
et avez écrit :
[[ "$x" =~ "^[0-9]+$" ]]
Vous avez transformé les caractères spéciaux cités en texte littéral.
Erreur classique : oublier que == peut utiliser un pattern
Cette expression :
[[ "$fichier" == *.txt ]]
est volontairement différente de :
[[ "$fichier" == "*.txt" ]]
Le premier teste un motif.
Le second recherche la chaîne littérale :
*.txt
Erreur classique : utiliser [ $variable … ] sans guillemets
Fragile :
if [ $nom = root ]; then
...
fi
Préférable :
if [ "$nom" = "root" ]; then
...
fi
Erreur classique : croire que [[ ]] rend tout automatiquement correct
[[ ... ]] résout plusieurs problèmes de parsing.
Il ne peut pas déterminer que :
if [[ "$repertoire" == "/srv/data" ]]; then
rm -rf -- "/srv/database"
fi
contient une cible différente de celle que vous aviez en tête.
La syntaxe peut être parfaite.
La logique peut toujours être catastrophique.
Erreur classique : tester le texte d’une commande au lieu de son statut
Inutilement compliqué :
etat="$(systemctl is-active ssh)"
if [[ "$etat" == "active" ]]; then
...
fi
Lorsque l’outil possède déjà un statut approprié :
if systemctl is-active --quiet ssh; then
...
fi
Erreur classique : ignorer les différents statuts d’une commande
Un :
else
englobe tous les statuts qui ne valent pas zéro.
Si :
1 = résultat normal négatif
2 = véritable erreur
et que cette distinction compte, gérez-la explicitement.
Erreur classique : abuser des conditions compactes
Ce code :
[[ -f "$f" ]] && [[ -r "$f" ]] && [[ -s "$f" ]] && traitement "$f" || erreur
est peut-être compact.
Mais déterminer quelle étape a échoué devient beaucoup moins agréable.
Une version plus longue peut fournir :
- meilleur diagnostic ;
- meilleure maintenance ;
- moins d’ambiguïtés.
Erreur classique : tester une variable inexistante avec set -u
Avec :
set -u
préférez selon l’objectif :
[[ -v VARIABLE ]]
ou :
[[ -n "${VARIABLE:-}" ]]
Erreur classique : croire que -e détecte toujours un lien cassé
Pour un lien symbolique dont la cible a disparu :
[[ -e "$lien" ]]
n’est pas le test approprié pour savoir si l’entrée est un lien.
Utilisez :
[[ -L "$lien" ]]
Erreur classique : croire que -w garantit l’écriture future
Le filesystem peut changer entre :
le test
et
l'opération
Testez également :
le succès réel de l'opération
Erreur classique : condition numérique sur une saisie arbitraire
Fragile :
read -r x
if (( x > 10 )); then
...
fi
Si :
x
provient d’une source non maîtrisée, validez d’abord son format.
Erreur classique : oublier les zéros initiaux
Une saisie :
08
peut déclencher une erreur arithmétique dans certains contextes Bash à cause de l’interprétation octale.
Pour un entier décimal positif validé :
(( 10#$x > limite ))
permet d’indiquer explicitement la base.
Erreur classique : utiliser elif pour deux propriétés indépendantes
Si vous voulez savoir :
est un fichier ?
ET
est lisible ?
deux :
if
peuvent être nécessaires.
Erreur classique : oublier le fi
Une structure :
if
se termine par :
fi
Tout comme :
case
→ esac
Bash a choisi de terminer certains mots-clés en les écrivant à l’envers.
Il faut reconnaître une certaine cohérence artistique.
Vérifier la syntaxe
Avant d’exécuter :
bash -n script.sh
permet de vérifier la syntaxe sans exécuter les commandes.
Il peut détecter notamment :
fimanquant ;- crochet mal formé ;
- guillemet non fermé ;
- parenthèse invalide ;
- plusieurs autres erreurs de parsing.
Mais bash -n ne vérifie pas votre logique
Ce script :
if [[ -d / ]]; then
rm -rf -- /chemin/important
fi
peut être syntaxiquement irréprochable.
bash -n n’est pas un comité d’éthique.
Utiliser ShellCheck
Après :
bash -n script.sh
analysez également :
shellcheck script.sh
ShellCheck peut détecter de nombreux problèmes liés à :
- quoting ;
- tests ;
- variables ;
- substitutions ;
- portabilité ;
- syntaxes fragiles.
Exemple complet : vérifier un fichier d’entrée
#!/bin/bash
set -u
if (( $# != 1 )); then
printf 'Usage : %s FICHIER\n' "$0" >&2
exit 2
fi
fichier="$1"
if [[ ! -e "$fichier" ]]; then
printf 'Chemin absent : %s\n' "$fichier" >&2
exit 1
fi
if [[ ! -f "$fichier" ]]; then
printf 'Pas un fichier régulier : %s\n' "$fichier" >&2
exit 1
fi
if [[ ! -r "$fichier" ]]; then
printf 'Fichier illisible : %s\n' "$fichier" >&2
exit 1
fi
if [[ ! -s "$fichier" ]]; then
printf 'Fichier vide : %s\n' "$fichier" >&2
exit 1
fi
printf 'Fichier accepté : %s\n' "$fichier"
Pourquoi plusieurs conditions plutôt qu’une seule ?
On aurait pu écrire :
if [[ -f "$fichier" && -r "$fichier" && -s "$fichier" ]]; then
...
fi
Mais la version détaillée permet de savoir précisément :
ce qui ne va pas
Pour un outil destiné à un humain, c’est souvent préférable.
Exemple complet : validation numérique
#!/bin/bash
set -u
read -r -p "Pourcentage [0-100] : " valeur
if [[ ! "$valeur" =~ ^[0-9]+$ ]]; then
printf 'Nombre invalide : %s\n' "$valeur" >&2
exit 2
fi
nombre=$((10#$valeur))
if (( nombre < 0 || nombre > 100 )); then
printf 'Valeur hors plage : %d\n' "$nombre" >&2
exit 2
fi
if (( nombre >= 90 )); then
printf '%s\n' "Critique"
elif (( nombre >= 70 )); then
printf '%s\n' "Élevé"
elif (( nombre >= 40 )); then
printf '%s\n' "Normal"
else
printf '%s\n' "Faible"
fi
Exemple complet : tester un service
#!/bin/bash
set -u
service="${1:-ssh}"
if ! command -v systemctl >/dev/null 2>&1; then
printf '%s\n' "systemctl est indisponible." >&2
exit 1
fi
if systemctl is-active --quiet "$service"; then
printf '[OK] %s est actif.\n' "$service"
elif systemctl is-failed --quiet "$service"; then
printf '[ERREUR] %s est en échec.\n' "$service" >&2
exit 1
else
printf '[INFO] %s n’est pas actif.\n' "$service"
exit 1
fi
Exemple complet : sélectionner un comportement selon un fichier
#!/bin/bash
set -u
fichier="${1:-}"
if [[ -z "$fichier" ]]; then
printf 'Usage : %s FICHIER\n' "$0" >&2
exit 2
fi
if [[ ! -e "$fichier" ]]; then
printf 'Introuvable : %s\n' "$fichier" >&2
exit 1
fi
case "$fichier" in
*.tar.gz|*.tgz)
printf '%s\n' "Archive gzip"
;;
*.tar.xz)
printf '%s\n' "Archive xz"
;;
*.zip)
printf '%s\n' "Archive ZIP"
;;
*.txt|*.md)
printf '%s\n' "Document texte"
;;
*)
printf '%s\n' "Type non reconnu par le nom"
;;
esac
Le suffixe ne garantit évidemment pas le contenu réel.
Pour identifier davantage un fichier :
file -- "$fichier"
peut fournir des informations supplémentaires.
Exemple complet : confirmation avant suppression
#!/bin/bash
set -u
fichier="${1:-}"
if [[ -z "$fichier" ]]; then
printf 'Usage : %s FICHIER\n' "$0" >&2
exit 2
fi
if [[ ! -e "$fichier" && ! -L "$fichier" ]]; then
printf 'Chemin inexistant : %s\n' "$fichier" >&2
exit 1
fi
printf 'Cible : %q\n' "$fichier"
read -r -p "Supprimer ? [o/N] " reponse
case "$reponse" in
o|O|oui|OUI)
if rm -- "$fichier"; then
printf '%s\n' "Suppression réussie."
else
printf '%s\n' "Suppression impossible." >&2
exit 1
fi
;;
*)
printf '%s\n' "Opération annulée."
;;
esac
Pourquoi tester -L en plus de -e ?
La condition :
[[ ! -e "$fichier" && ! -L "$fichier" ]]
permet de ne pas considérer automatiquement un :
lien symbolique cassé
comme un chemin totalement absent.
Exemple complet : vérifier plusieurs prérequis
#!/bin/bash
set -u
erreurs=0
if (( EUID != 0 )); then
printf '%s\n' "Root requis." >&2
erreurs=$((erreurs + 1))
fi
if ! command -v rsync >/dev/null 2>&1; then
printf '%s\n' "rsync absent." >&2
erreurs=$((erreurs + 1))
fi
if [[ ! -d /mnt/backup ]]; then
printf '%s\n' "/mnt/backup absent." >&2
erreurs=$((erreurs + 1))
fi
if (( erreurs > 0 )); then
printf '%d prérequis non satisfait(s).\n' \
"$erreurs" >&2
exit 1
fi
printf '%s\n' "Tous les prérequis sont satisfaits."
Arrêter immédiatement ou collecter toutes les erreurs ?
Deux stratégies sont valables.
Fail fast
condition1 || exit 1
condition2 || exit 1
condition3 || exit 1
Le script s’arrête au premier problème.
Collecte complète
erreurs=0
condition1 || erreurs=$((erreurs + 1))
condition2 || erreurs=$((erreurs + 1))
condition3 || erreurs=$((erreurs + 1))
(( erreurs == 0 )) || exit 1
Cette approche peut afficher tous les prérequis manquants en une fois.
Le bon choix dépend du contexte.
Exemple complet : fonction de validation
#!/bin/bash
set -u
valider_fichier() {
local fichier="$1"
if [[ ! -e "$fichier" ]]; then
printf 'Absent : %s\n' "$fichier" >&2
return 1
fi
if [[ ! -f "$fichier" ]]; then
printf 'Pas un fichier : %s\n' "$fichier" >&2
return 1
fi
if [[ ! -r "$fichier" ]]; then
printf 'Illisible : %s\n' "$fichier" >&2
return 1
fi
return 0
}
if valider_fichier "/etc/hosts"; then
printf '%s\n' "Validation réussie."
else
printf '%s\n' "Validation échouée." >&2
fi
Tableau récapitulatif : chaînes
| Expression | Signification |
|---|---|
[[ -z "$s" ]] |
Chaîne vide |
[[ -n "$s" ]] |
Chaîne non vide |
[[ "$a" == "$b" ]] |
Égalité littérale |
[[ "$a" != "$b" ]] |
Différence |
[[ "$a" == pattern ]] |
Correspondance à un pattern |
[[ "$a" =~ regex ]] |
Correspondance à une regex étendue |
Tableau récapitulatif : nombres
| Avec test | Avec (( )) | Signification |
|---|---|---|
-eq |
== |
Égal |
-ne |
!= |
Différent |
-lt |
< |
Inférieur |
-le |
<= |
Inférieur ou égal |
-gt |
> |
Supérieur |
-ge |
>= |
Supérieur ou égal |
Tableau récapitulatif : fichiers
| Expression | Signification principale |
|---|---|
-e |
Chemin/cible existant |
-f |
Fichier régulier |
-d |
Répertoire |
-L |
Lien symbolique |
-r |
Accès en lecture |
-w |
Accès en écriture |
-x |
Accès en exécution / traversée |
-s |
Taille non nulle |
-b |
Périphérique bloc |
-c |
Périphérique caractère |
-p |
FIFO |
-S |
Socket |
Tableau récapitulatif : constructions
| Construction | Quand l’utiliser |
|---|---|
if commande |
Tester directement le succès d’une opération |
[ ... ] |
Tests traditionnels / portabilité shell |
[[ ... ]] |
Tests avancés dans un script Bash |
(( ... )) |
Calculs et conditions sur entiers |
case |
Plusieurs valeurs ou patterns |
commande1 && commande2 |
Exécuter la seconde seulement si la première réussit |
commande1 || commande2 |
Exécuter la seconde seulement si la première échoue |
Quelle construction choisir ?
Pour :
Le fichier existe-t-il ?
utilisez par exemple :
[[ -f "$fichier" ]]
Pour :
Cette chaîne correspond-elle à un motif ?
utilisez :
[[ "$chaine" == pattern ]]
Pour :
Cette chaîne respecte-t-elle une regex ?
utilisez :
[[ "$chaine" =~ regex ]]
Pour :
Ce nombre est-il supérieur à 10 ?
utilisez :
(( nombre > 10 ))
Pour :
Cette commande a-t-elle réussi ?
utilisez simplement :
if commande; then
...
fi
Pour :
La valeur vaut-elle start, stop ou restart ?
utilisez probablement :
case
Checklist pour un test de fichier
- voulez-vous tester l’existence, le type ou l’accès ?
- un lien symbolique cassé doit-il être détecté ?
- l’accès peut-il changer entre le test et l’opération ?
- le répertoire parent joue-t-il un rôle ?
- pouvez-vous simplement tester l’opération réelle ?
Checklist pour un test numérique
- la valeur provient-elle d’une source fiable ?
- a-t-elle été validée comme nombre ?
- des zéros initiaux sont-ils possibles ?
- avez-vous besoin d’entiers ou de décimaux ?
(( ... ))exprime-t-il mieux la condition ?
Checklist pour une chaîne
- cherchez-vous une égalité littérale ?
- un pattern shell ?
- une regex ?
- la casse compte-t-elle ?
- la locale peut-elle influencer le résultat ?
- une chaîne vide est-elle différente d’une variable absente ?
Checklist pour tester une commande
- quel statut représente le succès ?
- plusieurs statuts non nuls ont-ils des significations différentes ?
- avez-vous réellement besoin de capturer stdout ?
- la commande possède-t-elle un mode silencieux adapté ?
- une pipeline nécessite-t-elle
pipefail?
Checklist de lisibilité
Avant de valider une condition, demandez-vous :
Est-ce que quelqu'un comprendra
en quelques secondes
pourquoi cette branche est exécutée ?
Si vous avez :
[[ ( "$a" == x* || "$b" =~ ... ) &&
! -z "$c" &&
( -f "$d" || -L "$d" ) ]]
une variable intermédiaire ou une fonction peut rendre l’intention beaucoup plus claire.
Nommer les décisions
Au lieu de :
if [[
"$env" == "prod"
&& -f "$config"
&& -r "$config"
]]; then
...
fi
on peut définir :
configuration_valide() {
[[ "$env" == "prod" ]] || return 1
[[ -f "$config" ]] || return 1
[[ -r "$config" ]]
}
Puis :
if configuration_valide; then
deployer
fi
Le test dit maintenant :
ce qu'il signifie
plutôt que seulement :
comment il est calculé
Les règles essentielles à retenir
ifteste le statut d’une commande ou d’une liste de commandes.- Le statut
0représente le succès dans un contexte conditionnel Bash. [ ... ]est une forme du builtintest, pas une simple ponctuation décorative.- Les espaces autour des opérateurs et crochets de
[ ... ]sont indispensables. - Dans un script Bash,
[[ ... ]]est souvent plus sûr et plus expressif pour les chaînes et fichiers. [[ ... ]]n’est pas du shell POSIX portable.- Dans
[[ ... ]], le membre droit non cité de==peut être un pattern. =~utilise une expression régulière et son quoting modifie sa sémantique.BASH_REMATCHpermet de récupérer les groupes capturés par une regex.- Pour les entiers,
(( ... ))est généralement plus lisible que les opérateurs-eq,-lt, etc. - Validez une saisie avant de l’utiliser comme expression arithmétique.
- Attention aux nombres avec zéro initial dans l’arithmétique Bash.
-eet-Lne répondent pas exactement à la même question lorsqu’un lien symbolique est cassé.-r,-wet-xsont des tests d’accès, pas de simples lectures de caractères affichés parls -l.- Lorsque vous voulez savoir si une commande réussit, testez souvent directement cette commande.
- Comprenez les différents codes de retour d’un programme lorsque la distinction compte.
A && B || Cn’est pas un remplacement général deif A; then B; else C; fi.caseest souvent préférable à une longue série de comparaisons de chaînes.- Une vérification préalable ne garantit pas qu’une opération ultérieure réussira : testez aussi l’opération elle-même.
Conclusion : Bash ne réfléchit pas, il vérifie des statuts
Les tests conditionnels transforment un script linéaire :
faire A
faire B
faire C
en une logique capable de réagir :
si A réussit
faire B
sinon
faire C
Mais le concept le plus important à retenir n’est ni :
[ ]
ni :
[[ ]]
ni même :
if
C’est :
le statut de sortie
Bash construit une grande partie de sa logique autour de cette convention :
0
→ succès
non zéro
→ autre résultat / échec
Une fois cette mécanique comprise, beaucoup de constructions deviennent naturelles :
if grep ...
if systemctl ...
if curl ...
if cp ...
if [[ ... ]]
if (( ... ))
Le bon réflexe n’est donc pas toujours :
« Comment transformer cette information en texte pour ensuite la comparer ? »
Mais plutôt :
« La commande que j’utilise fournit-elle déjà un statut indiquant exactement ce que je veux savoir ? »
Et avant une opération importante, une dernière condition reste toujours pertinente :
if je_sais_exactement_ce_que_fait_ce_script; then
executer
else
relire
fi
Cette fonction n’est malheureusement pas fournie avec Bash.
