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

# Bonnes pratiques et ShellCheck

```{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)
```

Bash est un langage au comportement parfois surprenant. Ses règles de quoting, son word splitting silencieux, ses globbing non sollicités et ses nombreuses subtilités historiques font que des scripts apparemment raisonnables peuvent se comporter de façon catastrophique sur des données réelles. Ce chapitre final rassemble les bonnes pratiques les plus importantes, présente les pièges classiques à éviter, et introduit ShellCheck — l'outil d'analyse statique qui détecte automatiquement la plupart de ces problèmes.

## Pièges de quoting : la règle fondamentale

Le quoting est sans doute la source de bugs la plus fréquente en Bash. La règle est simple mais doit être appliquée systématiquement.

```{prf:definition} La règle d'or du quoting
:label: definition-20-01
**Toujours mettre les variables entre guillemets doubles** : `"$variable"`, `"$@"`, `"$(commande)"`. Sans guillemets, Bash applique le **word splitting** (découpage sur les espaces, tabulations et sauts de ligne) et le **globbing** (expansion des caractères `*`, `?`, `[`) sur la valeur de la variable, ce qui peut produire des comportements inattendus ou destructeurs.
```

### Word splitting

```bash
fichier="mon fichier avec espaces.txt"

# FAUX : Bash voit deux arguments distincts : "mon" et "fichier"
cp $fichier /dest/              # Équivalent à cp mon fichier avec espaces.txt /dest/

# CORRECT : les guillemets préservent la valeur entière
cp "$fichier" /dest/

# FAUX : $@ sans guillemets ré-effectue le word splitting sur chaque argument
traiter_arguments() {
    for arg in $@; do           # Mauvais !
        echo "$arg"
    done
}

# CORRECT : "$@" préserve chaque argument comme une entité distincte
traiter_arguments() {
    for arg in "$@"; do         # Correct
        echo "$arg"
    done
}
```

### Globbing non voulu

```bash
# Si $pattern vaut "*.txt", ceci liste les fichiers .txt au lieu de chercher le pattern
ls "$pattern"           # Correct : cherche littéralement *.txt
ls $pattern             # DANGEREUX : s'étend en la liste des fichiers .txt

# Si une variable est vide, $@ sans guillemets peut s'étendre en rien du tout
# mais "$@" produit une liste vide, ce qui est le bon comportement
```

```{prf:remark}
:label: remark-20-01
Les seules situations où l'on omet intentionnellement les guillemets :

1. À l'intérieur de `[[ ]]` pour les comparaisons de motifs : `[[ $var == *.txt ]]` (ici le globbing côté droit est un comportement voulu).
2. Pour utiliser intentionnellement le word splitting pour découper une chaîne délimitée par des espaces — mais même là, un tableau est presque toujours préférable.
3. Pour les variables arithmétiques dans `$(( ))` où le word splitting n'a pas lieu.

Dans le doute : mettre des guillemets.
```

## Espaces dans les noms de fichiers

Les noms de fichiers Unix peuvent contenir n'importe quel caractère sauf `/` et le caractère nul (`\0`). En pratique, les espaces, les sauts de ligne, les tabulations, les apostrophes et autres caractères spéciaux sont légaux et courants sur les systèmes des utilisateurs.

```bash
# Démonstration du problème
for f in $(ls /home/utilisateur/Documents); do
    echo "Fichier : $f"
    wc -l "$f"    # Échouera si le nom contient des espaces
done
# Si un fichier s'appelle "rapport final.pdf", la boucle voit
# "rapport" et "final.pdf" comme deux fichiers distincts.

# Correction avec glob direct (pas de ls !)
for f in /home/utilisateur/Documents/*; do
    echo "Fichier : $f"
    wc -l "$f"    # Correct grâce aux guillemets
done

# Pour les noms avec des sauts de ligne (cas extrême), utiliser find avec -print0
find /repertoire -name "*.log" -print0 | while IFS= read -r -d '' fichier; do
    echo "Traitement : $fichier"
    gzip "$fichier"
done
```

```{prf:example} Lire les fichiers d'un répertoire de façon robuste
:label: example-20-01
Trois façons courantes de parcourir les fichiers d'un répertoire, de la moins à la plus robuste :

```bash
# Méthode 1 : fragile (word splitting, pas de gestion des espaces)
for f in $(ls *.txt); do echo "$f"; done

# Méthode 2 : robuste pour la plupart des cas (glob direct)
for f in *.txt; do echo "$f"; done

# Méthode 3 : maximalement robuste (gère les sauts de ligne dans les noms)
find . -maxdepth 1 -name "*.txt" -print0 \
    | while IFS= read -r -d '' f; do
        echo "$f"
    done
```
```

## `[[ ]]` plutôt que `[ ]`

Les tests en Bash peuvent s'écrire avec `[ ]` (conforme POSIX, disponible dans `sh`) ou `[[ ]]` (extension Bash, plus riche et plus sûr).

```{prf:definition} Différences entre `[ ]` et `[[ ]]`
:label: definition-20-02
`[[ ]]` est la **syntaxe de test étendue** de Bash. Elle offre plusieurs avantages sur `[ ]` :

- **Pas de word splitting ni de globbing** sur les variables à l'intérieur de `[[ ]]`, même sans guillemets. Cela dit, la convention est de toujours mettre des guillemets pour la clarté.
- **Opérateurs logiques lisibles** : `&&` et `||` au lieu de `-a` et `-o` (qui sont dépréciés dans `[ ]`).
- **Comparaison de chaînes avec patterns** : `[[ $var == *.txt ]]` avec glob côté droit.
- **Regex** : `[[ $var =~ ^[0-9]+$ ]]` pour tester une expression régulière ERE.
- **Pas de problème avec les chaînes vides** : `[[ -z $var ]]` est sûr même si `$var` est vide.
```

```bash
# Comparaison de chaînes
var="bonjour"
[ "$var" = "bonjour" ]      # [ ] : guillemets obligatoires
[[ $var == "bonjour" ]]     # [[ ]] : guillemets optionnels (mais recommandés)

# Opérateurs logiques
[ "$a" -gt 0 -a "$b" -lt 10 ]     # [ ] : opérateurs -a et -o (déconseillés)
[[ $a -gt 0 && $b -lt 10 ]]       # [[ ]] : && et || naturels

# Pattern matching (uniquement dans [[ ]])
fichier="rapport_2024_final.pdf"
[[ $fichier == *.pdf ]]           # Vrai si se termine par .pdf
[[ $fichier == rapport_* ]]       # Vrai si commence par rapport_

# Regex (uniquement dans [[ ]])
ip="192.168.1.100"
if [[ $ip =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then
    echo "Format IP valide"
fi

# Valeur numérique
if [[ $TIMEOUT =~ ^[0-9]+$ ]]; then
    echo "TIMEOUT est un entier positif"
fi

# Fichiers et répertoires
[[ -f "$fichier" ]]     # Fichier régulier existant
[[ -d "$repertoire" ]]  # Répertoire existant
[[ -r "$fichier" ]]     # Lisible
[[ -w "$fichier" ]]     # Accessible en écriture
[[ -x "$script" ]]      # Exécutable
[[ -s "$fichier" ]]     # Non vide (taille > 0)
[[ -L "$lien" ]]        # Lien symbolique
```

## Ne pas analyser la sortie de `ls`

L'une des erreurs les plus enseignées et les plus répandues est de vouloir analyser la sortie de `ls` pour obtenir la liste des fichiers d'un répertoire.

```{prf:remark}
:label: remark-20-02
**Ne jamais analyser la sortie de `ls`.** La sortie de `ls` est conçue pour être lue par des humains, pas parsée par des programmes. Elle peut :
- Tronquer les noms longs selon la largeur du terminal.
- Remplacer les caractères non imprimables par `?` selon les options.
- Changer de format selon les options, la locale et les versions du système.
- Ne pas gérer correctement les noms contenant des sauts de ligne.

Les alternatives correctes sont les **globs** et `find`.
```

```bash
# MAUVAIS : analyser ls
for f in $(ls /var/log/*.log); do ... done
nb=$(ls -l /tmp/ | grep "^-" | wc -l)

# CORRECT : globs
for f in /var/log/*.log; do ... done
nb=0
for f in /tmp/*; do [ -f "$f" ] && (( nb++ )); done

# CORRECT : find
find /tmp -maxdepth 1 -type f | wc -l

# Cas particulier : fichiers avec certains attributs
find /var/log -name "*.log" -newer /tmp/reference -size +1M
```

## La boucle `for f in $(cat liste)` : le piège

Une autre erreur courante est d'utiliser `for f in $(cat liste.txt)` pour parcourir les lignes d'un fichier.

```bash
# MAUVAIS : word splitting sur les espaces, globbing, lignes vides ignorées
for ligne in $(cat fichier.txt); do
    echo "Traitement : $ligne"
done

# Si une ligne vaut "rapport final.pdf", elle sera traitée comme deux entrées.
# Si une ligne vaut "*.txt", elle sera étendue en les fichiers .txt.

# CORRECT : while read avec redirection d'entrée
while IFS= read -r ligne; do
    echo "Traitement : $ligne"
done < fichier.txt

# CORRECT : while read depuis un pipe
grep "actif" users.txt | while IFS= read -r ligne; do
    echo "Utilisateur actif : $ligne"
done

# Explication des options :
# IFS= : ne pas supprimer les espaces de début et de fin de ligne
# -r   : ne pas interpréter les antislashes comme des échappements
```

```{prf:example} Lire un fichier ligne par ligne
:label: example-20-02
```bash
# Lire un fichier de configuration clé=valeur
while IFS='=' read -r cle valeur; do
    # Ignorer les commentaires et les lignes vides
    [[ "$cle" =~ ^[[:space:]]*# ]] && continue
    [[ -z "$cle" ]] && continue
    echo "Clé : $cle, Valeur : $valeur"
done < config.ini

# Lire un CSV (en supposant pas de guillemets dans les champs)
while IFS=',' read -r nom prenom email role; do
    echo "Utilisateur : $prenom $nom <$email> [$role]"
done < utilisateurs.csv
```

## ShellCheck : l'outil d'analyse statique

ShellCheck est un outil d'analyse statique pour les scripts shell qui détecte automatiquement une grande variété de bugs, pièges et problèmes de style. Il est comparable à `pylint` pour Python ou `eslint` pour JavaScript.

```{prf:definition} ShellCheck
:label: definition-20-03
**ShellCheck** est un analyseur statique open source pour les scripts shell (Bash, sh, dash, ksh). Il détecte les erreurs de quoting, les variables non définies, les constructions dépréciées, les pièges de portabilité et de nombreux autres problèmes. Il est disponible en ligne sur [shellcheck.net](https://www.shellcheck.net) et en ligne de commande.
```

### Installation

```bash
# Debian/Ubuntu
sudo apt install shellcheck

# Fedora/RHEL
sudo dnf install shellcheck

# macOS avec Homebrew
brew install shellcheck

# Avec cabal (Haskell)
cabal install ShellCheck

# Vérifier l'installation
shellcheck --version
```

### Utilisation en ligne de commande

```bash
# Analyser un script
shellcheck mon_script.sh

# Analyser plusieurs scripts
shellcheck *.sh scripts/**/*.sh

# Choisir le niveau de sévérité minimum affiché
shellcheck --severity=warning mon_script.sh

# Exclure certains codes d'erreur
shellcheck --exclude=SC2086,SC2046 mon_script.sh

# Format de sortie JSON (pour l'intégration dans des outils)
shellcheck --format=json mon_script.sh

# Format GCC (pour les éditeurs qui reconnaissent ce format)
shellcheck --format=gcc mon_script.sh
```

### Comprendre les codes ShellCheck

Chaque diagnostic ShellCheck est identifié par un code `SC` suivi d'un numéro :

```bash
# Exemple de sortie ShellCheck
$ shellcheck exemple.sh

In exemple.sh line 5:
for f in $(ls *.txt); do
         ^---------^ SC2045: Iterating over ls output is fragile.
                              Use globs.

In exemple.sh line 8:
cp $fichier /dest/
   ^------^ SC2086: Double quote to prevent globbing and word splitting.
```

### Directives inline

ShellCheck permet d'inhiber un avertissement spécifique sur une ligne ou une région :

```bash
# Inhiber un avertissement pour la ligne suivante
# shellcheck disable=SC2086
echo $variable_intentionnellement_sans_guillemets

# Inhiber pour une région
# shellcheck disable=SC2046,SC2086
commande_complexe_avec_raison_valide

# Inhiber pour tout le fichier (à éviter)
# shellcheck disable=SC2086

# Meilleure pratique : ajouter un commentaire expliquant pourquoi
variable="valeur avec espaces"
# shellcheck disable=SC2086
# Raison : la commande attend plusieurs arguments séparés
old_cmd $variable
```

### Intégration dans l'éditeur

ShellCheck s'intègre dans la plupart des éditeurs modernes :

```bash
# VS Code : extension "ShellCheck" (timonwong.shellcheck)
# Neovim : via ALE ou null-ls avec shellcheck comme linter
# Vim : via syntastic ou ALE
# Emacs : via flycheck avec sh-shellcheck

# Configuration recommandée pour VS Code (.vscode/settings.json)
{
    "shellcheck.enable": true,
    "shellcheck.run": "onType",
    "shellcheck.executablePath": "/usr/bin/shellcheck",
    "shellcheck.customArgs": ["--external-sources"]
}
```

### Intégration dans la CI

ShellCheck s'intègre naturellement dans une pipeline d'intégration continue :

```yaml
# GitHub Actions
name: ShellCheck
on: [push, pull_request]
jobs:
  shellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Vérification ShellCheck
        uses: ludeeus/action-shellcheck@master
        with:
          severity: warning
          scandir: './scripts'
```

```bash
# Hook pre-commit Git (.git/hooks/pre-commit)
#!/usr/bin/env bash
set -euo pipefail

# Vérifier les scripts shell modifiés
scripts_modifies=$(git diff --cached --name-only | grep -E '\.(sh|bash)$' || true)
if [ -n "$scripts_modifies" ]; then
    echo "Vérification ShellCheck..."
    shellcheck $scripts_modifies
fi

# Vérification syntaxique
bash_scripts=$(git diff --cached --name-only | grep -E '\.(sh|bash)$' || true)
for script in $bash_scripts; do
    bash -n "$script"
done
```

## Style et lisibilité

```{prf:definition} Conventions de nommage
:label: definition-20-04
Les conventions de nommage les plus répandues dans la communauté Bash :

- **`MAJUSCULES`** pour les **constantes** et les **variables d'environnement exportées** : `readonly MAX_TENTATIVES=3`, `export PATH`.
- **`minuscules_avec_underscores`** pour les **variables locales** et les **noms de fonctions** : `local compteur=0`, `calculer_moyenne()`.
- **`_prefixe`** parfois utilisé pour les variables internes à une bibliothèque ou module.
```

```bash
#!/usr/bin/env bash
# Style recommandé

# --- Constantes ---
readonly VERSION="2.1.0"
readonly REPERTOIRE_LOG="/var/log/mon-app"
readonly TIMEOUT_SECONDES=30

# --- Variables globales ---
verbose=false
mode_simulation=false

# --- Fonctions (snake_case, verbe_nom) ---
verifier_dependances() {
    local -a deps=("curl" "jq" "rsync")
    local dep
    for dep in "${deps[@]}"; do
        if ! command -v "$dep" &>/dev/null; then
            echo "Dépendance manquante : $dep" >&2
            return 1
        fi
    done
}

calculer_checksum_fichier() {
    local chemin="$1"
    sha256sum "$chemin" | awk '{ print $1 }'
}

# --- Indentation : 2 ou 4 espaces (cohérence dans tout le projet) ---
traiter_utilisateurs() {
    local fichier="$1"
    local compteur=0

    while IFS=',' read -r id nom email; do
        [[ "$id" == "id" ]] && continue   # Ignorer l'en-tête

        if [[ -z "$email" ]]; then
            echo "Avertissement : pas d'email pour l'utilisateur $id" >&2
            continue
        fi

        (( compteur++ ))
        $verbose && echo "Traitement de $nom ($email)"
    done < "$fichier"

    echo "Traitement terminé : $compteur utilisateurs traités."
}
```

### Commentaires

```bash
#!/usr/bin/env bash
# ===========================================================================
# deploy.sh — Script de déploiement de l'application
#
# Ce script effectue les étapes suivantes :
#   1. Vérification des prérequis
#   2. Récupération du code source
#   3. Construction de l'image Docker
#   4. Déploiement sur le cluster
#
# Usage    : ./deploy.sh [--env production|staging] [--tag VERSION]
# Prérequis: docker, kubectl, jq
# Auteur   : Équipe Infrastructure
# ===========================================================================

# Commentaire expliquant le POURQUOI (pas le comment)
# On utilise mktemp plutôt qu'un chemin fixe pour éviter les conditions de course
# dans un environnement où plusieurs instances du script peuvent tourner simultanément.
tmpdir=$(mktemp -d)

# TODO: Ajouter une vérification de l'espace disque disponible avant le build
taille_image=$(docker image inspect "$IMAGE" --format '{{.Size}}' 2>/dev/null || echo 0)
```

## Sécurité des scripts

### Injection de commandes

Le risque d'injection de commandes survient lorsqu'une entrée extérieure (argument de l'utilisateur, valeur d'une variable d'environnement, sortie d'une commande) est utilisée de façon non sécurisée dans une commande shell.

```bash
# DANGEREUX : injection possible si NOM_UTILISATEUR contient "; rm -rf /"
eval "ls /home/$NOM_UTILISATEUR"   # JAMAIS faire ça

# DANGEREUX : avec shell=True en Python ou eval en Bash
commande="cat $fichier_utilisateur"
eval "$commande"                   # DANGEREUX

# CORRECT : construire la commande comme une liste d'arguments
ls "/home/$NOM_UTILISATEUR"        # Les guillemets empêchent l'injection
```

```{prf:definition} L'instruction `eval` : à éviter
:label: definition-20-05
`eval` exécute son argument comme une commande shell après une double expansion. C'est l'un des rares cas où une commande Bash peut être véritablement dangereuse : si son argument contient des données non contrôlées, `eval` peut exécuter du code arbitraire avec les privilèges du script. Dans presque tous les cas, `eval` peut être remplacé par des tableaux Bash ou des variables de référence indirecte (`${!variable}`).
```

```bash
# Cas où eval est parfois tenté (et son alternative sûre)

# Mauvais : eval pour construire une commande dynamique
eval "commande_$action --flag"

# Correct : utiliser un tableau associatif ou un case
case "$action" in
    start)   demarrer_service ;;
    stop)    arreter_service ;;
    restart) redemarrer_service ;;
    *)       echo "Action inconnue : $action" >&2; exit 1 ;;
esac
```

### Gestion des secrets

```{prf:remark}
:label: remark-20-03
**Règles de sécurité pour les secrets dans les scripts :**

1. **Ne jamais mettre un mot de passe ou un token dans un script**, même dans un commentaire. Les scripts sont souvent versionnés dans Git, partagés, et lisibles par d'autres.
2. **Utiliser les variables d'environnement** pour passer les secrets au script, et les définir dans des fichiers `.env` exclus du versionnage (`.gitignore`).
3. **Utiliser un gestionnaire de secrets** : Vault (HashiCorp), AWS Secrets Manager, `pass`, ou les secrets de l'OS (keyring GNOME, macOS Keychain).
4. **Ne pas logger les secrets** : `set -x` affiche les valeurs des variables ; désactiver la trace pour les sections manipulant des secrets.
```

```bash
#!/usr/bin/env bash
set -euo pipefail

# Lire le token depuis une variable d'environnement (jamais en dur)
API_TOKEN="${API_TOKEN:?La variable API_TOKEN doit être définie}"

# Ou depuis un fichier de secrets (permissions 600)
if [ -f ~/.config/mon-app/token ]; then
    API_TOKEN=$(cat ~/.config/mon-app/token)
fi

# Désactiver set -x pour les sections sensibles
set +x
curl -s -H "Authorization: Bearer $API_TOKEN" https://api.exemple.fr/data
set -x  # Réactiver si nécessaire
```

### `umask` : permissions par défaut

```{prf:definition} `umask`
:label: definition-20-06
`umask` définit un **masque de permissions** qui est soustrait des permissions par défaut lors de la création de fichiers et répertoires. Un `umask` de `022` signifie que les fichiers sont créés avec les permissions `644` (rw-r--r--) et les répertoires avec `755` (rwxr-xr-x). Pour les scripts qui créent des fichiers contenant des données sensibles, un `umask` plus restrictif est conseillé.
```

```bash
#!/usr/bin/env bash

# Définir un umask restrictif pour ce script
umask 077   # Fichiers créés : 600 (rw-------)

# Les fichiers temporaires ne seront lisibles que par root/l'utilisateur courant
TMPFICHIER=$(mktemp)   # Créé avec 600 grâce au umask 077
echo "Données sensibles" > "$TMPFICHIER"
```

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

fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(-0.5, 14)
ax.set_ylim(-0.5, 10)
ax.axis('off')
ax.set_title('Top 10 des erreurs ShellCheck les plus fréquentes',
             fontsize=14, fontweight='bold', pad=20)

# Données : code, description, fréquence relative, catégorie
erreurs = [
    ('SC2086', 'Variable sans guillemets\n$var au lieu de "$var"', 95, 'Quoting'),
    ('SC2006', 'Backticks dépréciés\n`cmd` au lieu de $(cmd)', 82, 'Style'),
    ('SC2046', 'Expression sans guillemets\n$(cmd) sans "$(...)"', 78, 'Quoting'),
    ('SC2045', 'Itération sur la sortie de ls\nfor f in $(ls ...)', 70, 'Correctness'),
    ('SC2035', 'Glob commençant par un tiret\n-f* peut être une option', 60, 'Correctness'),
    ('SC2164', 'cd sans vérification\ncd /chemin sans || exit', 55, 'Robustesse'),
    ('SC2148', 'Shebang manquant\nscript sans #!/bin/bash', 50, 'Portabilité'),
    ('SC2034', 'Variable définie mais non utilisée\nlocal var=..., jamais lue', 45, 'Style'),
    ('SC2059', 'Percent dans printf\nprintf "$format" au lieu de "%s"', 40, 'Sécurité'),
    ('SC2155', 'Déclaration et affectation séparées\nlocal v=$(cmd) masque le code', 35, 'Robustesse'),
]

couleurs_cat = {
    'Quoting': '#e74c3c',
    'Style': '#3498db',
    'Correctness': '#e67e22',
    'Robustesse': '#27ae60',
    'Portabilité': '#9b59b6',
    'Sécurité': '#c0392b',
}

y_positions = np.arange(len(erreurs) - 1, -1, -1) * 0.92 + 0.5

for i, (code, desc, freq, cat) in enumerate(erreurs):
    y = y_positions[i]
    couleur = couleurs_cat[cat]

    # Barre de fréquence
    largeur_barre = freq / 100 * 7.0
    barre = patches.FancyBboxPatch((3.5, y - 0.30), largeur_barre, 0.60,
                                   boxstyle='round,pad=0.05', linewidth=1,
                                   edgecolor=couleur, facecolor=couleur, alpha=0.75)
    ax.add_patch(barre)

    # Code ShellCheck
    ax.text(3.4, y, code, ha='right', va='center',
            fontsize=9, fontweight='bold', color='#2c3e50',
            fontfamily='monospace')

    # Pourcentage
    ax.text(3.5 + largeur_barre + 0.15, y, f'{freq}%',
            ha='left', va='center', fontsize=8.5, color=couleur,
            fontweight='bold')

    # Description (à droite)
    ax.text(11.5, y, desc, ha='left', va='center',
            fontsize=7.8, color='#444', multialignment='left')

    # Badge catégorie
    b_cat = patches.FancyBboxPatch((10.3, y - 0.22), 0.9, 0.44,
                                   boxstyle='round,pad=0.08', linewidth=1,
                                   edgecolor=couleur, facecolor=couleur, alpha=0.15)
    ax.add_patch(b_cat)
    ax.text(10.75, y, cat[:5], ha='center', va='center',
            fontsize=7, color=couleur, fontweight='bold')

# En-têtes
ax.text(3.4, 9.6, 'Code', ha='right', va='center',
        fontsize=10, fontweight='bold', color='#2c3e50')
ax.text(7.0, 9.6, 'Fréquence relative', ha='center', va='center',
        fontsize=10, fontweight='bold', color='#2c3e50')
ax.text(11.5, 9.6, 'Description', ha='left', va='center',
        fontsize=10, fontweight='bold', color='#2c3e50')
ax.axhline(9.3, color='#bbb', lw=1, xmin=0.2, xmax=0.98)

# Axe des abscisses (pourcentages)
for pct in [0, 25, 50, 75, 100]:
    x = 3.5 + pct / 100 * 7.0
    ax.axvline(x, color='#ddd', lw=0.7, alpha=0.5,
               ymin=0.04, ymax=0.94)
    ax.text(x, 0.1, f'{pct}%', ha='center', va='bottom',
            fontsize=7.5, color='#888')

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

## Résumé et perspectives

Ce dernier chapitre a rassemblé les bonnes pratiques essentielles pour écrire du Bash fiable et maintenable :

- La **règle d'or du quoting** — `"$variable"` et non `$variable` — prévient le word splitting et le globbing non voulus, sources de la majorité des bugs Bash.
- Les **noms de fichiers avec espaces** nécessitent des globs directs (`for f in *.txt`) ou `find -print0` avec `read -d ''`, jamais `ls` parsé.
- `[[ ]]` est préférable à `[ ]` pour sa robustesse : pas de word splitting interne, opérateurs logiques naturels, pattern matching et regex.
- La boucle `while IFS= read -r ligne; do ... done < fichier` est le seul moyen correct de lire un fichier ligne par ligne.
- **ShellCheck** automatise la détection des pièges : son installation et son intégration dans l'éditeur et la CI sont vivement recommandées. Les codes les plus fréquents — SC2086 (quoting), SC2006 (backticks), SC2045 (ls) — correspondent précisément aux pièges décrits dans ce chapitre.
- Les conventions de **nommage** (MAJUSCULES pour les constantes, minuscules pour les variables locales), l'indentation cohérente et les commentaires explicatifs rendent les scripts maintenables sur le long terme.
- La **sécurité** implique d'éviter `eval`, de ne jamais mettre de secrets dans les scripts, de désactiver `set -x` autour des sections sensibles, et d'utiliser `umask` pour les fichiers de données confidentielles.

Ce livre vous a accompagné depuis les fondements de Linux et du terminal jusqu'aux techniques avancées d'automatisation, en passant par la programmation shell, les outils système, les expressions régulières et l'interopérabilité avec d'autres langages. Le shell est un outil vivant : sa maîtrise progresse par la pratique quotidienne, la lecture de scripts bien écrits, et l'instauration d'une discipline de vérification systématique avec ShellCheck. Chaque script que vous écrivez est une opportunité d'appliquer ces principes et de les faire progresser.
