aplat(1) : documents structurés pour Unix

Publié le 21a1 et modifié le 2224


Les utilitaires classiques de Unix se prêtent très bien à la manipulation de données structurées par des listes ou par des tableaux, c’est-à-dire à la manipulation de données qu’on encode aisément avec des lignes et des champs. Un bon nombre de ces utilitaires (grep, sed, awk, cut, sort, diff, uniq, etc.) ont en effet été conçus spécialement pour l’analyse et le traitement des données ligne par ligne. Aussi, ils offrent souvent la possibilité de reconnaître des segments de ligne séparés par un délimitateur arbitraire, faisant émerger ce deuxième niveau d’organisation qu’est le champs.

Il existe cependant d’autres types de strutures de données qu’on ne représente pas instinctivement par ces éléments, et les utilitaires qui s’attendent à trouver des données organisées ligne par ligne ont évidemment bien souvent de la difficulté à traiter correctement les formats de document structurés selon un autre principe. Parmi ces formats, on compte notamment le XML, le TeX, le Markdown, etc., mais aussi le C, le Shell, le Scheme, etc. Parce qu’ils supposent une structure au nombre de niveaux fixe et assez limité (souvent deux : la ligne et le champ), ces utilitaires semblent parfaitement inadaptés au traitement de ces autres formats, d’autant plus que plusieurs d’entre eux ont une structure récursive, admettant donc un nombre illimité de niveaux.

Bien que, donc, les utilitaires de traitement de texte conçus pour bien fonctionner dans un environnement de type Unix n’ont pas une portée absolument générale, ils restent très utiles pour mener à bien un grand nombre de tâches. Comme beaucoup d’autres, j’attribue cette utilité à leur simplicité, à leur (relative) cohérence, à leur composabilité et à leur omniprésence. Je les trouve en outre agréables à utiliser. Mais je ne venterai pas davantage leurs mérites, car le programme informatique que cet article doit présenter s’adresse avant tout à ceux que ces mérites séduisent déjà, surtout que ce programme est parfaitement inutile lorsqu’utilisé seul et qu’il ne trouve sa valeur que dans la coopérations avec ces autres utilitaires.

Puisqu’il est de toute façon utile d’apprendre à maîtriser ces utilitaires Unix et que je suis maintenant devenu à l’aise avec eux, je me suis donné pour objectif d’étendre leur portée en créant un format de document structuré selon le principe des lignes et des champs qui permettrait de représenter des données structurées selon n’importe quel autre principe.

Je présente ici le programme aplat(1) et les deux formats qu’il utilise. Le programme lit en entrée standard un document dans un certain format et exporte en sortie standard sa représentation dans un format pont organisé selon des lignes et des champs.

J’ai conçu le format de sortie pour qu’il soit facile pour les utilitaires classiques de Unix de le manipuler. Puisqu’il présente la structure des documents sous une forme applatie, j’ai décidé de lui donner le nom de plat.

J’ai conçu le format d’entrée pour qu’il soit facile pour une personne de le manipuler. C’est un format beaucoup plus simple à comprendre et selon moi beaucoup moins pénible à écrire à la main que le XML. Il s’inspire beaucoup du SXML. Puisqu’il s’oppose en quelque sorte au format plat(5), je lui ai donné le nom de aplat (non-plat). Il s’agit d’un a privatif, qu’un trouve par exemple dans des mots comme apatride ou anonyme.

Quant au programme, il se serait appelé àplat si la tradition restreignait pas le nom des programmes à l’ensemble des suite de caractères ASCII affichables. Sa tâche est en effet de passer d’aplat(5) à plat(5).

aplat(5)

Le format aplat(5) permet une organisation hiérarchique de données étiquettées. Il est inspiré du SXML et est aussi puissant que lui. C’est surtout pour des raisons de simplicité que j’ai choisi de créer un nouveau format de document plutôt que d’en utiliser un qui existait déjà. Bien que cette entreprise aurait été tout à fait possible, je ne voulais pas avoir à composer avec la complexité du XML, par exemple. Aussi, la création d’un nouveau format de toutes pièces donne une liberté bien plus grande. De toute façon, l’intérêt d’aplat(1)se trouve selon moi bien plus dans son format de sortie que dans son format d’entrée, et il est possible de créer un programme distinct traduisant le XML, le SXML, le HTML ou n’importe quel autre format vers plat(5).

Voici un exemple de document valide.

(doc
  (titre Un\ programme\ en\ C)
  (auteur ~ldp)
  (date-pub 2023 - 11 - 18)
  (date-mod (vide))
  (par "Voici un exemple de document dans le format aplat.")
  (lien
    (url "https://asteride.xyz/~ldp/exemple.txt")
    (texte "Un exemple d’URL"))
  (pre
""" langue=C
#include <stdio.h>

int
main(void)
{
	printf("Bonjour tout le monde!\n");
	return 0;
}
"""
  )
  (par "Ce paragraphe affiche " (it "Bonjour tout le monde!")))

Je veux faire quelques remarques sur le contenu de cet exemple. Sachez cependant que cet article complète plutôt qu’il ne remplace les pages du manuel livrées avec le programme.

=> aplat(1)
=> aplat(5)
=> plat(5)

La structure hiérarchique d’un document aplat(5) est explicitée par les parenthèses qui encadrent une section de ce document. J’appelle domaine l’espace se trouvant entre deux parenthèses de même niveau.

Un domaine, s’il n’est pas vide, est composé d’atomes. Un atome est une chaîne de caractères séparés par une parenthèse ou par des blancs (espaces, caractères de tabulation ou nouvelles lignes).

Le premier atome d’un domaine correspond à l’étiquette. Il nomme un domaine. Tous les autres atomes d’un même domaine sont comme concatenés pour former une longue chaîne de caractères. Je nomme cette chaîne le contenu. « doc », « titre », « auteur », « vide », etc. sont tous des exemples d’étiquettes ; « ~ldp » est un exemple de contenu. Aussi, notons que, puisque les atomes adjacents sont concatenés, le segment « 2023 - 11 - 18 » correspond à l’atome « 2023-11-18 ».

Il est possible d’inclure des caractères spéciaux dans des atomes en les échappant. Ils seront alors traités comme des caractères normaux.

Il existe trois manières d’échapper des caractères spéciaux.

D’abord, on peut utiliser la barre oblique inversée (« \\ »). Son effet varie selon le caractère échappé. Les parenthèses, les espaces normaux, les caractères de tabulation, les guillemets et le caractère d’échappement lui-même sont interprétés dans leur sens littéral lorsque ce caractère les précède. Cependant, une nouvelle ligne échappée est ignorée. Dans l’exemple ci-dessus, la chaîne « Un\ programme\ en\ C » est interprété comme un seul atome et correspond à « Un programme en C ».

Ensuite, on peut placer les caractères à échapper entre guillemets. Les parenthèses et les blancs placés entre deux guillemets reçoivent leur interprétation littérale. Par exemple, ici la chaîne « "Voici un exemple de document dans le format aplat." » forme un seul atome.

Finalement, on peut créer un bloc en plaçant les caractères à échapper entre deux triples guillemets droits doubles (« """ »). Tous les caractères reçoivent alors leur interprétation littérale. À l’intérieur d’un bloc, il est possible d’échapper la séquence « """ » en la faisant suivre d’un point d’exclamation (« """! »).

Tous les caractères entre la séquence d’ouverture d’un bloc et la première nouvelle ligne, inclusivement sont ignorés. C’est pour faciliter le travail de préprocesseurs, qui fonctionneront d’une manière analogue à ceux de troff. Ces préprocesseurs, contrairement aux postprocesseurs, opéreront sur l’entrée de aplat(1) plutôt que sur sa sortie. On pourra alors leur fournir de l’information sur cette première ligne.

Les blocs sont utiles pour inclure verbatim un bloc de texte, comme un programme informatique ou un document HTML.

Métaformats

À partir de cette description, il est possible — nécessaire, même — de créer un métaformat adapté à certains besoins. Ainsi, on peut imaginer une manière de représenter les métadonnées comme le fait le XML. Cette idée est empruntée au SXML. Est une métadonnée (paire attribut-valeur) un domaines et son contenu s’ils se trouvent à l’intérieur d’un domaine étiquetté avec « @ ».

Voici l’exemple précédent converti à cette convention.

(doc
  (@ (titre      "Un programme en C")
     (auteur     "~ldp")
     (date-pub   "2023-11-18")
     (date-mod   (vide))
     (métaformat "genre-de-sxml"))
  (par "Voici un exemple de document dans le format aplat.")
  (lien (@ (ref "https://asteride.xyz/~ldp/exemple.txt"))
	 "Un exemple d’URL")
  (pre
""" lang=C
#include <stdio.h>

int
main(void)
{
	printf("Bonjour tout le monde!\n");
	return 0;
}
"""
  )
  (par "Ce paragraphe affiche " (it "Bonjour tout le monde!")))

Ici, les attributs « titre », « auteur », « date-pub », etc. portent sur le domaine nommé « doc » et l’attribut « ref » porte sur celui nommé « lien ».

plat(5)

Le format plat(5) est une représentation équivalente au aplat(5), au XML, etc.

Voici la représentation en plat du dernier exemple en aplat.

:doc:	(	
:doc:@:	(	
:doc:@:titre:	()	Un programme en C
:doc:@:auteur:	()	~ldp
:doc:@:date-pub:	()	2023-11-18
:doc:@:date-mod:	(	
:doc:@:date-mod:vide:	()	
:doc:@:date-mod:	)	
:doc:@:métaformat:	()	genre-de-sxml
:doc:@:	)	
:doc:par:	()	Voici un exemple de document dans le format aplat.
:doc:lien:	(	
:doc:lien:@:	(	
:doc:lien:@:ref:	()	https://asteride.xyz/~ldp/exemple.txt
:doc:lien:@:	)	
:doc:lien:	)	Un exemple d’URL
:doc:pre:	()	#include <stdio.h>\n\nint\nmain(void)\n{\n\tprintf("Bonjour tout le monde!\\n");\n\treturn 0;\n}
:doc:par:	(	Ce paragraphe affiche 
:doc:par:it:	()	Bonjour tout le monde!
:doc:par:	)	
:doc:	)	

On voit tout de suite pourquoi ce format n’est pas destiné à être manipulé par des humains. Il est en revanche très adapté à des outils analysant les fichiers ligne par ligne et champ par champ.

Un document plat(5) est composé d’une série de lignes et de trois champs, séparés par des caractères de tabulation.

Le premier champ correspond au nom du domaine. Il prend la forme de la liste des toutes les étiquettes des domaines parents, précédées d’un deux-points (« : »). La dernière étiquette est aussi suivie d’un deux-points. Il n’est pas possible d’échapper ce caractère, car plusieurs outils auraient du mal à composer avec une telle syntaxe.

J’appelle domaine cadet le domaine le plus imbriqué. Ainsi, à la ligne commençant par « :doc:@:date-mod:vide: », le domaine cadet est « :vide: », alors qu’à la première ligne le domaine cadet est « :doc: ».

Tout nom de domaine doit apparaître au moins une fois en position de domaine cadet, et ce même s’il est vide. Par exemple, bien que « :@: », dans le domaine « :doc:@: », ne renferme que d’autres domaines, il a droit à sa ligne, de même que « :vide: ».

Le second champ est une liste de drapeaux. Pour l’instant — et je ne pense pas en ajouter de si tôt —, il en existe deux: la parenthèse ouvrante (« ( ») et la parenthèse fermante (« ) »), dénotant respectivement la limite initiale et la limite finale d’un domaine.

Le troisième champ correspond au « contenu » du format aplat(5).

plat(5) est organisé selon le principe des lignes et des champs ; les lignes sont séparées par le caractère de saut de ligne et les champs par un caractère de tabulation. Pour utiliser ces caractères au sens propre, il suffit de les échapper. On peut voir le mécanisme d’échappement à l’œuvre à la ligne commençant par « :doc:pre: » dans l’exemple ci-dessus.

Utilisation d’aplat(1)

Le programme aplat(1) ne lit aucun argument en ligne de commandes : ni options ni nom de fichier ; il se contente de lire en entrée standard et d’écrire en sortie standard.

Il s’ulitise donc de la manière suivante.

$ aplat <entree.aplat >sortie.plat

En supposant que le document aplat(5) de l’exemple précédent se trouve dans le fichier bonjour.aplat, on pourrait écrire la séquence de commandes suivantes pour en extraire le programme, le compiler et l’exécuter.

$ aplat <bonjour.aplat | grep '^:doc:pre:	' | cut -f3 |
	{ tr -d '\n'; echo; } | xargs -0 printf '%b' |
	cc -o bonjour -x c - && ./bonjour
Bonjour tout le monde!

Ici, aplat fait passer du format aplat(5) au format plat(5) le contenu du fichier bonjour.aplat ; grep ne retient de la sortie de la commande précédente que les lignes dont l'étiquette est :doc:pre: ; cut isole le troisième champ ; tr fusionne toutes les lignes en supprimant tous les caractère de saut de ligne ; echo réinsère la dernière nouvelle ligne, que tr avait supprimé en trop ; xargs -0 printf '%b' a pour effet de déséchapper les caractères de nouvelle ligne et de tabulation, cc compile le texte résultant et, finalement, ./bonjour exécute le programme.

Conclusion

J’ai créé cet utilitaire après de nombreuses heures de patentatage avec les solutions existantes. Je cherchais un format de document sémantique qui soit à la fois agréable à utiliser et qui s’intégre bien avec les utilitaires Unix. Je suis content de dire que cette recherche est maintenant terminée ; je suis satisfait de ma création.

Je ne prétends pas que ce programme et que les formats qu’il utilise soient d’une utilité universelle. On a vu dans le dernier exemple que les chaînes de commandes qui manipulent des documents plat(5) sont assez complexes. Cet exemple est même relativement simple dans son genre, puisqu’il ignore complètement le deuxième champ, celui des parenthèses, dont on n’aurait pas pu se passer si on n’avait pas préssuposé que le champ ne contiendrait qu’un seul domaine avec l’étiquette :doc:pre:. Aussi le format plat(5) est-il mieux adapté à être manipulé dans des scripts qu’à la ligne de commandes. On peut cependant abstraire une partie de la chaîne de commandes de cet exemple et en la plaçant dans un fichier compiler_c.

$ cat compiler_c
#!/bin/sh

grep '^:doc:pre:	' |
	cut -f3 |
	{ tr -d '\n'; echo; } |
	xargs -0 printf '%b' |
	cc -o "$1" -x c -
$ chmod +x compiler_c

On peut alors composer :

$ aplat <bonjour.aplat | ./compiler_c "bonjour" && ./bonjour
Bonjour tout le monde!

J’ai de nombreuses idées de programmes auxiliaires qui permettront de faciliter l’utilisation des formats aplat(5) et plat(5). Je leur dédierai certainement d’autres articles.

Voir aussi

=> Télécharger aplat(1)
=> Dépôt git du programme.

— ~ldp