Passer au contenu principal
Linux, OS

Tests conditionnels Bash : if, test, [[ ]] et (( )) sans ambiguïté

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, w ou x est-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 :

  • fi manquant ;
  • 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

  • if teste le statut d’une commande ou d’une liste de commandes.
  • Le statut 0 représente le succès dans un contexte conditionnel Bash.
  • [ ... ] est une forme du builtin test, 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_REMATCH permet 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.
  • -e et -L ne répondent pas exactement à la même question lorsqu’un lien symbolique est cassé.
  • -r, -w et -x sont des tests d’accès, pas de simples lectures de caractères affichés par ls -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 || C n’est pas un remplacement général de if A; then B; else C; fi.
  • case est 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.