Mon moteur de blog est un programme plus vieux que moiđź”—

🗓 In blog/ Blog/

#c #awk

Introduction

Ce blog est édité depuis au moins 2007. Il a connu plusieurs moteurs (le programme chargé de faire l'affichage sous forme de page HTML). J'ai utilisé serendipity, pyblosxom, ikiwiki et enfin, mkdocs. Lors de la dernière migration de ikiwiki vers mkdocs, j'avais même dû coder en Python pour adapter mkdocs à mes besoins. Globalement j'étais plutôt content, mais j'ai fini par me rendre compte que quelque-chose clochait avec le temps.

En effet, mkdocs, c'est très bien mais ça évolue dans le temps. Il faut mettre à jour. Comme j'ai codé un module à côté, il faut aussi que je fasse la maintenance de ce module. Donc, du travail en plus. Ça ne m'a pas coûté tant de temps que ça mais comme mon temps libre est précieux, ça compte quand-même.

Et puis, j'ai fini par me rendre compte qu'à chaque petite évolution de mkdocs, il fallait que je mette à jour un grand nombre de paquets sur ma distribution GNU/Linux. Ça se fait automatiquement mais comme je suis sur la distribution de test, les mises à jour, ça arrive souvent.

Enfin, mkdocs c'est bien mais c'est bourré de choses dont je n'ai pas besoin. Par exemple, ça embarque du Javascript que je n'utilise pas et ça impose d'avoir des packages javascript d'installés. J'aime assez peu le Javascript, simplement pour en souffrir tous les jours dans mon travail (celui qui me permet de manger) et quand j'en vois, même sous forme de package Debian, ça me fait du mal.

Et puis, en ces temps de fin de l'abondance et de retour vers une frugalité bienheureuse, je me suis dit qu'il fallait que je trouve un truc plus simple, plus stable dans le temps et exempt des défauts de mkdocs et c'est ce que je vais présenter dans cet article.

A la recherche d'une solution simple mais efficace

Je me suis dit qu'il était temps de revenir à l'essentiel de l'informatique. Depuis le début de l'année, je refais du C. Plus le temps passe et plus je code dans ce langage en utilisant la bibliothèque standard (enfin, la GNU libc), ainsi que les fonctions POSIX ou celles apportées par le noyau Linux, plus je me dis que c'est cool et que tout a déjà été inventé il y a 40 ans et que depuis, on ne fait que réinventer la roue. Plus je vieillis, et plus je m'en rends compte fréquemment.

Je vois passer des usines à gaz, des trucs bloatés comme la mort, qui mobilisent des temps de charge CPU et d'occupation de RAM qui feraient palir n'importe quel sysadmin d'il y a 15 ans. Le tout, pour des résultats identiques à ce qu'on pouvait produire il y a 15 ans, en termes de rapidité et de plus-value.

A l'inverse, plus je creuse les technos originelles d'Unix, plus je vois à quel point elles étaient clairvoyantes et efficaces. C'est sans doute le manque de ressources informatiques qui a conduit les développeurs de l'époque à trouver des solutions simples mais pour autant très perspicaces.

Si je reviens à mon problème de génération de blog, la démarche classique consiste à fouiller ce qui existe comme moteurs, à les tester et à essayer de faire une conversion avec ce moteur. A la fin du processus, ça se passe comme dans Highlander: il ne doit en rester qu'un. C'est la démarche que j'ai utilisée pour mkdocs: j'en ai testé une pelletée de moteurs et j'ai pris mkdocs qui était le plus simple par rapport à mes besoins. Aujourd'hui, je ne vais pas me relancer dans cette démarche mais plutôt voir ce que, moi, en tant qu'individu, je peux mettre en place.

Mon idée n'est pas de refaire un moteur de blog: je n'en ai pas les moyens. Je ne vais pas passer de la maintenance d'un module mkdocs à tout un moteur, j'ai encore moins de temps que ça. Mais la question fondammentale à se poser c'est plutôt: est-ce-que j'ai vraiment besoin d'un moteur de blog ? Et après l'avoir bien soupesée, ma réponse, c'est non !

Pour schématiser, mon blog et mon site web personnel, c'est ça:

Arborescence de texte en Multi-Markdown ----> transformation ---> arborescence de fichiers HTML5.

Alors, un programme dédié à ça, c'est sans doute très bien. Mais en fait, on peut faire sans. Par exemple, je pourrais écrire en HTML5 directement. J'ai essayé et ça n'a pas de valeur ajoutée, même si ça reste faisable. C'est compliqué et ça m'oblige à être compétent en HTML5, et surtout à le rester. C'est bien trop verbeux par rapport au résultat attendu et puis, ce n'est pas pour rien que Markdown existe. Pour l'utiliser régulièrement, ça répond vraiment à mes besoins d'édition: écrire du texte directement avec quelques options mais en étant focalisé sur le contenu plutôt que sur l'apparence. Ça confirme que je dois conserver cette phase de transformation.

La solution

Or, pour tout ce qui relève de la transformation de texte linéaire, en colonne ou à champs fixes, il existe un outil formidable pour ça: Awk. Inventé en 1977 par Brian Kernighan, Peter Weinberger et Alfred Aho, c'est un outil remarquable qui a fait l'objet d'un livre de renom (The AWK Programming Language). Je vous livre une traduction du tout premier paragraphe de sa préface:

"Les utilisateurs d'ordinateurs passent beaucoup de temps à effectuer des manipulations de données, de manière simple et mécanique comme changer le format des données, vérifier leur validité, trouver des objets avec certaines propriétés, ajouter des nombres, imprimer des rapports et tout ce qui s'y rapporte. Toutes ces tâches devraient être mécanisées, mais c'est un souci réel de devoir écrire un programme dédié dans un langage comme le C ou Pascal, à chaque fois qu'une de ces tâches survient."

Et effectivement, cette déclaration fait sens. En fait depuis 1977, date à laquelle Awk est sorti en version utilisable, les problèmes de lire et transformer des données de manière simple disposent déjà d'une solution dédiée sur-mesure. Et ce que les auteurs disent déjà à l'époque, c'est que faire un programme en C pour ça, c'est un peu overkilling non ? Donc, ça l'est probablement tout autant en 2022, y compris avec Python.

Aussi, après avoir lu the Awk Programming Language ainsi que le manuel de GNU Awk, je me suis lancé dans l'aventure de refaire un moteur de blog avec ce langage. Bien entendu, je me suis également appuyé sur d'autres programmes simples et éprouvés. Comme si j'avais pu produire ce truc dans les années 90.

Ce que j'ai mis au point

Le détail de ce que j'ai mis au point est disponible dans ma forge de développement, directement sur le dépôt git qui stocke à la fois le contenu (le Markdown) et le code du moteur de blog.

Pour résumer, le moteur est composé des blocs suivants:

Rien de bien complexe et ça reste très fonctionnel. Ça m'a pris quelques heures de développement, mais ça a été très instructif. Maintenant, je suis sensiblement plus compétent sur Make (j'arrive à chaîner deux Makefile de manière dynamique par exemple) et c'est la même chose pour Awk (j'ai fait de la gestion de coprocess par exemple).

Au final, rien que de vieilles technos ultra-éprouvées mais qui continuent à être maintenues ou améliorées:

Et au final, ça donne quoi ?

Au final, c'est vraiment contenu en termes de code:

En termes de performances, grâce à la capacité de Make à lancer plusieurs jobs en même temps (l'option -j), je peux regénérer l'intégralité de mon blog à partir de zéro en 5 secondes (même sans cache). Dans ces conditions, je crois que ça bat à peu près n'importe quel moteur de blog, y compris ceux en Go ou en Rust, même si ce n'était pas du tout mon objectif. Je voulais simplement quelque-chose de proche de mkdocs. J'ai produit mieux avec des technos d'il y a quarante ans. C'est dingue !

Est-ce-que j'ai galéré à coder tout ça ? Après avoir atteint la version 1.0 du truc, je dois dire que pas vraiment. Awk, ça reste abordable et c'est très bien documenté. J'ai eu un peu de mal, comme tout le monde avec getline et aussi les coprocess, mais ce sont des éléments avancés de Awk. À aucun moment je ne me suis retrouvé coincé, c'est le principal. Mieux, j'ai souvent trouvé plusieurs méthodes possibles pour atteindre mon objectif.

Bien entendu, ce ne sera pas parfait: je n'ai couvert que mes besoins propres qui sont loin de couvrir tout ce qu'on peut faire en Markdown. À un moment, je me suis dit que ça serait bien de couvrir la norme CommonMark. Mais, après tentatives, c'est bien trop complexe par rapport au temps que je peux y consacrer. Mais dans l'ensemble, je suis assez confiant dans le code que j'ai pu produire. Ça semble couvrir à peu près toutes les situations et ça reste rapide.

Ce qui est aussi sympathique, c'est le fait de me dire que ça ne devrait plus vraiment bouger dans le temps. Si je dois refaire la tronche de mon site web, j'ai simplement à changer le template HTML, pas le moteur.

Et pour le moteur de recherche ?

Bon, tinysearch c'était gentil comme truc à tester mais, avec le temps, j'ai fini par me rendre compte que tout ce qui vient avec Rust, c'est assez lourd. D'abord, parce que je ne suis pas du tout compétent dans ce langage. Ensuite, c'est aussi lié au fait que Rust n'est pas complètement intégré au système d'exploitation et qu'il faut installer une blindée de packages pour faire tourner un compilateur. Alors que pour le C, globalement, c'est pratiquement intégré de facto, et ce, depuis plus de 40 ans. De fait, avec le temps, la génération du moteur tinysearch en wasm depuis Rust et cargo prenait un temps fou. Je crois que c'est parce que, par défaut, ça mettait à jour tous les packages Rust utilisés par le programme (et il y en avait un paquet). Bon, j'ai rien contre Rust, juste que je suis pas compétent et que je n'ai pas le temps ni le goût à ça pour l'instant: je fais déjà du C, du C++, du javascript et du Python3, du Awk, c'est déjà pas mal non ?

Je me suis dit que, vu mon aversion pour Javascript et toutes ces technos, il me fallait un truc oldschool mais qui tienne la barque. Ensuite, embarquer l'intégralité d'un filtre bloom dans la page de recherche, c'est pas super fin à mon avis. Ça peut se justifier si on souhaite se passer d'un truc serveur. Mais dans mon cas, coder un truc qui fait la même chose que tinysearch en C et en Wasm, c'est au-delà du temps que je peux y consacrer.

Et puis, il y avait aussi le fait que les résultats de tinysearch étaient vraiment souvent à côté de la plaque. C'est lié au fait qu'un filtre bloom, ce n'est pas parfait. Je m'en contentais mais ça n'offrait pas vraiment une bonne expérience utilisateur.

Non, je suis revenu sur le fait que je pouvais par contre faire un truc côté serveur pour renvoyer les résultats de recherche via une API. Après quelques minutes de recherche, j'ai trouvé que je pouvais faire un script fast-cgi en C qui utilise le moteur de recherche de texte intégré de SQLite: FTS5. Après tout, SQLite on peut l'attaquer en C, c'est fait en C et le format de données est compact. Et c'est ce que j'ai fait, et ce, dans un temps finalement assez limité. J'ai fait une API qui retourne du JSON, sans utiliser d'autre bibliothèque que celle de fastcgi et de sqlite3 et aussi, sans allocation dynamique de mémoire (c'est exposé sur un serveur, faut pas déconner). Résultat, j'ai mon propre moteur de recherche interne en moins de 300 lignes de C commenté et ça tourne à fond la caisse, même pas besoin de cache. Il faudra que j'en parle plus tard, quand ce sera montrable. Pour autant, c'est en prod !

Investir du temps sur Awk

Ça faisait grosso-modo 20 ans que j'utilisais awk sans véritablement savoir m'en servir correctement. J'en ai fait des one-liners très pratiques au demeurant, mais j'avoue que parfois, ça devenait presque cryptique. D'une manière générale, tout ce qui utilise des regexp finit par devenir cryptique. Néanmoins, en 40 ans, on n'a pas trouvé mieux pour synthétiser une commande de correspondance ou de capture/substitution sur des chaînes de caractères.

Pour ce projet, j'avais donc besoin de me reprendre en main sur Awk. Et j'ai tout simplement lu les références du sujet pour ce faire:

Si vous lisez ça, vous serez sans doute d'un bon niveau en Awk. Je vous conseille de commencer par The Awk Programming Language. D'abord parce que c'est un ouvrage de référence et surtout, parce qu'il est bien fait. C'est assez dingue de pouvoir lire un livre de 1987 en informatique qui soit toujours à la page. En lisant The Awk Programming language, j'ai pris conscience qu'en fait Awk, déjà en 1987, c'était un ETL très puissant et qu'on n'a pas inventé grand-chose de mieux depuis. Par exemple les auteurs montrent pendant plusieurs chapitres, des exemples de restructuration de données (dont des aggrégations, des sommes sur des filtres) avec à la fin un export vers du reporting (en mode texte, certes mais ça reste du reporting).

On aurait dit une espèce de contrepoint parfait à Visicalc: ça n'est pas visuel, ça n'utilise pas de tableur et pourtant, on peut générer un bon rapport en quelques lignes de Awk, à partir d'un tableau de données. Ce n'est pas WYSIWYG, mais c'est très puissant même pour un débutant en Awk. Il y a également d'autres examples sur des opérations qui relèvent plus de l'administration système et ça montre bien la versatilité de Awk.

Et puis, il y a aussi la puissance des pipes. Les auteurs les utilisent de manière directe, pour faire la colle avec tout ce que le shell peut apporter. J'ai été surpris par ça. En règle générale, j'ai tendance à limiter ce qui sort de Awk pour l'envoyer moi-même dans un pipe. Mais, on peut le faire directement depuis Awk, c'est même prévu pour ça. Par exemple, on a l'utilisation de coprocess dans la version GNU de Awk qui permettent de sortir un bloc de texte de Awk, de l'envoyer vers une commande de traitement externe, puis de récupérer ce que la commande externe a digéré et produit pour le réinjecter dans Awk et refaire un traitement. C'est assez génial et ça se fait en 4 lignes de code.

D'ailleurs, d'une manière générale, je vous invite également à utiliser la version GNU de Awk. D'abord parce que c'est la plus fournie en termes de fonctionnalités. Il y a quelques fonctions en plus, mais elles m'ont été très utiles, notamment pour la gestion des dates: strftime permet de formatter des dates et ça n'existe pas dans les autres versions de AwK. Certes il y a des choses en plus, sans doute pas standardisées, mais avec le recul, je n'ai jamais encore été piégé par GNU Awk, à l'inverse de nawk (l'implémentation par défaut). Il faut dire que la commande gensub est bien pratique.

Et puis, GNU Awk vient avec un manuel vraiment bien fait et vraiment complet. Dans l'ensemble, une fois qu'on a compris le fonctionnement global de Awk, ça reste très abordable. Le manuel de GNU Awk va assez loin dans les détails et vour permet de trouver des solutions multiples à vos problèmes. Par exemple, j'avais un problème pour reformatter mes paragraphes où je devais faire sauter des retours à la ligne pour faire en sorte qu'un paragraphe de texte tienne sur une seule ligne (c'est conforme à la norme Gemini). J'ai trouvé la solution dans le manuel.

Enfin, GNU Awk est plutôt bon en termes de performances. Il est réputé pour être moins rapide que nawk, mais à l'usage, je trouve que ça reste blazingly fast comme on dit.

Mais alors, dans quels cas ne pas utiliser Awk ? Je dirais pour tout ce qui n'est pas linéaire ou tabulaire ou à champs délimités. Par exemple, les données de type graphe ou arbre, comme une arborescence XML ou JSON. Je pense que ce n'est pas impossible de le gérer avec Awk, mais je crois qu'il y a des outils plus simples d'accès pour ça, comme jq par exemple.

Conclusions

En un peu plus d'un mois de travail, j'ai réussi à revenir aux fondammentaux de l'informatique. Ça fait un peu oldschool mais ça reste cool ! Et en plus, c'est plein d'avantages:

Après avoir sérieusement augmenté mes compétences avec Awk, je vous conseille vraiment d'apprendre ce langage. Pour tout ce qui est traitement de texte linéaire, en colonnes ou à champs fixes, c'est vraiment très puissant et très efficace à coder. De mon expérience, je crois que c'est plus efficace et plus rapide de faire un script Awk que de coder l'équivalent en Python3. Et croyez-moi, des scripts Python ou des modules entiers destinés à retravailler des données avec des regexp et de la manipulation de fichiers, j'en ai fait pas mal dans ma vie (voir gis2fg par exemple). Dans Awk, tout est plus simple: c'est fait pour lire le fichier en direct et traiter les lignes avec des regexp et des fonctions. Pas besoin d'apprendre à gérer des modules spécialisés ou de se taper les multiples fonctions du module re, d'avoir à gérer les ouvertures/fermetures de fichier, de gérer les différents PEP et leur évolution, etc.

En plus de ça, je peux aussi dire que mon moteur de blog est plus vieux que moi (je suis né en 1978). Ça me fait tout drôle car, j'ai connu les débuts de l'informatique domestique et son évolution dans l'informatique d'aujourd'hui. Donc la majorité des programmes utilisés aujourd'hui sont très souvent postérieurs à 1978.

En conclusion, je ne peux que vous inciter à vous tourner vers des solutions "oldschool" lorsque vous cherchez une solution à vos besoins informatiques. Avec l'expérience, je constate que bien souvent, on trouve une réponse éprouvée et étayée par de nombreuses années de pratiques. Je crois qu'on peut dire qu'on peut plonger les yeux fermés vers tout ce qui a été réalisé par nos aïeux Unixiens des années 70 (Denis Ritchie, Rob Pike, Ken Thompson, Donald Knuth, Brian Kernighan et bien d'autres encore).