---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Structures de contrôle

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

Un script qui n'exécute que des commandes de manière linéaire, sans décision ni répétition, est d'une utilité limitée. C'est grâce aux **structures de contrôle** — conditions, boucles, sélections — que le shell devient un véritable langage de programmation. Ce chapitre couvre les tests (avec les subtilités importantes entre `[[ ]]`, `[ ]` et `test`), les structures conditionnelles `if`/`elif`/`else`, la sélection multi-cas avec `case`, les boucles `while`, `until` et les trois formes de `for`, ainsi que les instructions de rupture `break`, `continue` et `exit`.

## Tests et conditions

La base de toute structure conditionnelle est l'évaluation d'une condition. En Bash, une condition est simplement une **commande dont on observe le code de retour** : 0 signifie vrai (succès), toute valeur non nulle signifie faux (échec). C'est l'inverse de la convention de la plupart des langages de haut niveau, mais c'est cohérent avec le modèle Unix des codes de sortie.

```{prf:definition} Tests en Bash : `[[ ]]`, `[ ]` et `test`
:label: definition-09-01
Bash propose trois façons d'évaluer des conditions :

- **`test expression`** : la commande externe (ou built-in) historique. Elle évalue l'expression et retourne 0 (vrai) ou 1 (faux). Portable POSIX.
- **`[ expression ]`** : syntaxe alternative à `test`, équivalente. Les espaces autour des crochets sont **obligatoires** (il s'agit littéralement d'appeler la commande `[` avec l'argument de fermeture `]`). Portable POSIX.
- **`[[ expression ]]`** : extension Bash (non POSIX). Syntaxe plus moderne et plus sûre. Les principales différences avec `[ ]` :
  - Pas de word splitting ni de globbing sur les variables (les guillemets deviennent optionnels pour les variables simples).
  - Supporte les opérateurs `&&` et `||` directement à l'intérieur.
  - Supporte les correspondances de motifs (`==` avec wildcards et `=~` pour les expressions régulières POSIX étendu).
  - La comparaison `<` et `>` compare lexicographiquement sans avoir besoin d'échapper les opérateurs.
```

### Pourquoi préférer `[[ ]]` à `[ ]`

```bash
# Variable contenant des espaces
fichier="mon fichier.txt"

# [ ] — DANGEREUX sans guillemets (word splitting transforme en 3 arguments)
[ -f $fichier ]    # ERREUR : [ -f mon fichier.txt ] → trop d'arguments

# [ ] — correct avec guillemets
[ -f "$fichier" ]

# [[ ]] — sûr même sans guillemets
[[ -f $fichier ]]

# Opérateurs logiques
# [ ] — nécessite -a et -o (ou des [ ] imbriqués)
[ -f "$fichier" -a -r "$fichier" ]

# [[ ]] — supporte && et ||
[[ -f $fichier && -r $fichier ]]

# Expressions régulières : impossible avec [ ]
[[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]

# Wildcards avec ==
[[ "$fichier" == *.txt ]]
```

### Opérateurs de comparaison de chaînes

```bash
a="bonjour"
b="monde"

[[ "$a" == "$b" ]]        # égalité
[[ "$a" != "$b" ]]        # différence
[[ "$a" < "$b" ]]         # ordre lexicographique inférieur (b avant m)
[[ "$a" > "$b" ]]         # ordre lexicographique supérieur
[[ -z "$a" ]]             # vrai si la chaîne est vide (-z : zero length)
[[ -n "$a" ]]             # vrai si la chaîne est non vide (-n : non-zero)

# Correspondance de motif (pattern matching)
fichier="rapport_2024_final.pdf"
[[ "$fichier" == *.pdf ]]           # vrai
[[ "$fichier" == rapport_* ]]       # vrai
[[ "$fichier" == *[0-9]* ]]         # vrai (contient des chiffres)

# Expression régulière avec =~
# IMPORTANT : le motif ne doit PAS être entre guillemets
motif='^rapport_[0-9]{4}'
[[ "$fichier" =~ $motif ]]          # vrai
# Les groupes de capture sont dans BASH_REMATCH
[[ "$fichier" =~ _([0-9]{4})_ ]]
echo "${BASH_REMATCH[1]}"            # 2024
```

### Opérateurs de comparaison d'entiers

Pour comparer des nombres, Bash utilise des opérateurs textuels spécifiques à l'intérieur de `[ ]` et `[[ ]]`. À l'intérieur de `(( ))`, les opérateurs mathématiques habituels (`<`, `>`, `==`, etc.) fonctionnent.

```{prf:definition} Opérateurs de comparaison d'entiers
:label: definition-09-02
| Opérateur | Signification | Exemple |
|-----------|---------------|---------|
| `-eq` | égal (*equal*) | `[[ $a -eq $b ]]` |
| `-ne` | différent (*not equal*) | `[[ $a -ne $b ]]` |
| `-lt` | strictement inférieur (*less than*) | `[[ $a -lt $b ]]` |
| `-le` | inférieur ou égal (*less or equal*) | `[[ $a -le $b ]]` |
| `-gt` | strictement supérieur (*greater than*) | `[[ $a -gt $b ]]` |
| `-ge` | supérieur ou égal (*greater or equal*) | `[[ $a -ge $b ]]` |

À l'intérieur de `(( ))`, on utilise directement `<`, `<=`, `>`, `>=`, `==`, `!=`.
```

```bash
x=10
y=20

[[ $x -eq 10 ]]    # vrai
[[ $x -ne $y ]]    # vrai
[[ $x -lt $y ]]    # vrai
[[ $x -ge 10 ]]    # vrai

# Avec (( )) — syntaxe plus naturelle
(( x == 10 ))     # vrai
(( x < y ))       # vrai
(( x >= 10 ))     # vrai
```

### Tests sur les fichiers

Les tests de fichiers sont parmi les plus utilisés dans les scripts Bash. Ils permettent de vérifier l'existence, le type et les permissions d'un fichier avant d'agir dessus.

```{prf:definition} Tests sur les fichiers
:label: definition-09-03
| Opérateur | Condition vérifiée |
|-----------|-------------------|
| `-e fichier` | le fichier existe (quelle que soit sa nature) |
| `-f fichier` | existe et est un **fichier ordinaire** |
| `-d fichier` | existe et est un **répertoire** |
| `-l fichier` | existe et est un **lien symbolique** |
| `-p fichier` | existe et est un **pipe nommé** |
| `-s fichier` | existe et a une **taille non nulle** |
| `-r fichier` | existe et est **lisible** par le processus courant |
| `-w fichier` | existe et est **accessible en écriture** |
| `-x fichier` | existe et est **exécutable** |
| `-b fichier` | existe et est un **fichier spécial bloc** |
| `-c fichier` | existe et est un **fichier spécial caractère** |
| `-N fichier` | a été **modifié depuis sa dernière lecture** |
| `f1 -nt f2` | f1 est **plus récent** que f2 (*newer than*) |
| `f1 -ot f2` | f1 est **plus ancien** que f2 (*older than*) |
| `f1 -ef f2` | f1 et f2 sont le **même fichier** (même inode) |
```

```bash
# Vérifications courantes avant traitement
fichier="/etc/passwd"

if [[ -e "$fichier" ]]; then
    echo "Le fichier existe"
fi

if [[ -f "$fichier" && -r "$fichier" ]]; then
    echo "Fichier lisible"
fi

if [[ ! -d "/tmp/mon_répertoire" ]]; then
    mkdir -p "/tmp/mon_répertoire"
fi

# Vérifier qu'un script est exécutable
if [[ ! -x "$1" ]]; then
    echo "ERREUR : '$1' n'est pas exécutable" >&2
    exit 1
fi

# Vérifier qu'un fichier n'est pas vide
if [[ -s "rapport.txt" ]]; then
    echo "Le rapport contient des données"
fi
```

### Opérateurs logiques

```bash
# && (ET) et || (OU) dans [[ ]]
[[ -f "$f" && -r "$f" ]]    # f existe, est un fichier ET est lisible
[[ -z "$a" || -z "$b" ]]    # a OU b est vide

# ! (NON)
[[ ! -f "$f" ]]             # f n'existe pas ou n'est pas un fichier ordinaire

# Chaînage de commandes avec && et || en dehors des tests
mkdir répertoire && cd répertoire           # cd seulement si mkdir réussit
commande || echo "ERREUR : commande échouée"  # message si la commande échoue
commande || exit 1                          # terminer si la commande échoue
```

## La structure `if`

```{prf:definition} Structure `if` en Bash
:label: definition-09-04
La structure conditionnelle Bash suit la syntaxe :

```
if condition1; then
    commandes_si_condition1_vraie
elif condition2; then
    commandes_si_condition2_vraie
else
    commandes_par_défaut
fi
```

La `condition` est n'importe quelle commande : `if` vérifie simplement son code de retour (0 = vrai). Le `then` peut être sur la même ligne que `if` (séparé par `;`) ou sur la ligne suivante. Les branches `elif` et `else` sont optionnelles.
```

### Exemples de structures `if`

```bash
# Test simple sur un fichier
if [[ -f "/etc/debian_version" ]]; then
    echo "Système Debian détecté"
fi

# Avec else
âge=25
if [[ $âge -ge 18 ]]; then
    echo "Majeur"
else
    echo "Mineur"
fi

# Avec elif — vérifier une plage de valeurs
note=75
if (( note >= 90 )); then
    mention="Très bien"
elif (( note >= 75 )); then
    mention="Bien"
elif (( note >= 60 )); then
    mention="Assez bien"
elif (( note >= 50 )); then
    mention="Passable"
else
    mention="Insuffisant"
fi
echo "Mention : $mention"

# Test de succès d'une commande
if grep -q "root" /etc/passwd; then
    echo "L'utilisateur root existe dans /etc/passwd"
fi

# Test avec code de retour explicite
if ! ping -c1 -W1 google.com &>/dev/null; then
    echo "Pas de connexion Internet détectée"
fi
```

### Conditions avec plusieurs critères

```bash
# Vérifier les prérequis d'un script
if [[ -z "$1" ]]; then
    echo "Usage : $0 <fichier>" >&2
    exit 1
fi

if [[ ! -f "$1" ]]; then
    echo "ERREUR : '$1' n'est pas un fichier ordinaire" >&2
    exit 2
fi

if [[ ! -r "$1" ]]; then
    echo "ERREUR : '$1' n'est pas lisible" >&2
    exit 3
fi

echo "Traitement de '$1'..."

# Vérification de l'environnement
if [[ -z "${DATABASE_URL:-}" || -z "${API_KEY:-}" ]]; then
    echo "ERREUR : les variables d'environnement DATABASE_URL et API_KEY sont requises" >&2
    exit 1
fi
```

## La structure `case`

La structure `case` est l'équivalent Bash des instructions `switch` des autres langages. Elle est plus lisible que les chaînes de `if`/`elif` quand on compare une variable à plusieurs valeurs.

```{prf:definition} Structure `case` en Bash
:label: definition-09-05
La syntaxe de `case` est :

```
case expression in
    motif1)
        commandes
        ;;
    motif2 | motif3)
        commandes
        ;;
    *)
        commandes_par_défaut
        ;;
esac
```

Les motifs supportent les wildcards shell (`*`, `?`, `[...]`). Le `;;` termine le bloc courant (équivalent de `break` en C). Deux variantes existent : `;&` (continue vers le bloc suivant sans vérifier le motif — *fall-through*) et `;;&` (continue à vérifier les motifs suivants).
```

### Exemples de `case`

```bash
# Sélection selon le système d'exploitation
os=$(uname -s)
case "$os" in
    Linux)
        echo "Système Linux"
        gestionnaire_paquets="apt"
        ;;
    Darwin)
        echo "Système macOS"
        gestionnaire_paquets="brew"
        ;;
    CYGWIN* | MINGW* | MSYS*)
        echo "Système Windows (émulation Unix)"
        gestionnaire_paquets="choco"
        ;;
    *)
        echo "Système non reconnu : $os" >&2
        exit 1
        ;;
esac

# Traitement des arguments en ligne de commande
action="${1:-}"
case "$action" in
    start | démarrer)
        démarrer_service
        ;;
    stop | arrêter)
        arrêter_service
        ;;
    restart | redémarrer)
        arrêter_service
        démarrer_service
        ;;
    status | état)
        vérifier_état
        ;;
    -h | --help | "")
        afficher_aide
        ;;
    *)
        echo "Action inconnue : '$action'" >&2
        afficher_aide
        exit 1
        ;;
esac

# Vérifier l'extension d'un fichier
fichier="$1"
case "${fichier,,}" in    # ,, convertit en minuscules (Bash 4+)
    *.jpg | *.jpeg | *.png | *.gif | *.webp)
        traiter_image "$fichier"
        ;;
    *.mp4 | *.avi | *.mkv | *.mov)
        traiter_vidéo "$fichier"
        ;;
    *.pdf | *.docx | *.odt)
        traiter_document "$fichier"
        ;;
    *.sh | *.bash)
        bash "$fichier"
        ;;
    *)
        echo "Type de fichier non pris en charge : $fichier" >&2
        ;;
esac
```

### `case` avec fall-through (`;;&`)

```bash
# ;;&  continue la vérification des motifs suivants
# ;&   tombe directement dans le bloc suivant sans vérifier

valeur=3
case "$valeur" in
    [1-3])
        echo "Entre 1 et 3"
        ;;&   # continue à vérifier
    [2-4])
        echo "Entre 2 et 4"
        ;;&   # continue à vérifier
    3)
        echo "Exactement 3"
        ;;    # stop
esac
# Affiche :
# Entre 1 et 3
# Entre 2 et 4
# Exactement 3
```

## La boucle `while`

```{prf:definition} Boucle `while`
:label: definition-09-06
La boucle `while` répète un bloc de commandes **tant que** la condition est vraie (code de retour 0) :

```
while condition; do
    commandes
done
```

La condition est réévaluée à chaque itération. Si elle est fausse dès le début, le corps de la boucle n'est jamais exécuté.
```

### Usages courants de `while`

```bash
# Compter jusqu'à 5
compteur=1
while (( compteur <= 5 )); do
    echo "Itération $compteur"
    ((compteur++))
done

# Lire un fichier ligne par ligne (idiome fondamental)
while IFS= read -r ligne; do
    echo "Ligne : $ligne"
done < fichier.txt

# Lire et traiter un CSV (en ignorant l'en-tête)
while IFS=',' read -r nom prénom âge ville; do
    echo "$nom $prénom habite à $ville (${âge} ans)"
done < <(tail -n +2 utilisateurs.csv)  # Ignorer la première ligne

# Attendre qu'un service soit disponible
délai_max=60
décompte=0
while ! curl -s http://localhost:8080/health &>/dev/null; do
    if (( décompte >= délai_max )); then
        echo "ERREUR : service non disponible après ${délai_max}s" >&2
        exit 1
    fi
    echo "En attente du service... (${décompte}s)"
    sleep 2
    ((décompte += 2))
done
echo "Service disponible !"

# Boucle infinie avec break
while true; do
    read -p "Entrez une commande (q pour quitter) : " cmd
    case "$cmd" in
        q | quit | exit)
            echo "Au revoir !"
            break
            ;;
        "")
            continue
            ;;
        *)
            eval "$cmd"
            ;;
    esac
done
```

### `IFS= read -r` — l'idiome correct pour lire des lignes

```bash
# CORRECT : IFS= préserve les espaces, -r évite l'interprétation des backslashes
while IFS= read -r ligne; do
    echo "$ligne"
done < fichier.txt

# INCORRECT (courant mais cassé sur les lignes avec espaces ou backslashes)
while read ligne; do   # word splitting actif, backslashes interprétés
    echo "$ligne"
done < fichier.txt

# Lire plusieurs champs séparés par un délimiteur
while IFS=':' read -r utilisateur _ uid gid _ répertoire shell; do
    echo "$utilisateur (uid=$uid) → $shell"
done < /etc/passwd
```

## La boucle `until`

La boucle `until` est l'inverse de `while` : elle répète le bloc **tant que** la condition est **fausse** (code de retour non nul).

```bash
# Equivalent de while (( compteur > 0 ))
compteur=5
until (( compteur == 0 )); do
    echo "Décompte : $compteur"
    ((compteur--))
done
echo "Décollage !"

# Attendre qu'un processus se termine
pid=$!  # PID du dernier processus lancé en arrière-plan
until ! kill -0 "$pid" 2>/dev/null; do
    echo "En attente de la fin du processus $pid..."
    sleep 1
done
echo "Processus $pid terminé"

# Attendre qu'un fichier apparaisse
until [[ -f /tmp/signal_départ ]]; do
    sleep 0.5
done
echo "Signal reçu, démarrage..."
```

## La boucle `for`

La boucle `for` de Bash existe sous trois formes distinctes, chacune adaptée à des usages différents.

### Itération sur une liste

```bash
# Itération sur une liste littérale
for fruit in pomme banane cerise fraise; do
    echo "Fruit : $fruit"
done

# Itération sur des fichiers (globbing)
for fichier in /var/log/*.log; do
    echo "Taille de $fichier : $(wc -l < "$fichier") lignes"
done

# Itération sur la sortie d'une commande
for utilisateur in $(cut -d':' -f1 /etc/passwd | head -5); do
    echo "Utilisateur : $utilisateur"
done

# Itération sur un tableau
fruits=("pomme" "banane" "cerise")
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done

# Itération sur les arguments d'un script ($@)
for arg in "$@"; do
    echo "Argument : '$arg'"
done
```

```{prf:remark}
:label: remark-09-01
Il existe une subtilité importante avec la forme `for elem in $(commande)` : la sortie de la commande est soumise au *word splitting* et au *globbing*. Si les éléments peuvent contenir des espaces (comme des noms de fichiers), cette forme est dangereuse. La forme correcte pour itérer sur les lignes d'une commande est soit `while IFS= read -r` avec une substitution de processus, soit de stocker les résultats dans un tableau avec `mapfile`.

```bash
# DANGEREUX avec des noms de fichiers contenant des espaces
for f in $(find . -name "*.txt"); do ...  # CASSÉ

# CORRECT
while IFS= read -r f; do
    ...
done < <(find . -name "*.txt")

# AUSSI CORRECT (avec mapfile / readarray)
mapfile -t fichiers < <(find . -name "*.txt")
for f in "${fichiers[@]}"; do ...
```
```

### Itération sur une plage `{début..fin}`

```bash
# Plage numérique
for i in {1..10}; do
    echo "Nombre : $i"
done

# Plage avec pas
for i in {0..20..5}; do
    echo "$i"
done
# 0 5 10 15 20

# Plage alphabétique
for lettre in {a..z}; do
    echo -n "$lettre "
done
echo

# ATTENTION : les variables ne fonctionnent PAS dans les plages
n=5
for i in {1..$n}; do  # INCORRECT : {1..5} n'est pas expansé
    echo "$i"          # Affiche littéralement {1..$n}
done

# Solution : utiliser seq
for i in $(seq 1 "$n"); do
    echo "$i"
done

# Ou la boucle style C
for (( i=1; i<=n; i++ )); do
    echo "$i"
done
```

### Boucle `for` style C

```bash
# Syntaxe analogue au C
for (( i=0; i<10; i++ )); do
    echo "i = $i"
done

# Avec pas
for (( i=100; i>=0; i-=10 )); do
    echo "$i"
done

# Boucle double (tableau 2D)
for (( i=1; i<=3; i++ )); do
    for (( j=1; j<=3; j++ )); do
        printf "%4d" $((i*j))
    done
    echo
done
# Affiche la table de multiplication 3×3

# Itérer sur les indices d'un tableau
tableau=("alpha" "beta" "gamma" "delta")
for (( i=0; i<${#tableau[@]}; i++ )); do
    echo "tableau[$i] = ${tableau[$i]}"
done
```

## Contrôle du flux : `break`, `continue` et `exit`

```{prf:definition} Instructions de rupture de flux
:label: definition-09-07
- **`break [N]`** : interrompt la boucle courante (ou les N boucles imbriquées si N est spécifié) et passe à la commande suivant la boucle.
- **`continue [N]`** : passe immédiatement à l'itération suivante de la boucle courante (ou de la N-ième boucle imbriquée).
- **`exit [N]`** : termine le script courant (ou le sous-shell courant) avec le code de retour N (0 = succès, 1-255 = erreur). Si N est omis, utilise le code de retour de la dernière commande.
- **`return [N]`** : utilisé à l'intérieur d'une fonction, retourne avec le code N. En dehors d'une fonction, équivalent à `exit`.
```

```bash
# break — interrompre la recherche dès le premier résultat
trouvé=false
for fichier in /etc/*.conf; do
    if grep -q "motif_recherché" "$fichier" 2>/dev/null; then
        echo "Trouvé dans : $fichier"
        trouvé=true
        break
    fi
done
[[ $trouvé == false ]] && echo "Motif non trouvé"

# continue — sauter les fichiers vides
for fichier in /var/log/*.log; do
    [[ ! -s "$fichier" ]] && continue   # Ignorer les fichiers vides
    echo "Traitement de $fichier ($(wc -l < "$fichier") lignes)"
done

# break N — interrompre deux boucles imbriquées
for i in {1..5}; do
    for j in {1..5}; do
        if (( i * j > 10 )); then
            echo "Premier produit > 10 : $i × $j = $((i*j))"
            break 2   # Sort des DEUX boucles
        fi
    done
done

# exit avec code de retour
vérifier_prérequis() {
    if ! command -v git &>/dev/null; then
        echo "ERREUR : git n'est pas installé" >&2
        exit 1
    fi
    if ! command -v python3 &>/dev/null; then
        echo "ERREUR : python3 n'est pas installé" >&2
        exit 1
    fi
    echo "Prérequis satisfaits"
}
```

## Patterns avancés

### Le pattern guard (vérification anticipée)

```bash
#!/usr/bin/env bash
# Vérifications au début du script, avant tout traitement
[[ $# -eq 0 ]] && { echo "Usage : $0 <fichier>" >&2; exit 1; }
[[ ! -f "$1" ]] && { echo "ERREUR : fichier non trouvé : $1" >&2; exit 2; }
[[ ! -r "$1" ]] && { echo "ERREUR : fichier non lisible : $1" >&2; exit 3; }
```

### Sélection interactive avec `select`

```bash
# Menu interactif
PS3="Choisissez une option : "
options=("Démarrer le service" "Arrêter le service" "Voir le statut" "Quitter")

select choix in "${options[@]}"; do
    case "$REPLY" in
        1) démarrer_service ;;
        2) arrêter_service ;;
        3) voir_statut ;;
        4) echo "Au revoir !"; break ;;
        *) echo "Option invalide : $REPLY" ;;
    esac
done
```

### Tableaux de dispatch (alternative aux if/elif chaînés)

```bash
# Au lieu d'une longue chaîne if/elif, utiliser un tableau associatif
declare -A commandes=(
    [start]="démarrer_service"
    [stop]="arrêter_service"
    [restart]="redémarrer_service"
    [status]="voir_statut"
)

action="${1:-}"
if [[ -n "${commandes[$action]+x}" ]]; then
    "${commandes[$action]}"
else
    echo "Action inconnue : '$action'" >&2
    echo "Actions disponibles : ${!commandes[*]}" >&2
    exit 1
fi
```

## Visualisation : organigramme des structures de contrôle

```{code-cell} python
:tags: [hide-input]

fig, axes = plt.subplots(1, 2, figsize=(18, 10))
palette = sns.color_palette("muted", 8)

# ============================================================
# Schéma 1 : Structures conditionnelles
# ============================================================
ax = axes[0]
ax.set_xlim(-1, 12)
ax.set_ylim(-1, 12)
ax.axis('off')
ax.set_title('Structures conditionnelles', fontsize=14, fontweight='bold')

def losange(ax, x, y, dx, dy, couleur, texte, fontsize=9):
    coords = [(x, y+dy), (x+dx, y), (x, y-dy), (x-dx, y)]
    poly = patches.Polygon(coords, closed=True, linewidth=2,
                            edgecolor=couleur, facecolor=(*couleur[:3], 0.2))
    ax.add_patch(poly)
    ax.text(x, y, texte, ha='center', va='center', fontsize=fontsize,
            fontweight='bold', color=couleur)

def rect_arrondi(ax, x, y, w, h, couleur, texte, fontsize=9.5):
    r = patches.FancyBboxPatch((x - w/2, y - h/2), w, h,
                                boxstyle="round,pad=0.1", linewidth=2,
                                edgecolor=couleur, facecolor=(*couleur[:3], 0.2))
    ax.add_patch(r)
    ax.text(x, y, texte, ha='center', va='center', fontsize=fontsize,
            fontweight='bold', color=couleur, fontfamily='monospace')

def fleche_v(ax, x, y_start, y_end, couleur='#555555', label=None, côté='droite'):
    ax.annotate('', xy=(x, y_end), xytext=(x, y_start),
                arrowprops=dict(arrowstyle='->', color=couleur, lw=2))
    if label:
        décalage = 0.2 if côté == 'droite' else -0.2
        ax.text(x + décalage, (y_start + y_end) / 2, label,
                ha='left' if côté == 'droite' else 'right', va='center',
                fontsize=8.5, color=couleur, fontweight='bold')

def fleche_h(ax, x_start, x_end, y, couleur='#555555', label=None):
    ax.annotate('', xy=(x_end, y), xytext=(x_start, y),
                arrowprops=dict(arrowstyle='->', color=couleur, lw=2))
    if label:
        ax.text((x_start + x_end) / 2, y + 0.2, label,
                ha='center', va='bottom', fontsize=8.5, color=couleur,
                fontweight='bold')

# Début
rect_arrondi(ax, 5.5, 11, 2.5, 0.7, palette[0], 'if condition', fontsize=10)
losange(ax, 5.5, 9.5, 2.0, 0.9, palette[1], 'condition\nvraie ?')

# Flèche vers losange
fleche_v(ax, 5.5, 10.65, 10.4)

# Branche VRAI
fleche_h(ax, 5.5, 8.5, 9.5, palette[2], 'OUI')
rect_arrondi(ax, 9.5, 9.5, 2.2, 0.7, palette[2], 'then\ncommandes')

# Branche FAUX
fleche_h(ax, 3.5, 2.5, 9.5, palette[3], 'NON')
rect_arrondi(ax, 1.5, 9.5, 2.2, 0.7, palette[3], 'else\ncommandes')

# Jonction en bas
ax.plot([9.5, 9.5], [9.15, 8.0], color='#555555', lw=2)
ax.plot([1.5, 1.5], [9.15, 8.0], color='#555555', lw=2)
ax.plot([1.5, 9.5], [8.0, 8.0], color='#555555', lw=2)
ax.annotate('', xy=(5.5, 7.3), xytext=(5.5, 8.0),
            arrowprops=dict(arrowstyle='->', color='#555555', lw=2))
rect_arrondi(ax, 5.5, 7.0, 2.0, 0.7, palette[0], 'fi', fontsize=10)

# Structure case
rect_arrondi(ax, 5.5, 5.5, 2.5, 0.7, palette[4], 'case $var in', fontsize=10)
fleche_v(ax, 5.5, 6.65, 5.85)

motifs = [('motif1)', 3.0), ('motif2)', 5.5), ('*)', 8.0)]
for label, x_m in motifs:
    ax.plot([5.5, x_m], [5.2, 5.2], color='#555555', lw=1.5)
    ax.annotate('', xy=(x_m, 4.7), xytext=(x_m, 5.2),
                arrowprops=dict(arrowstyle='->', color='#555555', lw=1.5))
    rect_arrondi(ax, x_m, 4.3, 1.8, 0.7, palette[5], label, fontsize=8.5)

# Jonction case
for x_m in [3.0, 5.5, 8.0]:
    ax.plot([x_m, x_m], [3.95, 3.3], color='#555555', lw=1.5)
ax.plot([3.0, 8.0], [3.3, 3.3], color='#555555', lw=1.5)
ax.annotate('', xy=(5.5, 2.7), xytext=(5.5, 3.3),
            arrowprops=dict(arrowstyle='->', color='#555555', lw=2))
rect_arrondi(ax, 5.5, 2.4, 1.5, 0.6, palette[4], 'esac', fontsize=10)

# ============================================================
# Schéma 2 : Structures itératives
# ============================================================
ax2 = axes[1]
ax2.set_xlim(-1, 13)
ax2.set_ylim(-1, 12)
ax2.axis('off')
ax2.set_title('Structures itératives', fontsize=14, fontweight='bold')

# Structure while
rect_arrondi(ax2, 2.5, 11, 3.0, 0.7, palette[0], 'while / until', fontsize=10)
fleche_v(ax2, 2.5, 10.65, 10.4)
losange(ax2, 2.5, 9.7, 2.0, 0.9, palette[1], 'condition\nvraie ?')

# Boucle vraie
fleche_h(ax2, 2.5, 4.5, 9.7, palette[2], 'OUI')
rect_arrondi(ax2, 5.5, 9.7, 2.2, 0.7, palette[2], 'do\ncommandes')
# Retour en haut
ax2.plot([5.5, 7.5, 7.5, 2.5], [9.35, 9.35, 10.2, 10.2], color=palette[2], lw=2)
ax2.annotate('', xy=(2.5, 10.4), xytext=(2.5, 10.2),
            arrowprops=dict(arrowstyle='->', color=palette[2], lw=2))

# Sortie boucle
fleche_h(ax2, 0.5, -0.3, 9.7, palette[3], 'NON')
ax2.plot([-0.3, -0.3], [9.7, 8.7], color=palette[3], lw=2)
ax2.annotate('', xy=(2.5, 8.7), xytext=(-0.3, 8.7),
            arrowprops=dict(arrowstyle='->', color=palette[3], lw=2))
rect_arrondi(ax2, 2.5, 8.4, 1.5, 0.5, palette[0], 'done', fontsize=10)

# break et continue
rect_arrondi(ax2, 5.5, 7.5, 2.5, 0.7, palette[5], 'break', fontsize=10)
rect_arrondi(ax2, 5.5, 6.3, 2.5, 0.7, palette[6], 'continue', fontsize=10)

ax2.text(5.5, 8.5, 'Instructions de rupture :', ha='center', va='center',
         fontsize=10, fontweight='bold', color='#2c3e50')
ax2.annotate('', xy=(4.0, 8.1), xytext=(5.5, 7.85),
            arrowprops=dict(arrowstyle='->', color=palette[5], lw=2,
                            linestyle='dashed'))
ax2.text(4.4, 8.05, '→ sort de la boucle', ha='left', va='center',
         fontsize=8.5, color=palette[5], style='italic')

ax2.annotate('', xy=(7.2, 9.9), xytext=(5.5, 6.65),
            arrowprops=dict(arrowstyle='->', color=palette[6], lw=2,
                            linestyle='dashed', connectionstyle='arc3,rad=0.3'))
ax2.text(7.4, 9.1, '→ prochaine\n   itération', ha='left', va='center',
         fontsize=8.5, color=palette[6], style='italic')

# Boucle for
ax2.text(10.0, 11.0, 'for elem in liste', ha='center', va='center',
         fontsize=10, fontweight='bold', fontfamily='monospace', color=palette[0])
ax2.text(10.0, 10.5, 'for ((i=0;i<n;i++))', ha='center', va='center',
         fontsize=9, fontfamily='monospace', color=palette[1])
ax2.text(10.0, 10.0, 'for i in {1..10}', ha='center', va='center',
         fontsize=9, fontfamily='monospace', color=palette[2])

# Légende des trois formes de for
formes = [
    ('for var in liste', 'Itération sur une liste\n(fichiers, args, tableau)'),
    ('for ((;;))', 'Style C : init/condition/incrément'),
    ('for i in {m..n}', 'Plage numérique ou alphabétique'),
]
for i, (synt, desc) in enumerate(formes):
    y_f = 8.5 - i * 1.8
    fond = patches.FancyBboxPatch(
        (8.2, y_f - 0.65), 4.5, 1.35,
        boxstyle="round,pad=0.08", linewidth=1.5,
        edgecolor=palette[i], facecolor=(*palette[i][:3], 0.12)
    )
    ax2.add_patch(fond)
    ax2.text(8.45, y_f + 0.2, synt, ha='left', va='center',
             fontsize=9.5, fontweight='bold', color=palette[i],
             fontfamily='monospace')
    ax2.text(8.45, y_f - 0.3, desc, ha='left', va='center',
             fontsize=8, color='#555555', style='italic')

# exit
ax2.text(2.5, 1.5, 'exit [N]', ha='center', va='center',
         fontsize=11, fontweight='bold', fontfamily='monospace', color=palette[3])
ax2.text(2.5, 1.0, 'Terminer le script\ncode = N (0 = succès)',
         ha='center', va='center', fontsize=8.5, color='#555555', style='italic')

fond_exit = patches.FancyBboxPatch(
    (0.5, 0.5), 4.0, 1.5,
    boxstyle="round,pad=0.1", linewidth=2,
    edgecolor=palette[3], facecolor=(*palette[3][:3], 0.12)
)
ax2.add_patch(fond_exit)

plt.tight_layout()
plt.show()
```

```{code-cell} python
:tags: [hide-input]

# Tableau récapitulatif des opérateurs de test
fig, ax = plt.subplots(figsize=(16, 7))
ax.axis('off')
ax.set_title('Opérateurs de test en Bash — comparaison et référence rapide',
             fontsize=14, fontweight='bold', pad=20)

palette2 = sns.color_palette("muted", 5)

catégories = [
    {
        'titre': 'Chaînes',
        'couleur': palette2[0],
        'ops': [
            ('== ou =', 'Égalité', '"$a" == "$b"'),
            ('!=', 'Différence', '"$a" != "$b"'),
            ('<, >', 'Ordre lex.', '"$a" < "$b"'),
            ('-z', 'Vide', '-z "$a"'),
            ('-n', 'Non vide', '-n "$a"'),
            ('=~', 'Regex', '"$a" =~ motif'),
        ]
    },
    {
        'titre': 'Entiers',
        'couleur': palette2[1],
        'ops': [
            ('-eq', 'Égal', '$a -eq $b'),
            ('-ne', 'Différent', '$a -ne $b'),
            ('-lt', 'Inférieur', '$a -lt $b'),
            ('-le', 'Inf. ou égal', '$a -le $b'),
            ('-gt', 'Supérieur', '$a -gt $b'),
            ('-ge', 'Sup. ou égal', '$a -ge $b'),
        ]
    },
    {
        'titre': 'Fichiers',
        'couleur': palette2[2],
        'ops': [
            ('-e', 'Existe', '-e fichier'),
            ('-f', 'Fichier', '-f fichier'),
            ('-d', 'Répertoire', '-d répert.'),
            ('-r/-w/-x', 'Droits', '-r fichier'),
            ('-s', 'Non vide', '-s fichier'),
            ('-nt/-ot', 'Plus récent', 'f1 -nt f2'),
        ]
    },
]

n_cols = len(catégories)
col_w = 5.0
col_x = [1.5, 6.5, 11.5]

for j, (cat, x_c) in enumerate(zip(catégories, col_x)):
    c = cat['couleur']
    # En-tête de colonne
    fond_entête = patches.FancyBboxPatch(
        (x_c - 2.2, 6.3), 4.4, 0.7,
        boxstyle="round,pad=0.1", linewidth=2,
        edgecolor=c, facecolor=c, alpha=0.85
    )
    ax.add_patch(fond_entête)
    ax.text(x_c, 6.65, cat['titre'], ha='center', va='center',
            fontsize=12, fontweight='bold', color='white')

    for i, (op, desc, ex) in enumerate(cat['ops']):
        y_op = 5.7 - i * 0.87
        fond_op = patches.FancyBboxPatch(
            (x_c - 2.2, y_op - 0.3), 4.4, 0.8,
            boxstyle="round,pad=0.05", linewidth=1,
            edgecolor=c, facecolor=(*c[:3], 0.08)
        )
        ax.add_patch(fond_op)
        ax.text(x_c - 2.0, y_op + 0.12, op,
                ha='left', va='center', fontsize=10, fontweight='bold',
                color=c, fontfamily='monospace')
        ax.text(x_c - 0.3, y_op + 0.12, desc,
                ha='left', va='center', fontsize=9, color='#444444')
        ax.text(x_c - 0.3, y_op - 0.15, f'[[ {ex} ]]',
                ha='left', va='center', fontsize=8, color='#888888',
                style='italic', fontfamily='monospace')

plt.tight_layout()
plt.show()
```

## Résumé

Dans ce chapitre, nous avons parcouru l'intégralité des structures de contrôle de Bash :

- **`[[ ]]`** est la forme recommandée pour les tests en Bash : pas de word splitting ni de globbing accidentel, support des opérateurs `&&`/`||`, des wildcards avec `==` et des expressions régulières avec `=~`. **`[ ]`** est l'alternative portable POSIX mais requiert plus de précautions.
- Les **opérateurs de comparaison** diffèrent selon le type : chaînes (`=`, `!=`, `<`, `>`), entiers (`-eq`, `-ne`, `-lt`, `-gt`, `-le`, `-ge`), fichiers (`-e`, `-f`, `-d`, `-r`, `-w`, `-x`, `-s`). À l'intérieur de `(( ))`, les opérateurs mathématiques usuels s'appliquent.
- **`if`/`elif`/`else`** évalue le code de retour de n'importe quelle commande (0 = vrai). Les branches `elif` permettent les chaînes de conditions.
- **`case`** teste une expression contre des motifs avec wildcards. Le `;;` termine chaque branche, `;;&` continue la vérification des motifs suivants.
- **`while`** répète tant que la condition est vraie ; **`until`** répète tant qu'elle est fausse. L'idiome `while IFS= read -r ligne; do ... done < fichier` est la façon correcte de lire un fichier ligne par ligne.
- **`for`** existe en trois variantes : liste (`for x in ...`), plage (`for x in {1..10}`), style C (`for ((;;))`). La forme liste est dangereuse avec `$(commande)` et des noms contenant des espaces — préférer `while read` dans ce cas.
- **`break`**, **`continue`** et **`exit`** contrôlent le flux dans les boucles et les scripts. `break N` et `continue N` permettent de gérer les boucles imbriquées.

Dans le chapitre suivant, nous aborderons l'écriture de **fonctions et de scripts professionnels** : organisation du code, gestion des arguments, portée des variables, codes de retour et bonnes pratiques pour les scripts robustes en production.
