Bonnes pratiques et ShellCheck#
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.
Définition 65 (La règle d’or du quoting)
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#
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#
# 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
Remarque 55
Les seules situations où l’on omet intentionnellement les guillemets :
À l’intérieur de
[[ ]]pour les comparaisons de motifs :[[ $var == *.txt ]](ici le globbing côté droit est un comportement voulu).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.
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.
# 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
Exemple 34 (Lire les fichiers d’un répertoire de façon robuste)
Trois façons courantes de parcourir les fichiers d’un répertoire, de la moins à la plus robuste :
# 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.
# 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.
Remarque 56
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.
# 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.
# 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
Exemple 35 (Lire un fichier ligne par ligne)
# 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.
Définition 66 (ShellCheck)
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 et en ligne de commande.
Installation#
# 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#
# 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 :
# 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 :
# 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 :
# 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 :
# 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'
# 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é#
Définition 67 (Conventions de nommage)
Les conventions de nommage les plus répandues dans la communauté Bash :
MAJUSCULESpour les constantes et les variables d’environnement exportées :readonly MAX_TENTATIVES=3,export PATH.minuscules_avec_underscorespour les variables locales et les noms de fonctions :local compteur=0,calculer_moyenne()._prefixeparfois utilisé pour les variables internes à une bibliothèque ou module.
#!/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#
#!/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.
# 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#
Remarque 57
Règles de sécurité pour les secrets dans les scripts :
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.
Utiliser les variables d’environnement pour passer les secrets au script, et les définir dans des fichiers
.envexclus du versionnage (.gitignore).Utiliser un gestionnaire de secrets : Vault (HashiCorp), AWS Secrets Manager,
pass, ou les secrets de l’OS (keyring GNOME, macOS Keychain).Ne pas logger les secrets :
set -xaffiche les valeurs des variables ; désactiver la trace pour les sections manipulant des secrets.
#!/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"
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[2], line 83
78 ax.axvline(x, color='#ddd', lw=0.7, alpha=0.5,
79 ymin=0.04, ymax=0.94)
80 ax.text(x, 0.1, f'{pct}%', ha='center', va='bottom',
81 fontsize=7.5, color='#888')
---> 83 plt.tight_layout()
84 plt.show()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/pyplot.py:2843, in tight_layout(pad, h_pad, w_pad, rect)
2835 @_copy_docstring_and_deprecators(Figure.tight_layout)
2836 def tight_layout(
2837 *,
(...) 2841 rect: tuple[float, float, float, float] | None = None,
2842 ) -> None:
-> 2843 gcf().tight_layout(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/figure.py:3640, in Figure.tight_layout(self, pad, h_pad, w_pad, rect)
3638 previous_engine = self.get_layout_engine()
3639 self.set_layout_engine(engine)
-> 3640 engine.execute(self)
3641 if previous_engine is not None and not isinstance(
3642 previous_engine, (TightLayoutEngine, PlaceHolderLayoutEngine)
3643 ):
3644 _api.warn_external('The figure layout has changed to tight')
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/layout_engine.py:188, in TightLayoutEngine.execute(self, fig)
186 renderer = fig._get_renderer()
187 with getattr(renderer, "_draw_disabled", nullcontext)():
--> 188 kwargs = get_tight_layout_figure(
189 fig, fig.axes, get_subplotspec_list(fig.axes), renderer,
190 pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'],
191 rect=info['rect'])
192 if kwargs:
193 fig.subplots_adjust(**kwargs)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_tight_layout.py:266, in get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer, pad, h_pad, w_pad, rect)
261 return {}
262 span_pairs.append((
263 slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row),
264 slice(ss.colspan.start * div_col, ss.colspan.stop * div_col)))
--> 266 kwargs = _auto_adjust_subplotpars(fig, renderer,
267 shape=(max_nrows, max_ncols),
268 span_pairs=span_pairs,
269 subplot_list=subplot_list,
270 ax_bbox_list=ax_bbox_list,
271 pad=pad, h_pad=h_pad, w_pad=w_pad)
273 # kwargs can be none if tight_layout fails...
274 if rect is not None and kwargs is not None:
275 # if rect is given, the whole subplots area (including
276 # labels) will fit into the rect instead of the
(...) 280 # auto_adjust_subplotpars twice, where the second run
281 # with adjusted rect parameters.
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_tight_layout.py:82, in _auto_adjust_subplotpars(fig, renderer, shape, span_pairs, subplot_list, ax_bbox_list, pad, h_pad, w_pad, rect)
80 for ax in subplots:
81 if ax.get_visible():
---> 82 bb += [martist._get_tightbbox_for_layout_only(ax, renderer)]
84 tight_bbox_raw = Bbox.union(bb)
85 tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
1396 """
1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
1399 when encountering third-party subclasses that do not support it.
1400 """
1401 try:
-> 1402 return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
1403 except TypeError:
1404 return obj.get_tightbbox(*args, **kwargs)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
4584 bbox_artists = self.get_default_bbox_extra_artists()
4586 for a in bbox_artists:
-> 4587 bbox = a.get_tightbbox(renderer)
4588 if (bbox is not None
4589 and 0 < bbox.width < np.inf
4590 and 0 < bbox.height < np.inf):
4591 bb.append(bbox)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:364, in Artist.get_tightbbox(self, renderer)
348 def get_tightbbox(self, renderer=None):
349 """
350 Like `.Artist.get_window_extent`, but includes any clipping.
351
(...) 362 Returns None if clipping results in no intersection.
363 """
--> 364 bbox = self.get_window_extent(renderer)
365 if self.get_clip_on():
366 clip_box = self.get_clip_box()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
964 raise RuntimeError(
965 "Cannot get window extent of text w/o renderer. You likely "
966 "want to call 'figure.draw_without_rendering()' first.")
968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969 bbox, info, descent = self._get_layout(self._renderer)
970 x, y = self.get_unitless_position()
971 x, y = self.get_transform().transform((x, y))
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:382, in Text._get_layout(self, renderer)
380 clean_line, ismath = self._preprocess_math(line)
381 if clean_line:
--> 382 w, h, d = _get_text_metrics_with_cache(
383 renderer, clean_line, self._fontproperties,
384 ismath=ismath, dpi=self.get_figure(root=True).dpi)
385 else:
386 w = h = d = 0
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py:215, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
--> 215 self.mathtext_parser.parse(s, self.dpi, prop)
216 return width, height, descent
218 font = self._prepare_font(prop)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:86, in MathTextParser.parse(self, s, dpi, prop, antialiased)
81 from matplotlib.backends import backend_agg
82 load_glyph_flags = {
83 "vector": LoadFlags.NO_HINTING,
84 "raster": backend_agg.get_hinting_flag(),
85 }[self._output_type]
---> 86 return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:100, in MathTextParser._parse_cached(self, s, dpi, prop, antialiased, load_glyph_flags)
97 if self._parser is None: # Cache the parser globally.
98 self.__class__._parser = _mathtext.Parser()
--> 100 box = self._parser.parse(s, fontset, fontsize, dpi)
101 output = _mathtext.ship(box)
102 if self._output_type == "vector":
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_mathtext.py:2167, in Parser.parse(self, s, fonts_object, fontsize, dpi)
2164 result = self._expression.parse_string(s)
2165 except ParseBaseException as err:
2166 # explain becomes a plain method on pyparsing 3 (err.explain(0)).
-> 2167 raise ValueError("\n" + ParseException.explain(err, 0)) from None
2168 self._state_stack = []
2169 self._in_subscript_or_superscript = False
ValueError:
$var au lieu de "$var"
^
ParseException: Expected end of text, found '$' (at char 0), (line:1, col:1)
Error in callback <function _draw_all_if_interactive at 0x7fc1127674c0> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
276 def _draw_all_if_interactive() -> None:
277 if matplotlib.is_interactive():
--> 278 draw_all()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
129 for manager in cls.get_all_fig_managers():
130 if force or manager.canvas.figure.stale:
--> 131 manager.canvas.draw_idle()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
1891 if not self._is_idle_drawing:
1892 with self._idle_draw_cntx():
-> 1893 self.draw(*args, **kwargs)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
379 # Acquire a lock on the shared font cache.
380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
381 else nullcontext()):
--> 382 self.figure.draw(self.renderer)
383 # A GUI class may be need to update a window using this draw, so
384 # don't forget to call the superclass.
385 super().draw()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
92 @wraps(draw)
93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94 result = draw(artist, renderer, *args, **kwargs)
95 if renderer._rasterizing:
96 renderer.stop_rasterizing()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/figure.py:3257, in Figure.draw(self, renderer)
3254 # ValueError can occur when resizing a window.
3256 self.patch.draw(renderer)
-> 3257 mimage._draw_list_compositing_images(
3258 renderer, self, artists, self.suppressComposite)
3260 renderer.close_group('figure')
3261 finally:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/axes/_base.py:3226, in _AxesBase.draw(self, renderer)
3223 if artists_rasterized:
3224 _draw_rasterized(self.get_figure(root=True), artists_rasterized, renderer)
-> 3226 mimage._draw_list_compositing_images(
3227 renderer, self, artists, self.get_figure(root=True).suppressComposite)
3229 renderer.close_group('axes')
3230 self.stale = False
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:752, in Text.draw(self, renderer)
749 renderer.open_group('text', self.get_gid())
751 with self._cm_set(text=self._get_wrapped_text()):
--> 752 bbox, info, descent = self._get_layout(renderer)
753 trans = self.get_transform()
755 # don't use self.get_position here, which refers to text
756 # position in Text:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:382, in Text._get_layout(self, renderer)
380 clean_line, ismath = self._preprocess_math(line)
381 if clean_line:
--> 382 w, h, d = _get_text_metrics_with_cache(
383 renderer, clean_line, self._fontproperties,
384 ismath=ismath, dpi=self.get_figure(root=True).dpi)
385 else:
386 w = h = d = 0
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py:215, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
--> 215 self.mathtext_parser.parse(s, self.dpi, prop)
216 return width, height, descent
218 font = self._prepare_font(prop)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:86, in MathTextParser.parse(self, s, dpi, prop, antialiased)
81 from matplotlib.backends import backend_agg
82 load_glyph_flags = {
83 "vector": LoadFlags.NO_HINTING,
84 "raster": backend_agg.get_hinting_flag(),
85 }[self._output_type]
---> 86 return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:100, in MathTextParser._parse_cached(self, s, dpi, prop, antialiased, load_glyph_flags)
97 if self._parser is None: # Cache the parser globally.
98 self.__class__._parser = _mathtext.Parser()
--> 100 box = self._parser.parse(s, fontset, fontsize, dpi)
101 output = _mathtext.ship(box)
102 if self._output_type == "vector":
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_mathtext.py:2167, in Parser.parse(self, s, fonts_object, fontsize, dpi)
2164 result = self._expression.parse_string(s)
2165 except ParseBaseException as err:
2166 # explain becomes a plain method on pyparsing 3 (err.explain(0)).
-> 2167 raise ValueError("\n" + ParseException.explain(err, 0)) from None
2168 self._state_stack = []
2169 self._in_subscript_or_superscript = False
ValueError:
$var au lieu de "$var"
^
ParseException: Expected end of text, found '$' (at char 0), (line:1, col:1)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
400 pass
401 else:
--> 402 return printer(obj)
403 # Finally look for special method names
404 method = get_real_method(obj, self.print_method)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
167 from matplotlib.backend_bases import FigureCanvasBase
168 FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
171 data = bytes_io.getvalue()
172 if fmt == 'svg':
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
2154 # we do this instead of `self.figure.draw_without_rendering`
2155 # so that we can inject the orientation
2156 with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157 self.figure.draw(renderer)
2158 if bbox_inches:
2159 if bbox_inches == "tight":
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
92 @wraps(draw)
93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94 result = draw(artist, renderer, *args, **kwargs)
95 if renderer._rasterizing:
96 renderer.stop_rasterizing()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/figure.py:3257, in Figure.draw(self, renderer)
3254 # ValueError can occur when resizing a window.
3256 self.patch.draw(renderer)
-> 3257 mimage._draw_list_compositing_images(
3258 renderer, self, artists, self.suppressComposite)
3260 renderer.close_group('figure')
3261 finally:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/axes/_base.py:3226, in _AxesBase.draw(self, renderer)
3223 if artists_rasterized:
3224 _draw_rasterized(self.get_figure(root=True), artists_rasterized, renderer)
-> 3226 mimage._draw_list_compositing_images(
3227 renderer, self, artists, self.get_figure(root=True).suppressComposite)
3229 renderer.close_group('axes')
3230 self.stale = False
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:752, in Text.draw(self, renderer)
749 renderer.open_group('text', self.get_gid())
751 with self._cm_set(text=self._get_wrapped_text()):
--> 752 bbox, info, descent = self._get_layout(renderer)
753 trans = self.get_transform()
755 # don't use self.get_position here, which refers to text
756 # position in Text:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:382, in Text._get_layout(self, renderer)
380 clean_line, ismath = self._preprocess_math(line)
381 if clean_line:
--> 382 w, h, d = _get_text_metrics_with_cache(
383 renderer, clean_line, self._fontproperties,
384 ismath=ismath, dpi=self.get_figure(root=True).dpi)
385 else:
386 w = h = d = 0
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py:215, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
--> 215 self.mathtext_parser.parse(s, self.dpi, prop)
216 return width, height, descent
218 font = self._prepare_font(prop)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:86, in MathTextParser.parse(self, s, dpi, prop, antialiased)
81 from matplotlib.backends import backend_agg
82 load_glyph_flags = {
83 "vector": LoadFlags.NO_HINTING,
84 "raster": backend_agg.get_hinting_flag(),
85 }[self._output_type]
---> 86 return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:100, in MathTextParser._parse_cached(self, s, dpi, prop, antialiased, load_glyph_flags)
97 if self._parser is None: # Cache the parser globally.
98 self.__class__._parser = _mathtext.Parser()
--> 100 box = self._parser.parse(s, fontset, fontsize, dpi)
101 output = _mathtext.ship(box)
102 if self._output_type == "vector":
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_mathtext.py:2167, in Parser.parse(self, s, fonts_object, fontsize, dpi)
2164 result = self._expression.parse_string(s)
2165 except ParseBaseException as err:
2166 # explain becomes a plain method on pyparsing 3 (err.explain(0)).
-> 2167 raise ValueError("\n" + ParseException.explain(err, 0)) from None
2168 self._state_stack = []
2169 self._in_subscript_or_superscript = False
ValueError:
$var au lieu de "$var"
^
ParseException: Expected end of text, found '$' (at char 0), (line:1, col:1)
<Figure size 1400x900 with 1 Axes>
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) oufind -print0avecread -d '', jamaislsparsé.[[ ]]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 < fichierest 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ésactiverset -xautour des sections sensibles, et d’utiliserumaskpour 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.