Objectifs d'apprentissage
À la fin de ce cours, l'apprenant peut :
- écrire un script shell qui s'arrête à la première erreur, protège ses variables par des guillemets et renvoie un code de sortie exact ;
- relire un script avec ShellCheck et comprendre un avertissement ;
- choisir Python plutôt que le shell dès que le script manipule des fichiers, des options ou des données structurées ;
- utiliser
pathlib,argparseetjsonde la bibliothèque standard ; - lancer un programme externe depuis Python avec
subprocess, sans passer par un shell ; - créer et utiliser un environnement virtuel, et dire pourquoi il n'est pas versionné ;
- écrire un test
unittestet lancer le lint et le formatage avec Ruff ; - refuser d'exécuter ou d'interpréter des données non fiables ;
- écrire un script idempotent avec une option
--a-blancqui décrit sans modifier.
Prérequis
- Savoir ouvrir Terminal, lire une commande avant de la valider et manipuler des redirections (cours « Terminal macOS »).
- Python 3 et, pour les contrôles, ShellCheck et Ruff : non vérifiés sur le poste de Stéphane. Ce cours n'enseigne pas leur installation et n'installe rien.
- Un dossier de test jetable. Aucun exercice ne demande de droits administrateur.
Parcours et chapitres
1. Shell, script et shebang sur macOS
D'après Apple, zsh est le shell par défaut des comptes créés à partir de macOS 10.15 ; bash l'était jusqu'à macOS 10.14, et zsh est présenté comme très compatible avec le shell Bourne (sh) et « presque » compatible avec bash, avec des différences. Un script indique son interpréteur sur sa première ligne (shebang, voir le cours « Terminal macOS ») :
| Première ligne | Sens |
|---|---|
#!/bin/sh | shell Bourne ; écrire alors du sh portable |
#!/usr/bin/env bash | bash trouvé dans le PATH (usage courant, non vérifié dans les sources Apple) |
#!/usr/bin/env python3 | Python 3 trouvé dans le PATH (idem) |
À retenir : le shell interactif de Stéphane (probablement zsh) n'est pas forcément celui qui exécute le script : c'est le shebang qui décide. Un script se teste avec l'interpréteur qu'il déclare. La version de bash livrée avec macOS n'a pas été relevée dans les sources consultées : non vérifié.
Règle de choix : shell pour enchaîner quelques commandes ; Python dès qu'il faut des options, des boucles non triviales, du JSON, des fichiers ou des tests.
2. Un script shell sûr
Squelette recommandé, à adapter :
#!/usr/bin/env bash
set -euo pipefail
dossier="${1:?usage : $0 <dossier>}"
for fichier in "$dossier"/*.txt; do
[ -e "$fichier" ] || continue
printf '%s\n' "$fichier"
done- Contexte : Terminal, script enregistré sous
lister-txt.shdans un dossier de test ; bash ; version non vérifiée. - Droits : utilisateur normal ; lecture du dossier passé en argument.
- Effet : liste, une par ligne, les fichiers
.txtdu dossier. Aucune modification de fichier. - Vérification : sans argument, le script s'arrête avec un message d'usage et un code non nul (
echo $?) ; avec un dossier de test contenant deux fichiers, il en affiche deux. - Retour arrière : sans objet (lecture seule) ; supprimer le script de test.
Chaque ligne a un rôle :
set -e: arrêt à la première commande qui échoue. Il a des exceptions (commandes dans un testif, une liste&&/||, etc.) : lire la page « The Set Builtin » du manuel Bash avant de s'y fier (page non relue ici, voir « Points non vérifiés »).set -u: une variable non définie est une erreur au lieu d'une chaîne vide silencieuse.set -o pipefail: d'après le manuel Bash, le statut d'un tube devient celui de la dernière commande (la plus à droite) qui a échoué, ou zéro si toutes ont réussi ; sans cette option, seule la dernière commande compte. Option de bash, pas deshstrict : non vérifié pour leshde macOS."${1:?message}": forme usuelle (non vérifiée dans les sources consultées) qui arrête avec le message si l'argument manque.
À retenir : set -euo pipefail n'est pas une garantie, c'est un filet. Il ne remplace ni la vérification des entrées ni la lecture du code de retour.
Guillemets
Le wiki ShellCheck (SC2086) explique qu'une variable sans guillemets subit un découpage en mots (selon IFS) puis une expansion de motifs (globbing) : echo $1 n'est donc pas echo "$1". Quoter protège des espaces, des sauts de ligne et des caractères spéciaux. Règle simple : toujours écrire "$variable", "$@" pour tous les arguments, et "${tableau[@]}" pour construire une commande à plusieurs options (le wiki donne l'exemple make "${options[@]}" file).
fichier="mon rapport.txt"printf '%s\n' "$fichier"Bloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : bash ou zsh dans un dossier de test.
- Droits : utilisateur normal.
- Effet : affiche
mon rapport.txtcomme un seul argument ; sans guillemets,printf '%s\n' $fichieren verrait deux.printf: comportement usuel, non vérifié dans les sources consultées. - Vérification : une seule ligne s'affiche avec les guillemets, deux sans.
- Retour arrière : sans objet.
Codes de sortie
Convention usuelle de bash : 0 signifie succès, toute autre valeur un échec ; $? donne le code de la dernière commande. Les valeurs précises à connaître (126, 127, 128 + numéro de signal) sont non vérifiées ici, la page « Exit Status » du manuel GNU n'ayant pas pu être lue (erreur 429). Un script bien élevé :
- renvoie
0seulement si tout s'est bien passé,exit 1(ou une autre valeur non nulle) sinon ; - écrit ses messages d'erreur sur
stderr(echo "erreur" >&2) et son résultat surstdout, pour rester utilisable en tube ; - ne masque pas un échec par
|| truesans raison écrite en commentaire.
Nettoyer avec trap
Le builtin trap exécute une commande à la sortie du script. Exemple d'usage courant (non vérifié dans la page du manuel, non lue) pour nettoyer un fichier temporaire créé avec mktemp (outil BSD ; options non vérifiées) :
tmp="$(mktemp)"trap 'rm -f "$tmp"' EXITBloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : dans un script bash de test.
- Droits : utilisateur normal ; création dans le dossier temporaire du système.
- Effet : crée un fichier temporaire vide, puis programme sa suppression à la fin du script, même en cas d'erreur.
- Vérification : le fichier n'existe plus après la fin du script.
- Retour arrière : le
traps'en charge ; sinon supprimer le fichier en relisant son nom.
3. ShellCheck
ShellCheck est, d'après son dépôt, un outil d'analyse statique GPLv3 qui signale erreurs de syntaxe, problèmes sémantiques et pièges des scripts bash et sh ; il cible sh, bash, dash et ksh. Il lit le shebang (ou une directive) pour connaître le dialecte ; zsh n'est pas listé parmi les shells pris en charge dans la page consultée : ne pas lui soumettre un script zsh. Il s'utilise ainsi :
shellcheck lister-txt.shecho $?Bloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : Terminal, ShellCheck installé (non vérifié sur le poste de Stéphane).
- Droits : utilisateur normal ; lecture seule.
- Effet : affiche des avertissements numérotés (
SC2086…) ; le code de sortie est non nul s'il en trouve (« usage canonique des codes de sortie », d'après le dépôt). - Vérification : un script sans avertissement n'affiche rien et renvoie
0. - Retour arrière : sans objet.
Chaque numéro SCxxxx a une page dans le wiki (https://www.shellcheck.net/wiki/SCxxxx) avec le code fautif, le code corrigé et la justification. Désactiver un avertissement se fait au cas par cas, avec un commentaire qui dit pourquoi.
Astuce : exécuter ShellCheck avant le premier lancement d'un script, pas après un incident.
4. Python d'abord, bibliothèque standard d'abord
Règle de l'Orchestrator : Python stdlib d'abord ; toute dépendance ou service payant demande l'accord de Stéphane. Trois modules couvrent l'essentiel d'un script.
pathlib : les chemins comme des objets
D'après la documentation Python, pathlib.Path représente un chemin ; l'opérateur / assemble ; read_text() et write_text() lisent et écrivent ; mkdir(parents=True, exist_ok=True) crée une arborescence sans erreur si elle existe déjà ; unlink(missing_ok=True) supprime en ignorant l'absence ; glob, iterdir, resolve et exists parcourent et interrogent.
from pathlib import Path
dossier = Path("sortie-demo")
dossier.mkdir(parents=True, exist_ok=True)
(dossier / "salut.txt").write_text("bonjour\n", encoding="utf-8")
print(sorted(p.name for p in dossier.glob("*.txt")))- Contexte : Python 3 dans un dossier de test (version de Stéphane : non vérifiée) ;
encoding=: paramètre usuel, non relu dans la page consultée. - Droits : utilisateur normal ; écriture dans le dossier courant.
- Effet : crée
sortie-demo/s'il manque, écrasesalut.txt, puis affiche['salut.txt']. - Vérification : la sortie est
['salut.txt']. - Retour arrière : supprimer le dossier de test en relisant son nom.
Attention : write_text remplace le contenu d'un fichier existant sans avertissement. Pour ne rien écraser par accident, tester exists() avant ou utiliser l'essai à blanc (chapitre 9).
argparse : des options claires
argparse.ArgumentParser définit les arguments, génère l'aide (-h) et vérifie les entrées ; add_argument accepte type, choices, default, help ; action="store_true" crée un indicateur booléen. D'après la documentation, une erreur d'arguments affiche un message sur stderr et sort avec le code 2.
json : échanger des données structurées
json.load et json.dump lisent et écrivent un fichier ; loads et dumps travaillent sur des chaînes. indent met en forme, sort_keys ordonne les clés, ensure_ascii=False garde les accents. Une erreur de syntaxe lève json.JSONDecodeError (sous-classe de ValueError).
Attention : la documentation avertit qu'un JSON venant d'une source non fiable peut consommer beaucoup de processeur et de mémoire : limiter la taille lue.
5. subprocess : lancer un programme sans shell
D'après la documentation de subprocess, la bibliothèque n'appelle pas implicitement un shell : tous les caractères, métacaractères compris, peuvent être passés en toute sécurité à un processus enfant, à condition de donner les arguments sous forme de liste. Avec shell=True, c'est à l'application de protéger chaque espace et métacaractère pour éviter l'injection (la page cite shlex.quote comme aide).
import subprocess
resultat = subprocess.run(
["git", "status", "--short"],
capture_output=True,
text=True,
check=True,
timeout=30,
)
print(resultat.stdout)- Contexte : Python 3 dans un dépôt Git de test.
- Droits : utilisateur normal ; lecture seule du dépôt.
- Effet : lance
git status --shortsans shell, capture la sortie en texte, lèveCalledProcessErrorsi le code de sortie n'est pas nul (check=True) etTimeoutExpiredau bout de 30 secondes. Selon la documentation, un processus dépassant le délai n'est pas tué automatiquement dans tous les cas : prévoir le nettoyage. - Vérification : la sortie courte de
git statuss'affiche ; dans un dossier qui n'est pas un dépôt, l'exception est levée. - Retour arrière : sans objet (lecture seule).
À retenir : liste d'arguments, check=True, timeout=, jamais shell=True avec une donnée qui ne vient pas de vous. Ne construire une commande qu'avec des morceaux validés (liste blanche) ; ne jamais y coller une chaîne d'origine externe.
6. Environnements virtuels
python -m venv <dossier> crée un environnement isolé : fichier pyvenv.cfg, sous-dossier bin avec l'interpréteur et lib/pythonX.Y/site-packages pour les paquets (d'après la documentation de venv). Sur POSIX, on l'active avec source <dossier>/bin/activate (bash/zsh) et on en sort avec deactivate.
python3 -m venv .venvsource .venv/bin/activatepython --versiondeactivateBloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : Terminal, dossier de projet de test, Python 3 installé (version non vérifiée).
- Droits : utilisateur normal ; écriture dans le dossier courant.
- Effet : crée
.venv/(dossier de plusieurs dizaines de Mo possible : taille non vérifiée), modifie lePATHdu terminal courant jusqu'àdeactivate. Aucun paquet n'est installé par ces commandes. - Vérification :
python --versionrépond avec l'interpréteur de l'environnement ;which pythonpointe dans.venv(usage courant, non relu). - Retour arrière :
deactivate, puis supprimer le dossier.venven relisant son nom.
À retenir : la documentation précise qu'un environnement virtuel est jetable (on le supprime et on le recrée), non déplaçable et à ne pas versionner. Depuis Python 3.13, venv crée par défaut un .gitignore dans l'environnement ; ajouter tout de même .venv/ au .gitignore du dépôt si la version de Stéphane est plus ancienne.
Dans l'Orchestrator, la règle « stdlib d'abord » rend l'environnement virtuel utile surtout pour les outils de développement (Ruff) ; installer un paquet reste une décision à valider. Les commandes d'installation (pip) ne sont pas couvertes ici : page pip non consultée.
7. Tester avec unittest
unittest est dans la bibliothèque standard. Un test est une méthode test_… d'une sous-classe de unittest.TestCase ; les assertions courantes sont assertEqual, assertTrue, assertIn et assertRaises ; setUp et tearDown préparent et nettoient ; subTest distingue des cas dans une boucle. La découverte automatique trouve les fichiers test*.py :
import unittest
from archiver import nom_archive
class TestNomArchive(unittest.TestCase):
def test_nom_simple(self):
self.assertEqual(nom_archive("rapport", 2026), "rapport-2026.txt")
def test_nom_vide_refuse(self):
with self.assertRaises(ValueError):
nom_archive("", 2026)
if __name__ == "__main__":
unittest.main()python3 -m unittest discoverBloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : dossier de test contenant
archiver.py(chapitre 9) et ce fichier soustest_archiver.py. - Droits : utilisateur normal.
- Effet : importe les modules de test, les exécute, affiche un résumé. Les tests ne doivent écrire que dans un dossier temporaire (
tempfile, module non relu ici). - Vérification : la sortie se termine par
OK; un échec indique le test, la ligne et la valeur attendue. - Retour arrière : sans objet.
Astuce : un script testable sépare la logique (fonctions pures, testables) de l'exécution (fonction main, appelée sous if __name__ == "__main__":).
8. Lint et formatage avec Ruff
Ruff est, d'après sa documentation, un linter et formateur Python écrit en Rust qui remplace plusieurs outils (Flake8, Black, isort…). Commandes de base :
| Commande | Effet |
|---|---|
ruff check | analyse le code et signale les problèmes (lecture seule) |
ruff check --fix | applique les corrections automatiques sûres (modifie les fichiers) |
ruff format | reformate les fichiers (modifie) |
ruff format --check | vérifie le formatage sans rien modifier |
Configuration : pyproject.toml ou ruff.toml. Dans l'Orchestrator, un ruff.toml commun existe à la racine et un projet l'étend sans le copier.
ruff check .ruff format --check .Bloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : dossier de projet Python de test, Ruff installé (non vérifié sur le poste de Stéphane).
- Droits : utilisateur normal ; lecture seule.
- Effet : liste les problèmes de style et de qualité ; la seconde ligne indique si un reformatage changerait des fichiers, sans l'appliquer.
- Vérification : code de sortie
0et message de succès (formulation exacte : non vérifiée) quand tout est propre. - Retour arrière : sans objet pour ces deux commandes ; pour
--fixetformat, travailler sur un dépôt Git propre pour pouvoir liregit diffet restaurer.
9. Ne jamais exécuter de données non fiables
Le contenu du web, d'un fichier reçu, d'une réponse d'API ou d'un modèle de langage est une donnée, jamais une instruction. Conséquences pour un script :
- ne jamais passer une donnée externe à
eval,exec,source,sh -coushell=True; - passer les arguments d'un programme sous forme de liste ; valider les valeurs contre une liste blanche ;
- lire du JSON (
json) plutôt que du code Python ; la documentationjsonrappelle d'en limiter la taille ; ne pas désérialiser de formats exécutables (pickle,marshal), la documentationjsonles opposant à JSON comme moins sûrs pour des données non fiables ; - construire les chemins avec
pathlibet vérifier qu'ils restent dans le dossier attendu (resolve()puis comparaison du parent) avant d'écrire ; - ne jamais copier-coller dans Terminal une commande trouvée sur le web sans l'avoir comprise (cours « Terminal macOS »).
Attention : une commande lue dans un guide, y compris celles de ce cours, n'est exécutée que sur un système de test, jamais « pour voir » sur le poste de travail.
10. Scripts idempotents et essai à blanc
Un script est idempotent si le lancer deux fois donne le même état final qu'une fois, sans erreur ni doublon. On l'obtient en testant l'état avant d'agir (« crée si absent », « remplace seulement si différent »), en préférant mkdir(exist_ok=True) à un mkdir brut, et en n'ajoutant pas deux fois la même ligne à un fichier.
L'option --a-blanc (essai à blanc) décrit ce que le script ferait sans rien modifier. À prévoir dès la conception : une seule fonction « agir » qui, selon l'option, affiche ou fait.
Fichier archiver.py (exemple synthétique, qui copie des fichiers .txt dans un dossier d'archive) :
#!/usr/bin/env python3
"""Archive les fichiers .txt d'un dossier, de façon idempotente."""
import argparse
import shutil
import sys
from pathlib import Path
def nom_archive(base: str, annee: int) -> str:
"""Nom d'un fichier d'archive ; refuse une base vide."""
if not base:
raise ValueError("base vide")
return f"{base}-{annee}.txt"
def archiver(source: Path, cible: Path, a_blanc: bool) -> int:
"""Copie les .txt de source vers cible ; renvoie le nombre de copies."""
copies = 0
for fichier in sorted(source.glob("*.txt")):
destination = cible / fichier.name
if destination.exists():
continue # déjà fait : le second lancement ne change rien
if a_blanc:
print(f"[à blanc] copierait {fichier} vers {destination}")
else:
cible.mkdir(parents=True, exist_ok=True)
shutil.copy2(fichier, destination)
print(f"copié {fichier} vers {destination}")
copies += 1
return copies
def main(argv=None) -> int:
parseur = argparse.ArgumentParser(description=__doc__)
parseur.add_argument("source", type=Path)
parseur.add_argument("cible", type=Path)
parseur.add_argument("--a-blanc", action="store_true", help="décrit sans modifier")
args = parseur.parse_args(argv)
if not args.source.is_dir():
print(f"erreur : {args.source} n'est pas un dossier", file=sys.stderr)
return 1
n = archiver(args.source, args.cible, args.a_blanc)
print(f"{n} fichier(s) concerné(s)")
return 0
if __name__ == "__main__":
sys.exit(main())python3 archiver.py donnees-test archive-test --a-blancpython3 archiver.py donnees-test archive-testpython3 archiver.py donnees-test archive-testBloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : dossier de test contenant
archiver.pyet un sous-dossierdonnees-testavec deux fichiers.txtsynthétiques ; Python 3 (version non vérifiée).shutil.copy2n'a pas été relu dans la documentation : non vérifié. - Droits : utilisateur normal ; lecture de
donnees-test, écriture dearchive-test(hors premier appel). - Effet : le premier appel liste seulement les copies prévues et ne crée rien ; le deuxième copie les fichiers ; le troisième ne copie plus rien (« 0 fichier(s) concerné(s) ») : c'est l'idempotence. Un fichier déjà présent dans
archive-testn'est jamais écrasé. - Vérification : après le premier appel,
archive-testn'existe pas ; après le deuxième, il contient deux fichiers ; le code de sortie (echo $?) vaut0, et1si la source n'est pas un dossier. - Retour arrière : supprimer
archive-test(dossier de test) en relisant son nom.
À retenir : l'essai à blanc est un filet de lecture, pas une preuve : il faut aussi un test (chapitre 7) sur un dossier temporaire et la relecture de la sortie. Le même principe vaut pour un script shell : une variable A_BLANC qui préfixe les commandes à effet par echo.
11. Le cycle d'un script sûr
Un script de l'Orchestrator expose une seule commande de bout en bout, nommée dans le CLAUDE.md de son projet ; il ne contient aucun secret en clair (Trousseau macOS, bin/secret), n'écrit jamais un mot de passe dans l'historique, et ne contourne jamais un captcha ni une protection anti-robot.
Exercices et vérifications de fin de cours
Tous dans un dossier de test jetable.
- Shell sûr. Écrire
lister-txt.sh(chapitre 2), le lancer sans argument, avec un dossier vide, avec un dossier de deux fichiers ; relever chaque code de sortie. - Guillemets. Créer un fichier dont le nom contient une espace ; montrer ce qui casse sans guillemets, puis corriger.
- ShellCheck. Soumettre un script contenant
echo $1à ShellCheck ; lire la page SC2086 du wiki et corriger. - pathlib. Réécrire en Python la boucle du script shell avec
Path.glob. - argparse. Ajouter
--verbeuxà un script, lire-h, provoquer une erreur d'argument et relever le code 2. - subprocess. Appeler
git status --shortavec une liste d'arguments ; provoquer une erreur et capterCalledProcessError. - Environnement virtuel. Créer, activer et supprimer
.venv; l'ajouter au.gitignore. - Tests. Écrire deux tests pour
nom_archive, dont un qui attendValueError, et les lancer avecdiscover. - Ruff. Lancer
ruff check .etruff format --check .surarchiver.py. - Idempotence. Lancer
archiver.pyà blanc, puis deux fois pour de bon ; expliquer chaque sortie. - Données non fiables. Citer trois constructions interdites avec une donnée externe et leur remplaçant.
Critères de réussite
- Expliquer
-e,-uetpipefail, et reconnaître qu'ils ne suffisent pas. - Écrire
"$variable"sans oubli et dire pourquoi. - Lire un code de sortie et en produire un cohérent.
- Lancer un programme depuis Python sans
shell=True. - Dire pourquoi un environnement virtuel n'est pas versionné.
- Exécuter
unittest,ruff checketruff format --check. - Prouver l'idempotence d'un script par deux lancements successifs.
- Citer le périmètre des sources (documentation en ligne, Python 3.15.0 affichée le 17 octobre 2026) sans la confondre avec la version installée.
Points non vérifiés ou à confirmer
- Versions de macOS, de zsh, de bash, de Python, de ShellCheck et de Ruff sur le Mac de Stéphane ; version de bash livrée avec macOS.
- Numéro de version courant de Ruff et de ShellCheck : non affiché dans les pages lues.
- Pages du manuel GNU Bash « The Set Builtin », « Quoting », « Exit Status » et « Bourne Shell Builtins » : réponse HTTP 429 à la consultation ; la définition de
pipefaila été confirmée par un résultat de recherche citant la page, le reste (exceptions de-e, codes 126, 127 et 128+n, syntaxe detrap) est non vérifié et à relire sur la page officielle. set -euo pipefail,trap … EXIT,${1:?…},mktemp,printf,shutil.copy2,tempfile,which,pip: usages courants non relus dans une source officielle consultée.- Support de zsh par ShellCheck (non listé dans la page lue) ; comportement de
set -een zsh. - Indication Apple sur le shebang et les scripts : voir le cours « Terminal macOS » ; le « Shell Scripting Primer » d'Apple n'a pas été retrouvé dans la recherche effectuée (non vérifié).
- Pages de documentation lues par un outil de synthèse automatique : citations en traduction libre, à relire sur les pages.
Sources officielles
Toutes consultées le 17 octobre 2026. Python : documentation affichant 3.15.0 comme version stable. Ruff et ShellCheck : numéro de version non affiché (état de la page à la date de consultation).
| Source | Usage |
|---|---|
| Use zsh as the default shell on your Mac (Apple) (s’ouvre dans un nouvel onglet) | chapitre 1 |
| Bash Reference Manual — The Set Builtin (s’ouvre dans un nouvel onglet) | chapitre 2 (pipefail, via résultat de recherche ; page non relue, 429) |
| ShellCheck — wiki SC2086 (s’ouvre dans un nouvel onglet) | chapitre 2, guillemets |
| ShellCheck — dépôt (s’ouvre dans un nouvel onglet) | chapitre 3 |
| Python — pathlib (s’ouvre dans un nouvel onglet) | chapitre 4 |
| Python — argparse (s’ouvre dans un nouvel onglet) | chapitre 4 |
| Python — json (s’ouvre dans un nouvel onglet) | chapitres 4 et 9 |
| Python — subprocess (s’ouvre dans un nouvel onglet) | chapitre 5 |
| Python — venv (s’ouvre dans un nouvel onglet) | chapitre 6 |
| Python — unittest (s’ouvre dans un nouvel onglet) | chapitre 7 |
| Ruff — documentation (s’ouvre dans un nouvel onglet) | chapitre 8 |