Convertir plusieurs fichiers Markdown d’un coup, sans mentir à votre build
12 août 2026 · 42 min de lecture
Convertir un fichier Markdown est un problème résolu. Déposez-le sur une page, ou tapez une commande, et vous avez du HTML. En convertir cent quarante, répartis dans onze répertoires dont quatre que personne n’a ouverts depuis la migration, est un autre travail — et ce n’est pas le même travail fait cent quarante fois.
Le convertisseur n’est presque jamais la pièce qui casse. Ce qui casse, c’est le glob qui a discrètement sauté un sous-répertoire, les deux fichiers index.md devenus un seul index.html, l’exécution parallèle dont le journal est quatre fichiers entrelacés, le build qui a annoncé un succès parce que seule la dernière conversion avait été vérifiée, et la tâche de dix-sept minutes qui a reconverti chaque fichier pour changer un paragraphe.
Chacune de ces défaillances est silencieuse. Un tableau Markdown qui n’a pas pu être analysé produit quand même du HTML. Un fichier que le glob n’a jamais atteint ne produit rien du tout, et rien ressemble exactement à rien d’anormal.
En bref
Laissez le shell trouver les fichiers et laissez un petit script en convertir un seul, parce qu’une boucle que vous pouvez tester sur un chemin unique est une boucle que vous pouvez déboguer. Utilisez find … -print0 | sort -z plutôt qu’un simple glob *.md : un glob ne descend pas dans l’arborescence tant que vous n’avez pas activé globstar, il ignore les répertoires commençant par un point, il passe le motif littéral à votre convertisseur quand rien ne correspond, et il atteindra la limite de longueur des arguments avant qu’un grand dépôt ne le fasse. Construisez le chemin de sortie par expansion de paramètre pour que l’arborescence garde sa forme — basename écrase docs/api/index.md et docs/guide/index.md sur le même fichier, et le build se termine quand même par zéro. Décidez ensuite délibérément de deux choses : si un échec sur le quarantième fichier arrête l’exécution ou s’il est collecté puis signalé à la fin, et si vous voulez réellement beaucoup de pages ou un seul document, parce que fusionner est un autre travail dont la réponse est différente.
Convertir un dossier est un autre travail
Une conversion unique a une entrée, une sortie et un résultat. Une conversion de dossier comporte cinq décisions qui n’existent pas pour un seul fichier, et la réponse par défaut à chacune est fausse assez souvent pour que cela compte.
Quels fichiers. « Tout le Markdown du dépôt » a l’air sans ambiguïté jusqu’au moment où vous l’écrivez. Est-ce que cela inclut node_modules ? Le répertoire .github ? Le CHANGELOG.md à la racine ? Une copie vendorisée de la documentation de quelqu’un d’autre ? Le répertoire en lien symbolique qui pointe vers une copie de travail voisine ? Chacun de ces cas est une vraie réponse à une vraie question, et votre glob va y répondre à votre place, sans le dire.
Dans quel ordre. L’ordre des fichiers n’a aucune importance quand chaque fichier devient sa propre page. Il en a toute une quand les fichiers deviennent un seul document, et il compte pour la reproductibilité dans les deux cas : un build dont le journal liste les fichiers dans un ordre différent à chaque exécution est un build que vous ne pouvez pas comparer.
Où va la sortie. À côté de l’entrée, ou dans une arborescence séparée. C’est la décision que les gens regrettent, parce que les deux fonctionnent à la première exécution et qu’une seule survit à une suppression, à un renommage ou à un lien relatif.
Ce qui se passe quand un fichier échoue. Deux cents fichiers, l’un d’eux mal formé. Arrêter, ou continuer et signaler ? Les deux se défendent. Le comportement par défaut — continuer et sortir avec zéro — ne se défend pas.
À quelle fréquence. Un dossier qui change une fois par semaine n’a pas besoin d’être reconverti chaque nuit, et un dossier converti dans une pull request ne devrait probablement convertir que ce que la branche a touché.
Remarquez que rien de tout cela ne concerne le convertisseur. Lequel vous utilisez est une question séparée, avec son propre comparatif, et chacune des approches ci-dessous fonctionne avec n’importe lequel d’entre eux, du moment que la commande prend un chemin d’entrée et un chemin de sortie et dit la vérité sur son code de sortie.
Comparatif rapide : l’aide-mémoire
| Approche | Idéal pour | Tourne en parallèle | Peut faire échouer le build | Saute les fichiers inchangés |
|---|---|---|---|---|
Boucle for sur un glob |
Un script court que quelqu’un modifiera l’an prochain | Non | Oui, avec set -e, dès le premier fichier fautif |
Non |
find … -exec … + |
Une arborescence de profondeur inconnue aux noms pénibles | Non | Pas de façon fiable — le code de sortie n’est pas celui de la commande | Non |
find -print0 | xargs -0 -P |
Des centaines de fichiers, et le temps d’horloge | Oui | Oui — code 123 si un fichier a échoué | Non |
| GNU parallel | Du travail parallèle dont la sortie doit rester ordonnée | Oui | Oui, avec --halt now,fail=1 |
Non |
make avec une règle de motif |
Un dossier dont la plupart des fichiers n’ont pas changé | Oui, avec make -j |
Oui, s’arrête à la première recette en échec | Oui, à la date de modification |
| Un script Node ou Python | Une sortie que vous maîtrisez, habillage compris | Oui, avec une limite de concurrence | Seulement si vous fixez le code de sortie vous-même | Seulement si vous l’implémentez |
| Un appel API ou CLI par fichier | Un runner sans chaîne d’outils et sans installation | Oui, jusqu’à la limite de débit | Oui, par appel | Non |
| Un générateur de site statique | Navigation, recherche et liens entre documents | En interne | Oui | En général, via son propre cache |
Tous sont gratuits. Les outils du shell sont déjà sur la machine ; le reste porte les licences indiquées dans chaque section ci-dessous.
Toutes les manières de lancer une conversion sur un dossier
Une boucle for sur un glob — idéal pour un script que quelqu’un relira
La chose la plus courte qui fonctionne, et la version à écrire en premier, parce que vous pouvez la lire à voix haute.
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob globstar
for file in docs/**/*.md; do
output="build/${file#docs/}"
output="${output%.md}.html"
mkdir -p "$(dirname "$output")"
bin/one.sh "$file" "$output"
done
| Avantages | Inconvénients |
|---|---|
| Lisible, et il est évident de voir ce qu’elle va faire | Séquentielle : le temps d’horloge est la somme de tous les fichiers |
set -e fait du premier échec la dernière chose qui arrive |
globstar est une option bash, donc sh script.sh change le comportement |
| Aucune dépendance au-delà du shell | Ignore les répertoires commençant par un point sauf si vous activez aussi dotglob |
| Le quoting est sous votre contrôle, en un seul endroit | Un glob assez gros pour dépasser la limite d’arguments échoue ici aussi |
Prix : gratuit ; bash est sous licence GPL et déjà installé.
Détails techniques
shopt -s globstarfait que**traverse les séparateurs de répertoire ; sans cela,**se comporte exactement comme*et vos sous-répertoires sont ignorés en silenceshopt -s nullglobfait qu’une correspondance vide s’étend à rien, au lieu de passer la chaîne littéraledocs/**/*.mdau convertisseur comme nom de fichier${file#docs/}retire la racine source ;${output%.md}.htmlremplace l’extension sans toucher aux noms de répertoires- Lancez le script avec
bash script.sh, jamaissh script.sh—shoptn’est pas portable, et unshfourni par dash le refusera
Pour qui ? Pour les dépôts comptant quelques dizaines de fichiers, et pour quiconque a d’abord besoin que la personne suivante puisse modifier le script sans lire une page de manuel.
find … -exec … + — idéal pour une arborescence de profondeur inconnue
find n’a besoin d’aucune option de shell pour descendre, se comporte pareil dans tous les shells, et se moque de ce que contiennent les noms de fichiers.
find docs -type f -name '*.md' -exec bin/one.sh {} +
| Avantages | Inconvénients |
|---|---|
| Récursion, filtrage et élagage dans une seule expression | La question du code de sortie est réellement trouble |
| Passe les noms en arguments : espaces et guillemets survivent | -printf et d’autres primaires utiles sont propres à GNU |
+ groupe les arguments, donc pas de dépassement de la limite de longueur |
Ordre du répertoire, pas ordre trié |
-prune exclut tout un sous-arbre à peu de frais |
C’est au script de dériver lui-même le chemin de sortie |
Prix : gratuit ; GNU findutils est sous licence GPL, et un find BSD est livré avec macOS.
Détails techniques
-type fexclut les répertoires qui se trouvent finir par.md, ce qui est plus rare qu’un lien symbolique vers l’un d’eux mais pas assez rare pour être ignoré-exec cmd {} +passe autant de chemins par invocation qu’il en tient ;-exec cmd {} \;lance un processus par fichier, ce qui est plus lent et plus simple à raisonner- Avec
-exec … \;, une commande en échec ne change pas du tout le code de sortie defindlui-même : une tâche construite ainsi ne peut pas signaler un échec de conversion. GNUfinddocumente un code non nul quand une commande lancée avec+échoue, et les implémentations diffèrent — c’est la raison de déplacer la question du code de sortie versxargs, où elle est écrite noir sur blanc find docs -name node_modules -prune -o -type f -name '*.md' -printest l’idiome pour exclure un sous-arbre ;-not -path '*/node_modules/*'donne le même résultat mais parcourt quand même l’ensemblefindne suit pas les liens symboliques sauf si vous passez-L, et passer-Lsur une arborescence contenant un lien vers son propre parent bouclera jusqu’à la limite de profondeur
Pour qui ? Pour toute arborescence plus profonde qu’un niveau, et tout dépôt dont vous ne contrôlez pas personnellement les noms de fichiers.
find -print0 | xargs -0 -P — idéal quand le compte se chiffre en centaines
La manière standard de faire tenir une conversion de dossier dans une fraction du temps, et le moment où vous cessez de pouvoir lire le journal de haut en bas.
find docs -type f -name '*.md' -print0 \
| sort -z \
| xargs -0 -P 8 -n 1 bin/one.sh
| Avantages | Inconvénients |
|---|---|
| Du vrai parallélisme avec une seule option | La sortie des tâches concurrentes s’entrelace, ligne par ligne |
| Un code de sortie agrégé documenté : 123 si un fichier a échoué | xargs exécute directement : ni redirection ni globbing dans la commande |
| Séparation par NUL : tout nom de fichier légal survit | Les garanties d’ordre disparaissent sauf à trier d’abord et afficher ensuite |
-n règle la taille des lots, ce qui compte pour les petits fichiers |
La liste des échecs doit être collectée hors bande |
Prix : gratuit, sous licence GPL, partie de findutils.
Détails techniques
-print0et-0utilisent NUL comme séparateur, le seul octet qu’un nom de fichier ne peut pas contenir — un saut de ligne dans un nom de fichier est légal et couperait sinon un chemin en deuxsort -ztrie des enregistrements séparés par NUL ; l’ordre propre defindest l’ordre du répertoire, qui n’est pas trié et n’est pas stable d’une machine à l’autre. AjoutezLC_ALL=Csi vous voulez le même ordre sur un runner que sur votre portablexargssort avec 123 si une invocation est sortie entre 1 et 125, 124 si l’une est sortie avec 125, 125 si l’une a été tuée par un signal, 126 si la commande n’a pas pu être lancée et 127 si elle est introuvable. Ces cinq codes constituent tout le protocole de remontée d’erreurs : faites donc sortir votre script avec un code non nul et laissez l’agrégat parler-P 0lance autant de processus qu’il le peut ;-P "$(nproc)"est le choix habituel sous Linux, et macOS veutsysctl -n hw.ncpuà la placexargsne lance pas de shell.xargs -0 cmd > out.htmlredirige toute l’exécution dans un seul fichier, pas un fichier par entrée ; s’il vous faut une redirection, mettez-la dans le script-n 1démarre un processus par fichier. Pour un millier de petits documents, le démarrage du processus domine la conversion elle-même, et-n 20avec un script qui boucle sur"$@"est nettement meilleur — mesurez sur votre propre arborescence plutôt que de vous fier à un ratio
Pour qui ? Pour les ensembles de documentation qui se comptent en centaines, et tout build où la conversion est devenue l’étape lente.
GNU parallel — idéal quand une sortie parallèle doit rester ordonnée
parallel, c’est xargs avec l’ergonomie complétée : sortie ordonnée, politique d’échec, exécution à blanc et affichage de la progression.
find docs -type f -name '*.md' -print0 \
| parallel -0 -k --halt now,fail=1 bin/one.sh {}
| Avantages | Inconvénients |
|---|---|
-k met chaque tâche en tampon et affiche dans l’ordre d’entrée |
Une installation de plus, et elle n’est pas là par défaut |
--halt now,fail=1 arrête l’exécution au premier échec |
Sa syntaxe de quoting et de substitution est un langage à part entière |
--dry-run affiche les commandes sans les exécuter |
La mise en tampon qui préserve l’ordre coûte de la mémoire et du disque |
--joblog enregistre le statut et la durée de chaque tâche |
Démesuré quand rien ne lit la sortie standard |
Prix : gratuit, sous licence GPL.
Détails techniques
-k(--keep-order) est l’option qui le sépare dexargs: les tâches tournent toujours en parallèle, la sortie reste lisible--haltprend une politique — arrêter tout de suite ou quand les tâches en cours se terminent, sur un nombre ou un pourcentage d’échecs--joblog FICHIERest la réponse honnête à « quel fichier a échoué ? » : un tableau donnant le code de sortie de chaque tâche, que vous pouvez filtrer après coup au lieu de lire le journal{.}retire l’extension de la chaîne de substitution,{//}donne le répertoire — utile, et un dialecte de plus à retenir- Il affiche une demande de citation dans les travaux universitaires, ce qui n’est pas une restriction de licence mais surprend bel et bien la première fois que cela apparaît dans un journal de build
Pour qui ? Pour les builds où la conversion affiche quelque chose qu’un humain lit, et pour quiconque veut un tableau de statut par tâche sans avoir à l’écrire.
make avec une règle de motif — idéal quand la plupart des fichiers n’ont pas changé
Le seul outil de cette liste conçu exactement pour ce problème : un ensemble de sorties dérivées d’un ensemble d’entrées, reconstruites quand l’entrée est plus récente.
MD := $(shell find docs -type f -name '*.md')
OUT := $(patsubst docs/%.md,build/%.html,$(MD))
build/%.html: docs/%.md tools/wrapper.html
@mkdir -p $(@D)
bin/one.sh lt; $@
.PHONY: all clean
all: $(OUT)
clean:
rm -rf build
| Avantages | Inconvénients |
|---|---|
| Ne convertit que ce qui a changé, sans cache à vous | Les recettes doivent être indentées par une tabulation, pour toujours |
make -j8 parallélise gratuitement, en respectant les dépendances |
Les noms de fichiers avec des espaces sont de fait non pris en charge |
| Un gabarit modifié invalide toutes les sorties, correctement | Les dates de modification sont fausses dans un clone frais |
make clean et make one/file.html viennent sans effort |
La syntaxe ne ressemble à rien d’autre dans le dépôt |
Prix : gratuit ; GNU make est sous licence GPL.
Détails techniques
tools/wrapper.htmlà droite des deux-points est la partie que les gens omettent. Sans elle, modifier le gabarit ne change rien, parce que chaque sortie reste plus récente que son propre Markdown$(@D)est le répertoire de la sortie, doncmkdir -p $(@D)crée l’arborescence au fur et à mesure$(shell find …)s’exécute à chaque invocation de make : un fichier nouvellement ajouté est donc pris en compte sans toucher au Makefilemake -jsans nombre lance un nombre illimité de tâches, ce qui, sur une grande arborescence, démarrera des centaines de processus d’un coup ; donnez-lui un nombre- Ajouter le convertisseur lui-même comme prérequis — un fichier de verrouillage, un binaire épinglé, un fichier d’empreinte de version — fait qu’une mise à jour reconstruit tout, ce que vous voulez et que personne ne fait
Pour qui ? Pour les dépôts dont l’arborescence de documentation est grande et surtout statique, et où une conversion complète dure assez longtemps pour que quelqu’un l’ait remarqué.
Un script Node ou Python — idéal quand vous voulez aussi l’habillage
À un certain point, le shell cesse d’être le bon endroit : vous voulez que le <title> du document de sortie vienne du frontmatter du fichier, ou un sommaire, ou un lien réécrit de .md vers .html. C’est un programme, pas un pipeline.
// convert-all.mjs
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, relative, resolve } from 'node:path';
import fg from 'fast-glob';
import pLimit from 'p-limit';
const files = await fg('**/*.md', { cwd: 'docs', dot: false, absolute: true });
const limit = pLimit(8);
const failures = [];
await Promise.all(
files.map((file) =>
limit(async () => {
try {
const output = resolve('build', relative(resolve('docs'), file)).replace(/\.md$/, '.html');
await mkdir(dirname(output), { recursive: true });
await writeFile(output, render(await readFile(file, 'utf8')));
} catch (error) {
failures.push(`${file}: ${error.message}`);
}
})
)
);
if (failures.length) {
console.error(failures.join('\n'));
process.exitCode = 1;
}
| Avantages | Inconvénients |
|---|---|
| La sortie est un document que vous avez conçu, pas un fragment | Il est à vous, bogues compris |
| Frontmatter, titres et réécriture des liens sont tous accessibles | Un arbre de dépendances à épingler et à auditer |
| Les échecs s’accumulent en une liste plutôt qu’en une ligne de journal | La concurrence, c’est à vous de la borner |
| Tourne pareil sur toutes les plateformes, ce que le shell ne fait pas | Démarrage plus lent qu’un binaire C ou Rust |
Prix : gratuit ; fast-glob et p-limit sont sous licence MIT.
Détails techniques
pLimitn’est pas facultatif. UnPromise.allsur trois mille fichiers ouvre trois mille descripteurs de fichiers et le processus meurt avecEMFILE, ce qui se lit comme un système de fichiers corrompu alors que ce n’en est pas unprocess.exitCode = 1plutôt queprocess.exit(1), pour que les écritures en attente s’achèvent avant que le processus ne partedot: falseest la valeur par défaut dans la plupart des bibliothèques de glob, ce qui veut dire que.github/CONTRIBUTING.mdest invisible tant que vous ne dites pas le contraire — le même piège que tend le shell, posé ailleurs- Collecter les échecs et les signaler à la fin est un choix : cela convertit tout et fait quand même échouer la tâche. L’alternative, lever une erreur au premier échec, laisse l’arborescence de sortie à moitié écrite
Pour qui ? Pour tout dépôt où le HTML doit avoir l’air fini, et toute conversion qui a besoin de savoir quelque chose du document plutôt que de ses seuls octets.
Un appel API ou CLI par fichier — idéal quand rien n’est installé
Si le runner n’a pas de convertisseur et que vous n’allez pas lui en donner un, la boucle a la même forme et le corps est un appel réseau. Le CLI sans dépendances de TransformPipe en est un exemple ; un post curl vers n’importe quelle API de conversion est la même idée avec plus d’options.
find docs -type f -name '*.md' -print0 \
| sort -z \
| xargs -0 -P 4 -n 1 -I {} tp push {} --share --json
| Avantages | Inconvénients |
|---|---|
| Aucun convertisseur à installer, à épingler ou à mettre en cache | Exige le réseau : ce ne peut donc pas être votre build hors ligne |
| Chaque fichier revient sous forme de lien que quelqu’un peut ouvrir | Exige un secret dans l’environnement |
| Le comportement de conversion ne peut pas dériver avec une installation locale | Les limites de débit et de taille s’appliquent par clé |
| La même conversion en intégration continue que sur un portable | Un document par fichier, cela s’additionne face au quota du compte |
Prix : gratuit ; convertir et télécharger ne demandent aucun compte, et un compte ajoute l’historique, le partage et l’API dans les limites ci-dessous (vérifié sur transformpipe.com, le 8 septembre 2026).
Détails techniques
- Les limites publiées sont celles à anticiper : 10 Mo pour un fichier à convertir, 4 Mo pour un document conservé dans un compte, 100 Mo et 500 documents par compte, et 60 requêtes par minute comptées par clé (vérifié sur transformpipe.com/docs, le 8 septembre 2026)
- Cette limite de débit est la raison de
-P 4et non de-P 32. Huit ouvriers parallèles sur une connexion rapide épuiseront soixante requêtes en bien moins d’une minute et commenceront à collecter des 429, et un 429 est un échec que votre script doit traiter comme tel --retry 3 --retry-connrefusedsurcurl, ou une pause entre les lots, couvre l’échec transitoire qui casserait sinon un build par mois- Si vous postez avec
curlplutôt qu’avec un CLI,-fest porteur : sans lui,curlsort avec zéro sur un 401 ou un 429 et écrit le corps de l’erreur dans votre fichier de sortie - Cent fichiers, cela fait cent documents et cent liens. Ce n’est généralement pas ce que quiconque voulait, sujet d’une section plus loin
Pour qui ? Pour les runners verrouillés, et les chaînes dont la sortie est un ensemble de liens plutôt qu’un ensemble de fichiers. Pour une pull request en particulier, une action fait la même chose sans rien installer sur le runner.
Un générateur de site statique — la réponse quand « beaucoup de fichiers » veut dire « un site »
Hugo, Eleventy, MkDocs, Docusaurus et Jekyll convertissent tous des répertoires de Markdown en HTML, et aucun d’eux n’est un convertisseur par lot. Ce sont des systèmes de build, et la différence se voit dans ce qu’ils vous rendent.
| Avantages | Inconvénients |
|---|---|
| Navigation, recherche, liens croisés et une page d’index | Un fichier de configuration, un thème et une étape de build à maintenir |
| Leurs propres builds incrémentaux et modes de surveillance | La sortie est un site, pas un ensemble de documents que vous pouvez envoyer par courriel |
| Vérification des liens et taxonomie sur l’ensemble | Une charge démesurée pour quarante fichiers que personne ne parcourt |
| Le déploiement est un problème résolu pour tous | C’est le thème qui décide de quoi vos pages ont l’air |
Prix : gratuit, tous open source.
Pour qui ? Pour quiconque publie des documents qui se lient entre eux et doivent être trouvés. Si votre lecteur arrive avec une URL que vous lui avez donnée et repart après une page, vous n’avez pas besoin d’un générateur.
Les six choses qui cassent sur un dossier et pas sur un fichier
Chacune des défaillances ci-dessous a été diagnostiquée comme un bogue du convertisseur par quelqu’un. Aucune n’en est un.
Jusqu’où descend un glob, et ce qu’il laisse tomber
docs/*.md ne descend pas dans l’arborescence. C’est de loin le bogue de conversion par lot le plus fréquent, et il est invisible : la tâche convertit les neuf fichiers du haut de l’arborescence, sort avec zéro, et les quarante des sous-répertoires ne sont tout simplement pas mentionnés. Rien ne vous prévient, parce que rien ne sait ce que vous vouliez dire.
Bash a besoin de shopt -s globstar avant que ** ne traverse un séparateur de répertoire ; sans cela, docs/**/*.md est exactement docs/*/*.md — un niveau en dessous, ni plus ni moins. zsh a le ** récursif sans option, ce qui explique qu’une ligne copiée depuis l’historique zsh d’un collègue se comporte différemment dans votre script bash sans qu’aucun de vous deux ne voie pourquoi.
Il y a ensuite les fichiers qu’un glob refuse de faire correspondre par principe :
| Ce qui est ignoré | Pourquoi | Le remède |
|---|---|---|
.github/CONTRIBUTING.md |
Les globs ne correspondent pas à un point initial | shopt -s dotglob, ou nommer le chemin |
docs/api/reference.md |
* ne traverse pas / |
globstar et **, ou find |
README.MD |
Correspondance sensible à la casse sous Linux, pas sous macOS | find … -iname '*.md' |
notes.markdown |
Une autre extension est un autre motif | -name '*.md' -o -name '*.markdown' |
| Tout, quand rien ne correspond | Bash laisse passer le motif tel quel | shopt -s nullglob ou failglob |
Rien du tout, dans node_modules |
Le glob était trop généreux | -prune, avant que le parcours n’y arrive |
La dernière ligne est la défaillance inverse, et elle est pire qu’elle n’en a l’air. Un **/*.md à la racine du dépôt atteint chaque README vendorisé de chaque paquet installé, et une conversion par lot qui se met à produire du HTML pour le changelog d’une dépendance est une conversion qu’on supprimera discrètement une semaine plus tard.
Il existe aussi une limite dure. Chaque chemin auquel un glob s’étend devient un argument, et la taille totale de la liste d’arguments est plafonnée par le noyau — getconf ARG_MAX en affiche le nombre. Un dépôt assez gros pour la dépasser échoue avec Argument list too long, un vrai message d’erreur qui sonne comme un bogue dans votre script. find … -exec … + et xargs groupent tous deux les arguments pour rester sous la limite, et ni l’un ni l’autre n’étend quoi que ce soit dans le shell — c’est pourquoi tous les exemples ci-dessus passent par un tube plutôt que par un glob dès que le compte est inconnu.
Les liens symboliques méritent une phrase de réflexion délibérée plutôt qu’un comportement par défaut. Que le ** d’un shell descende ou non à travers un répertoire lié a varié entre les versions et diffère d’un shell à l’autre : si votre arborescence contient des liens — un docs/shared pointant vers une copie de travail voisine est un montage courant —, utilisez find et décidez explicitement, avec -L ou sans. Deviner, c’est accepter que le même dépôt convertisse un ensemble de fichiers différent sur deux machines.
L’ordre dans lequel les fichiers arrivent
find renvoie les entrées dans l’ordre du répertoire. L’ordre du répertoire est ce que le système de fichiers veut bien rendre, il n’est pas trié, et il n’est pas le même sur deux machines portant les mêmes fichiers. Pour une conversion où chaque fichier devient sa propre page, c’est inoffensif. Cela cesse de l’être en trois endroits.
Les journaux cessent d’être comparables. Quand la liste des fichiers d’un build est dans un ordre différent à chaque exécution, vous ne pouvez pas comparer deux exécutions pour voir ce qui a changé, et c’est exactement cette comparaison que vous voulez d’abord quand une tâche nocturne casse.
L’ordonnancement parallèle cesse d’être reproductible. Avec huit ouvriers et sans tri, la répartition des fichiers entre les ouvriers change d’une exécution à l’autre, et avec elle celui qui touche la limite de débit.
Et la concaténation devient fausse plutôt que simplement brouillonne. Dès que plusieurs fichiers deviennent un seul document, l’ordre est du contenu. Même trié, 10-api.md vient avant 2-setup.md, parce qu’un tri lexical n’est pas un tri numérique. Complétez les nombres par des zéros — 02-setup.md, 10-api.md — et tout outil qui lira un jour ce répertoire aura le bon ordre gratuitement.
find docs -type f -name '*.md' -print0 | LC_ALL=C sort -z
LC_ALL=C compte plus qu’il n’y paraît. L’ordre de tri dépend de la locale : un nom portant un accent ou un tiret bas initial peut donc se trier autrement sur un runner que sur votre portable, et tout l’intérêt de figer l’ordre était d’empêcher cela.
À côté de l’entrée, ou dans sa propre arborescence
Il y a deux réponses, et elles ne sont pas équivalentes.
| Sortie à côté de l’entrée | Sortie dans une arborescence séparée | |
|---|---|---|
| Liens relatifs entre documents | Continuent de fonctionner sans changement | Ne fonctionnent que si la forme de l’arborescence est préservée |
| Images à chemins relatifs | Se résolvent comme avant | Demandent une copie ou un embarquement |
| Nettoyage | Supprimer les fichiers selon un motif, prudemment | rm -rf build |
Un .md supprimé |
Laisse un .html orphelin derrière lui, indéfiniment |
Disparaît au prochain build propre |
| Gestion de versions | Des entrées .gitignore qui se battent avec les fichiers source |
Un seul répertoire ignoré |
| Relire la modification | Du HTML généré dans chaque diff | Rien de généré dans le diff |
| Déploiement | Livrer tout le dépôt, ou le filtrer | Pointer l’hébergeur sur un seul répertoire |
À côté de l’entrée gagne sur les liens et les images, et perd sur tout le reste. La ligne de l’orphelin est celle qui tranche pour la plupart des gens : rien, dans un schéma à côté de l’entrée, ne remarque que docs/old-api.md a été supprimé ; docs/old-api.html reste donc sur le disque, se fait committer, se fait déployer, et est encore servi à quelqu’un un an plus tard. Une arborescence séparée que l’on supprime et reconstruit ne peut pas avoir ce problème, parce que la réponse à « quelles sorties sont périmées ? » est « toutes, à chaque fois ».
Si vous utilisez une arborescence séparée, gardez-en la forme, et faites-le par expansion de paramètre plutôt qu’avec basename. basename est le mauvais outil ici, et il échoue de la pire manière possible : docs/api/index.md et docs/guide/index.md deviennent tous deux index.html, le second écrase le premier en silence, le build sort avec zéro, et quelle page survit dépend de l’ordre dans lequel les fichiers sont arrivés — lequel, d’après la section précédente, n’est pas fixé.
La réécriture des liens est la partie qui n’a pas de solution en shell. Un lien vers ../guide/index.md dans votre Markdown survit à la conversion seulement si l’arborescence de sortie reflète celle d’entrée et si le .md de la cible est réécrit en .html. Les convertisseurs ne le font pas par défaut ; la plupart laissent le lien exactement tel qu’il est écrit, pointant vers un fichier qui n’est plus à côté de la page. C’est un programme, pas un pipeline — et ce qui casse quand un document se déplace vaut aussi pour chaque chemin d’image du dossier.
Le parallélisme, et l’ordre auquel vous renoncez
Une conversion, c’est peu de travail par fichier et un lancement de processus par fichier. Cela en fait une charge presque idéalement parallèle, et l’accélération obtenue par -P est réelle. Ce que vous échangez contre elle, c’est toute garantie d’ordre que vous aviez.
La sortie standard s’entrelace. Pas par tâche — par écriture. Deux convertisseurs qui affichent un avertissement de trois lignes au même instant produisent six lignes dans un ordre qu’aucun des deux n’a choisi, et un journal pareil ne se lit pas. Si les tâches affichent quoi que ce soit, utilisez soit parallel -k, soit faites écrire à chaque tâche son propre fichier de journal et concaténez-les ensuite dans l’ordre trié.
L’état partagé ne fonctionne pas comme il en a l’air. Chaque invocation de xargs est un processus distinct : un compteur incrémenté dans la boucle est incrémenté dans un sous-shell et disparaît. Ajouter les échecs à un fichier commun fonctionne mais demande de la prudence quant à l’entrelacement ; la version où il n’y a rien à réfléchir est un petit fichier par échec dans un répertoire, comptés à la fin :
# in bin/one.sh
if ! convert "$1" "$2"; then
mkdir -p build/.failed
printf '%s\n' "$1" > "build/.failed/$(printf '%s' "$1" | tr / _)"
exit 1
fi
Plus d’ouvriers n’est pas monotoniquement mieux. Passé le point où les processeurs sont occupés, des processus supplémentaires n’ajoutent que de la contention ; et si le corps de votre boucle est un appel réseau, des ouvriers supplémentaires ajoutent des 429. Quatre requêtes simultanées contre une limite de soixante par minute, c’est confortable. Trente-deux, c’est un test de limite de débit avec un build accroché derrière.
La taille des lots est le paramètre que l’on oublie. -n 1 démarre un processus par fichier, et pour de petits documents le lancement du processus peut coûter plus cher que la conversion. Un script qui boucle sur "$@" et qu’on invoque avec -n 20 démarre vingt fois moins de processus. Si cela aide dépend de vos fichiers et de votre convertisseur : chronométrez donc les deux — et chronométrez-les deux fois, parce que la première exécution lit à froid et la seconde depuis le cache de pages, une différence assez grande pour renverser une conclusion.
Les fichiers qui n’ont pas changé
Reconvertir cent quarante fichiers pour corriger une coquille se défend sur un portable et ne se défend pas dans une tâche qui tourne à chaque poussée. Il y a trois façons de sauter ceux qui n’ont pas changé, et elles échouent différemment.
La date de modification. C’est ce que fait make, et à l’intérieur d’une copie de travail c’est exactement juste : vous modifiez un fichier, sa mtime bouge, la règle se déclenche. Le piège, c’est que git n’enregistre pas les dates de modification. Un clone frais ou une extraction après échec de cache estampille chaque fichier à l’heure de l’extraction : sur un runner d’intégration continue, chaque fichier a donc l’air plus récent que chaque sortie et toute l’arborescence se reconstruit. Les builds incrémentaux fondés sur la mtime fonctionnent en local et ne font absolument rien en intégration continue, sauf si le répertoire de sortie est lui aussi restauré depuis un cache — et les sorties restaurées portent alors leurs propres horodatages, ce qui fait une deuxième chose à réussir.
L’empreinte du contenu. Plus lente à calculer et correcte partout, y compris dans un clone frais. Stockez l’empreinte de l’entrée à côté de la sortie et comparez avant de convertir :
# in bin/one.sh — $1 is the .md, $2 is the .html
stamp="$2.sha256"
now="$(sha256sum "$1" | cut -d' ' -f1)"
if [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$now" ] && [ -s "$2" ]; then
exit 0
fi
convert "$1" "$2" && printf '%s\n' "$now" > "$stamp"
sha256sum vient de GNU coreutils ; macOS veut shasum -a 256. Le test [ -s "$2" ] est là parce qu’une empreinte qui correspond à un fichier de sortie de zéro octet est une entrée de cache pour une exécution ratée, et un cache qui se souvient des échecs est pire que pas de cache du tout.
Demander à git ce qui a changé. La moins chère des trois quand la réponse est courte, et la seule qui tienne l’échelle d’un gros monodépôt :
git diff --name-only --diff-filter=ACMR origin/main...HEAD -- '*.md'
--diff-filter=ACMR exclut les suppressions : un fichier retiré ne devient donc pas un chemin que votre convertisseur est prié d’ouvrir. Cela réclame de l’historique — un clone superficiel n’a aucun commit de base auquel se comparer —, et c’est le marché : fetch-depth: 0 coûte du temps d’extraction sur un dépôt qui a des années de commits.
Quelle que soit votre préférence, une règle vaut pour les trois : la clé de cache doit contenir tout ce dont la sortie dépend, pas seulement le Markdown. Changez votre habillage HTML, votre feuille de style ou la version du convertisseur, et chaque sortie est périmée alors que chaque entrée est inchangée. Faites entrer le gabarit dans l’empreinte, ajoutez-le comme prérequis dans le Makefile, ou acceptez que la première personne à modifier la feuille de style passe un après-midi à se demander pourquoi la page n’a pas changé.
Les codes de sortie, et ce que « ça a marché » veut dire pour deux cents fichiers
Pour un fichier, le succès est sans ambiguïté. Pour deux cents, « est-ce que ça a marché ? » a trois réponses possibles et vous devez en choisir une avant d’écrire le script.
S’arrêter au premier échec. set -euo pipefail et une boucle toute simple. L’arborescence de sortie reste convertie à moitié, ce qui convient s’il s’agit d’un répertoire de build que vous supprimez de toute façon, et le journal s’arrête au fichier qui a cassé — le diagnostic le plus rapide possible.
Tout convertir, échouer à la fin. Plus utile quand une personne attend, parce qu’une seule exécution vous parle des six fichiers cassés au lieu du premier. Il faut un accumulateur explicite, sans quoi set -e mettrait fin à l’exécution :
failed=0
for file in docs/**/*.md; do
bin/one.sh "$file" "$(output_for "$file")" || failed=$((failed + 1))
done
if [ "$failed" -gt 0 ]; then
echo "$failed files failed" >&2
exit 1
fi
Notez le || failed=$(…). Sans lui, set -e se déclenche au premier fichier fautif et l’accumulateur ne tourne jamais. Avec lui, la boucle ne peut pas échouer — le exit 1 explicite de la fin est donc la seule chose qui rende la tâche honnête, et supprimer ce bloc par mégarde produit un build qui passe toujours.
Laisser l’exécuteur parallèle agréger. xargs vous donne 123 quand une tâche a échoué, parallel --joblog vous donne un tableau disant laquelle. Les deux conviennent, et les deux dépendent du fait que votre script par fichier sorte réellement avec un code non nul, ce qui est la partie qui tourne mal : un convertisseur qui écrit une erreur sur la sortie d’erreur et sort avec zéro, un curl sans -f, ou un script qui attrape une exception, la journalise et rend la main normalement.
Trois vérifications valent la peine d’être ajoutées quelle que soit la forme retenue :
- Chaque sortie existe et n’est pas vide —
[ -s "$output" ], parce que plusieurs modes de défaillance produisent un fichier de zéro octet plutôt qu’aucun fichier - Le nombre de sorties correspond au nombre d’entrées, affiché à la fin de l’exécution, parce qu’un glob qui a discrètement sauté un répertoire se voit ici et nulle part ailleurs
- Toute l’exécution est sous
set -euo pipefail, et tout convertisseur derrière un tube est soit redirigé à la place, soit couvert parpipefail
La deuxième est l’assertion utile la moins chère de toute la chaîne. find docs -name '*.md' | wc -l face à find build -name '*.html' | wc -l tient en une ligne, et elle attrape la défaillance qu’aucun code de sortie ne signalera jamais : le fichier qui n’a jamais été converti parce que rien n’est jamais allé le chercher.
Le faire en intégration continue sans convertir toute l’arborescence
Le coût d’installation d’un convertisseur est une question que l’article sur la ligne de commande traite comme il faut. La question propre au lot est différente : quels fichiers, et comment les sorties ressortent.
Convertissez toute l’arborescence sur la branche par défaut, et seulement les fichiers modifiés sur une pull request. L’exécution complète est votre garantie que l’arborescence est convertible ; l’exécution sur la branche est le retour rapide, et elle a besoin de fetch-depth: 0 pour que le diff ait une base à laquelle se comparer. Ajoutez un filtre paths sur **.md pour que la tâche ne se lance pas du tout sur une pull request qui n’a touché que du code.
Si les sorties valent la peine d’être conservées, téléversez-les comme artefact plutôt que de les committer. Du HTML généré dans une pull request rend chaque relecture deux fois plus longue et chaque fusion conflictuelle, et l’artefact expire tout seul.
Si vous committez malgré tout du HTML généré — certains dépôts le servent directement, et c’est un montage légitime —, ajoutez la vérification qui rend cela sûr :
npm run build:docs
git diff --exit-code -- build/
--exit-code fait échouer la tâche quand une régénération n’a pas été committée. Sans lui, le HTML committé s’écarte du Markdown une fusion hâtive à la fois, et personne ne s’en aperçoit avant qu’un lecteur ne remarque que la page contredit la source.
Mettez en cache sur une clé dérivée des entrées — GitHub Actions a hashFiles('**/*.md') exactement pour cela — et pensez à inclure votre gabarit et votre fichier de verrouillage dans la clé. Un cache indexé sur le seul Markdown vous servira du HTML périmé après un changement de feuille de style, le résultat le plus déroutant possible et le plus difficile à attribuer.
Deux choses plus petites. Ne découpez en matrice que si la conversion est vraiment l’étape lente : huit tâches parallèles ayant chacune son extraction et son installation prennent souvent plus de temps au total qu’une tâche avec xargs -P 8. Et gardez explicite le shell du runner — GitHub Actions exécute un bloc run avec bash -e {0}, ce qui n’est pas votre shell de connexion, et les réglages de shopt ne se transmettent pas d’une étape à l’autre.
Un document à partir de plusieurs, ou plusieurs à partir de plusieurs
À mi-chemin de la construction d’un convertisseur de dossier, la plupart des gens découvrent qu’ils voulaient autre chose. « Convertir ces quarante fichiers » se scinde en deux besoins qui se ressemblent et n’en sont pas un seul.
| Quarante pages | Un document | |
|---|---|---|
| Ce que vous envoyez à quelqu’un | Quarante liens, ou un répertoire | Un lien, ou un fichier |
| Navigation | Les liens que les documents avaient déjà | Un sommaire que vous générez |
| Niveaux de titre | Le # propre à chaque fichier est le titre de la page |
Chaque titre doit être rétrogradé d’un niveau |
| Identifiants d’ancre | Les doublons d’un fichier à l’autre sont inoffensifs | #installation dans quatre fichiers entre en collision |
| Ordre | Cosmétique | Du contenu — le mauvais ordre fait un mauvais document |
| Recherche | La recherche du site du lecteur, s’il y en a une | La recherche du navigateur, qui suffit souvent |
| Taille | Chaque page est petite | Un seul fichier, et un plafond de taille auquel penser |
| Contenu périmé | Une page par fichier source, supprimée avec lui | Tout régénérer, sinon c’est faux |
Si le lecteur va lire l’ensemble, c’est un document qu’il vous faut, et la conversion en est la moitié facile. Concaténer du Markdown n’est pas un cat : un cat brut colle la dernière ligne d’un fichier à la première du suivant, les titres doivent être rétrogradés pour que le # du deuxième fichier ne devienne pas un autre titre de page, et les ancres doivent être désambiguïsées. Transformer un dossier en un seul document traite de l’ordre, des niveaux de titre et des collisions d’ancres, et cela vaut la peine d’être lu avant d’écrire la boucle plutôt qu’après.
Il y a un plafond pratique à la réponse fusionnée : un document assez gros est un document que personne ne peut ouvrir. Les navigateurs encaissent quelques mégaoctets de HTML et cessent d’être agréables bien avant les limites qu’impose n’importe quel convertisseur — celui qui fait tourner ce site refuse un fichier à convertir de plus de 10 Mo et un document stocké de plus de 4 Mo (vérifié sur transformpipe.com/docs, le 8 septembre 2026), ce qui, en Markdown, fait un très long livre. Si votre sortie fusionnée approche l’un ou l’autre de ces nombres, la réponse honnête n’est pas un fichier plus gros, c’est un ensemble de pages avec une navigation, autrement dit un générateur.
Là où une boucle sur un dossier cesse d’être la réponse
La boucle est le bon outil pour un ensemble borné de documents dont la seule relation est d’habiter le même répertoire. Quatre choses brisent cela, et chacune a un coût qu’il vaut mieux nommer avant de passer une semaine sur un script de build.
Des documents qui se lient entre eux. À l’instant où docs/api.md pointe vers docs/guide.md, un simple convertisseur produit une page dont les liens visent des fichiers .md qui ne sont pas là. Les réécrire suppose d’analyser le Markdown, de résoudre la cible, de vérifier qu’elle existe et de réécrire l’extension — et vérifier qu’elle existe est le moment où vous découvrez les quatre liens qui étaient déjà cassés. Ce n’est l’option d’aucun convertisseur. C’est un vrai programme, et un générateur de site l’a déjà écrit.
Un lecteur qui arrive sans URL. Un dossier de pages n’a ni index, ni recherche, ni navigation. Si quelqu’un doit trouver le bon document au lieu de se le faire envoyer, vous construisez un site, et le faire avec un script shell revient à réimplémenter mal un générateur, un besoin à la fois. Le coût de l’admettre tôt, c’est un fichier de configuration. Le coût de l’admettre tard, c’est un script de build qu’une seule personne comprend et que plus personne ne touchera après son départ.
Des fichiers dont la sortie ne devrait pas exister. Les brouillons, les gabarits, les fragments, le répertoire _includes, la section archivée que quelqu’un a gardée « pour référence ». Un glob n’a d’opinion sur aucun d’eux : ils deviennent donc tous des pages, et certaines de ces pages seront trouvées par un moteur de recherche avant de l’être par vous. Les exclure veut dire une liste d’exceptions, dans le script, maintenue à la main, exactement le fichier de configuration que vous cherchiez à éviter.
Une arborescence qui change de forme. La boucle encode la forme de l’arborescence dans ses expressions de chemin. Réorganisez les répertoires et toutes les URL de sortie changent, tous les liens que quelqu’un avait enregistrés cassent, et il n’y a rien depuis quoi rediriger parce que rien n’a consigné les anciens chemins. Un convertisseur ne peut pas réparer cela et un générateur n’aide qu’un peu ; la vraie réponse est de décider les chemins de sortie délibérément et de les garder stables même quand la source bouge.
Rien de tout cela ne plaide contre la boucle dans le cas qui lui convient : un ensemble de documents, converti pour des gens à qui l’on donnera les liens. Cela plaide contre le fait d’en faire pousser une en système de publication par accident, la manière habituelle dont un script de quinze lignes devient quatre cents lignes que personne ne peut supprimer.
Comment choisir
- Comptez les fichiers, puis recomptez-les tels qu’ils seront dans un an. En dessous de vingt, une boucle
foravecset -euo pipefailest toute la réponse et le reste est un loisir. Au-delà de quelques centaines, il vous fautfind, la séparation par NUL et-P, parce que la limite d’arguments et le temps d’horloge deviennent tous deux réels plutôt que théoriques. - Tranchez à-côté-ou-séparé avant d’écrire une ligne. Une arborescence séparée vous coûte les liens relatifs et les chemins d’images, et vous donne un build propre, une sortie supprimable et un diff sans fichiers générés dedans. La choisir plus tard veut dire déplacer chaque sortie et réparer chaque lien d’un coup, sous la pression du temps.
- Écrivez d’abord la conversion d’un seul fichier comme son propre script. Si
bin/one.sh input.md output.htmlest correct et sort avec un code non nul en cas d’échec, chaque approche de cette page tient en une ligne à changer, et vous pouvez tester la partie difficile sans dossier. Si la logique de conversion vit dans la boucle, vous ne pouvez pas la tester du tout. - Choisissez votre politique d’échec explicitement, et faites-la prouver par la tâche. S’arrêter au premier fichier fautif, ou tout convertir et sortir avec un code non nul à la fin — les deux conviennent, et le comportement par défaut, « continuer et annoncer un succès », est ce qui met une arborescence de documentation à moitié construite en production. Ajoutez ensuite l’assertion sur le nombre de sorties, parce qu’aucun code de sortie ne vous parlera jamais du répertoire où le glob n’est jamais entré.
- N’ajoutez la conversion incrémentale que si l’exécution complète est vraiment trop lente, et indexez-la sur le contenu. La mtime marche sur un portable et ne fait silencieusement rien en intégration continue : une empreinte est donc la version qui survit à un clone frais. Incluez le gabarit et la version du convertisseur dans la clé, sinon une mise à jour vous laissera servir la sortie de l’ancienne.
- Demandez-vous si la réponse est un seul document. Si le destinataire va lire tout l’ensemble, quarante liens sont un moins bon livrable qu’une seule page, et le travail se déplace de la boucle vers la fusion. C’est un autre problème, aux modes de défaillance différents, et le découvrir après coup veut dire écrire le script deux fois.
Conclusion
Une conversion par lot, c’est une petite quantité de conversion emballée dans une grande quantité de comptabilité, et c’est dans la comptabilité que vivent les défauts : le glob qui a atteint neuf fichiers sur quarante-neuf, le chemin de sortie qui a replié deux pages en une, l’exécution parallèle dont les échecs sont partis dans un sous-shell, le cache qui se souvenait d’une feuille de style qu’il n’avait jamais vue. Écrivez la conversion d’un seul fichier comme un script qui sort honnêtement, pilotez-le avec find et la séparation par NUL pour que l’ensemble des fichiers soit connaissable et stable, gardez la sortie dans une arborescence que vous pouvez supprimer, et vérifiez le nombre de sorties à la fin pour qu’un répertoire manquant soit un build en échec plutôt qu’une page dont personne ne remarque l’absence. Ensuite, quand l’ensemble se révélera être un document plutôt que quarante, vous aurez gardé les deux travaux séparés — et quand il s’agit vraiment d’un seul fichier qui doit avoir l’air fini, en convertir un dans le navigateur est gratuit, ne demande aucune installation et ne téléverse rien tant que vous n’êtes pas connecté.
FAQ
Comment convertir plusieurs fichiers Markdown d’un coup depuis un terminal ?
Laissez le shell énumérer et laissez un script convertir un seul fichier : find docs -type f -name '*.md' -print0 | sort -z | xargs -0 -P 8 -n 1 bin/one.sh. Construisez le chemin de sortie dans le script avec ${file%.md}.html pour que l’arborescence garde sa forme, et faites un mkdir -p de la destination avant d’écrire. Mettez set -euo pipefail en tête du script et laissez xargs renvoyer 123 si un fichier a échoué.
Pourquoi ma conversion par lot a-t-elle sauté les fichiers des sous-répertoires ?
Parce que docs/*.md ne descend pas dans l’arborescence et que ** ne traverse les séparateurs de répertoire que si l’option globstar de bash est activée. Sans shopt -s globstar, le motif docs/**/*.md correspond à exactement un niveau en dessous, et tout fichier plus profond est ignoré sans la moindre erreur. Utilisez find à la place, ou activez l’option, et comparez le nombre d’entrées et de sorties à la fin de l’exécution.
Puis-je convertir un dossier de fichiers Markdown en parallèle ?
Oui — xargs -0 -P 8, GNU parallel ou make -j8 le font tous, et l’accélération est réelle parce que l’essentiel du coût est le démarrage des processus plutôt que l’analyse. Ce que vous perdez, c’est l’ordre : les tâches concurrentes entrelacent leur sortie ligne par ligne, et un compteur incrémenté dans la boucle vit dans un sous-shell et disparaît. Utilisez parallel -k si la sortie doit rester ordonnée, et écrivez un petit fichier par échec au lieu d’alimenter une variable.
Comment sauter les fichiers Markdown qui n’ont pas changé ?
make avec une règle de motif le fait à la date de modification et ne demande aucun cache à vous, mais git ne stocke pas les mtimes : dans une extraction d’intégration continue fraîche, tout a donc l’air neuf et toute l’arborescence se reconstruit. Une empreinte de contenu stockée à côté de chaque sortie fonctionne partout, y compris dans un clone frais. Quelle que soit votre méthode, incluez le gabarit HTML et la version du convertisseur dans la clé, sinon un changement de feuille de style laissera chaque page périmée.
Le HTML doit-il aller à côté du Markdown ou dans un dossier séparé ?
Une arborescence séparée, dans presque tous les cas : vous pouvez la supprimer, elle reste hors de vos diffs, et un fichier Markdown supprimé ne peut pas laisser de page orpheline derrière lui. La sortie à côté de l’entrée n’est clairement meilleure que si les liens relatifs et les chemins d’images entre documents doivent continuer de fonctionner sans être touchés. Si vous prenez une arborescence séparée, reproduisez la structure des répertoires — basename repliera deux fichiers index.md en une seule sortie, et sortira avec zéro en le faisant.
Pourquoi mon build passe-t-il alors que certaines conversions ont échoué ?
Parce que rien n’a vérifié. Une boucle shell continue après un échec et sort avec le code de la dernière commande, un tube rapporte l’étape finale plutôt que le convertisseur, et plusieurs outils traitent une erreur comme un avertissement — curl sort avec zéro sur un 429 quand -f manque. Utilisez set -euo pipefail, redirigez au lieu de mettre en tube, et ajoutez un exit 1 explicite après avoir compté les échecs.
Vaut-il mieux convertir quarante fichiers ou les fusionner en un seul ?
Cela dépend entièrement du lecteur. Quarante pages conviennent à quelqu’un qui arrive avec un lien vers l’une d’elles ; un document convient à quelqu’un qui va lire l’ensemble, et c’est un lien plutôt que quarante. Fusionner n’est pourtant pas concaténer — les niveaux de titre doivent être rétrogradés et les ancres en double désambiguïsées — alors traitez cela comme un travail à part plutôt que comme une option de la boucle.