Sur la lecture des fichiers de configuration

configurationparserhomoiconicitéerlanglisphaskell Par drksd et GuilOooo, le dimanche 16 août 2009, à 14:08

Dans cet article, nous allons aborder une méthode originale pour stocker des données de manière persistante, en utilisant les types natifs des langages concernés, sans devoir écrire un parser nous-mêmes.

Le « stockage persistant » veut dire que nous allons enregistrer des données pour notre application d'une exécution sur l'autre, et nous les consulterons - et éventuellement les mettrons à jour - à chaque lancement de celle-ci.

« En utilisant les types natifs » signifie que l'on manipulera directement des entiers, des listes, des couples ou d'autres valeurs, sans devoir les convertir en chaîne/depuis une chaîne manuellement.

Interéssés ? Bonne lecture !

Cet article m'a été inspiré par celui traitant de la configuration des applications erlang paru sur spawn_link. L'article en question est disponible ici.

Un problème : la configuration

Pratiquement toutes les applications peuvent être configurées par les utilisateurs : ceux-ci peuvent définir leurs préférences, qui seront enregistrées, puis utilisées à chaque démarrage de l'application pour s'adapter à l'utilisateur.

Un des moyens d'enregistrer la configuration est de la définir à l'intérieur même du code source du programme, mais cela implique que les utilisateurs doivent recompiler1 l'application à chaque fois qu'ils désirent changer leurs préférences. C'est la méthode utilisée par le WM (Window Manager) dwm, par exemple, où on édite un fichier C (config.h) pour le reconfigurer.

Pour la suite de cet article, nous allons prendre l'exemple d'un client IRC : pour fonctionner, il doit savoir à quel serveur se connecter, sur quel port et avec quel pseudo. Dans notre cas, devoir recompiler l'application à chaque fois que l'on souhaite se connecter à un nouveau serveur serait un poil contraignant, n'est-ce pas ?

Une solution à ce problème serait de demander à chaque fois ces données à l'utilisateur lorsqu'il lance le programme, mais cela reste toutefois assez lourd. En effet, même s'il arrive de changer de serveur ou de pseudo, on ne le fait pas non plus tous les jours, et taper le même serveur/pseudo à chaque connexion finira par devenir ennuyeux.

La troisième solution est l'utilisation de ce qu'on appelle des fichiers de configuration. Le principe est simple : l'utilisateur rentre une première fois ses préférences, puis on les sauvegarde dans un fichier. Par la suite, on retourne dans ce fichier chaque fois qu'on a besoin d'accéder à ces informations.

Cette solution a le mérite de laisser à l'utilisateur la liberté de modifier ces données sans toutefois l'obliger à les ressaisir à chaque utilisation du programme.

1 : À noter que cela ne vaut que pour les langages compilés, pour ce qui est des langages interprétés on peut toujours trouver les sources quelque part si on est motivé et les retoucher.

Mise en place pratique

En pratique, le problème qui se pose est celui du format de fichier. En d'autres termes, comment allons-nous organiser les informations au sein de notre fichier ? L'idéal serait d'avoir un format qui soit à la fois aisément modifiable par un humain et lisible par un ordinateur. Ce dernier point implique que l'on puisse facilement accéder aux données du fichier depuis le langage dans lequel on développe notre programme.

La plupart du temps, la solution retenue est simple, et les fichiers ressembleront à quelque chose du style :

# Ceci est un commentaire
# Je garde ici l'exemple du client IRC imaginaire et des données
# théoriques dont il peut avoir besoin.
host=irc.epiknet.org
port=6667
nick=Dark-Side

Il s'agit juste de vous montrer le format par un exemple, les vrais fichiers de configuration sont en réalité bien plus étoffés que celui-ci. Toutefois, cette syntaxe est très répandue pour les applications qui ont besoin d'une configuration simple, puisqu'une syntaxe de ce type est très facilement exploitable, le "parser" étant très facile à coder. En effet, comme dirait Octal :

« J'me fais pas chier, je split sur '=' »

Cependant, pour des fichiers un peu plus complexes ou volumineux que celui de notre exemple, la syntaxe des '=' présente un inconvénient de taille : le typage n'est pas fait automatiquement. En d'autres termes, c'est au programmeur de vérifier que le port est bien un nombre, puis le convertir, ou que le pseudo est bien une chaîne de caractères. De plus, si on utilise des valeurs un peu plus évoluées que de simples nombres, comme des couples ou des listes, il va falloir spécifier le format et écrire un parser.

On se retrouve au final à faire beaucoup de manipulation de chaînes assez répétitive et peu intéressante. Heureusement pour nous, certains langages sont là pour nous faciliter la vie. :)

Moi à mon langage je lui fais des bisous

La ressemblance entre cette chanson et celle des bisounours est pûrement fortuite.

Des gentils, des tout doux
Des géants, des tout fous
Un bisou sur la joue
Un bisou dans le cou
Car mon p'tit langage
Il adore les bisous

Plus sérieusement, il est possible, avec certains langages, de lire des données typées dans un fichier très facilement. Ce qui va grandement nous intéresser pour la gestion des fichiers de configuration. L'exemple le plus évident est fourni par les langages de la famille lisp et illustré par le très célèbre : « Code as data ».

Exemple en scheme

Dans cette partie, je présuppose que vous connaissez déjà brièvement le scheme, ou tout du moins les types qu'il propose (listes, couples, symboles, etc).

Comme vous le savez sûrement (ou pas, mais ce n'est pas grave :p ) scheme est un langage "lisp-like", tout comme l'est par exemple Common Lisp. En tant que représentant de cette famille, il permet de jouer avec les S-expressions.

On va donc pouvoir construire nos fichiers de configuration à base de s-expressions. Voici donc à quoi ressemblerait notre exemple précédent une fois adapté à cette nouvelle syntaxe :

;;; Ceci est un commentaire
;;; Je garde ici l'exemple du client IRC imaginaire et des
;;; données théoriques dont il peut avoir besoin
(host . "irc.epiknet.org")
(port . 6667)
(nick . "Dark-Side")

Surprise ! Le fichier est typé cette fois, et on a en outre accès à tous les types de scheme, comme ici les couples, les symboles, les chaînes de caractères et les entiers. En partant de là, on peut facilement utiliser des types évolués, comme des listes. Par exemple si on veut rajouter une liste de canaux à rejoindre automatiquement on aurait qu'à ajouter cette ligne :

(autojoin . (list "#sdz" "#zds"))

Bien, maintenant que notre fichier de configuration est écrit, encore faut-il pouvoir le lire. On ouvre tout d'abord notre fichier, en utilisant la fonction open-input-file, qui demande comme argument une chaîne de caractère : l'adresse du fichier. Ceci fait, on va utilise la fonction read pour lire une à une les s-expressions du fichier, jusqu'à rencontrer l'expression #<eof> qui signifie : « End of file », soit : « on est arrivé à la fin du fichier ». Exemple :

> (define file (open-input-file "config.scm"))
> (read file)
(host . "irc.epiknet.org")
> (read file)
(port . 6667)
...
> (read file)
#<eof>

Pour utiliser ces informations il suffira de binder le résultat d'un read à une variable, exemple :

> (define couple (read file))
>

Ce qui est tout de même nettement plus pratique que d'écrire notre propre parser de zéro ! Le seul détail gênant ici est que l'on ne peut pas lire tout le fichier d'un coup, nous sommes obligés de lire les termes un à un. On peut y pallier facilement avec une fonction récursive, mais c'est toujours un manque.

Un exemple surprenant : Erlang

C'est ici que je rejoins mon confrère de spawn_link.

Lisp, avec le principe du « Code as Data », est bien connu pour pouvoir stocker et lire des s-expressions comme on l'a fait, aussi le premier exemple n'a-t-il surpris personne. Par contre, le fait qu'Erlang ait cette capacité est peut-être un peu plus étonnant : même si ce n'est pas inconcevable, on ne s'y attendrait pas.

Quoiqu'il en soit, Erlang aussi nous permet de gérer la configuration, et même de le faire bien. On peut notamment lire des termes Erlang un à un, comme en Scheme, mais aussi - et surtout - lire l'intégralité du fichier en une seule fois. Tout d'abord, réécrivons le fichier de configuration avec la syntaxe Erlang :

%%% Ceci est un commentaire
%%% Je garde ici l'exemple du client IRC imaginaire et des
%%% données théoriques dont il peut avoir besoin
{host, "irc.epiknet.org"}.
{port, 6667}.
{nick, "Dark-Side"}.
{autojoin, ["#sdz", "#zds"]}.

Encore une fois, on a accès à tous les types du langages : couples, string, entiers, listes, atomes2 ...

Pour lire les termes un à un, on procède ainsi :

1> {ok, S} = file:open("config.cfg", read).
{ok,<0.36.0>}
2> io:read(S, '').
{ok, {host, "irc.epiknet.org"}}
3> io:read(S, '').
{ok, {port, 6667}
...
6> io:read(S, '').
eof
7> file:close(S)

À noter qu'ici vous pouvez voir la procédure pour ouvrir et fermer un fichier. Le principe étant le même qu'en scheme, les gens connaissant Erlang sauront enregistrer les données lues dans une variable et les exploiter - cette partie étant relativement peu intéressante, je ne la détaille pas ici.

Passons maintenant au point qui fait l'intérêt de la méthode Erlang : lire l'intégralité d'un fichier. Cette fois, plus besoin d'ouvrir et fermer le fichier manuellement, Erlang le fait pour nous, il suffit juste d'appeller la bonne fonction, qui va nous renvoyer un couple de la forme {ok, Liste}3 si tout s'est bien déroulé et un couple {error, Raison} sinon.

Par exemple :

8> file:consult("config.cfg").
{ok, [{host, "irc.epiknet.org"}, {port, 6667}, {nick, "Dark-Side"},
{autojoin, ["#sdz", "#zds"]}]} 
9>

Plutôt sympathique, n'est-ce pas ? :)

Un troisième exemple, en Haskell

En langage Haskell, on dispose de deux fonctions pour faire notre travail : ce sont « show » et « read ». Elles prennent respectivement n'importe quelle valeur Haskell pour la transformer en chaîne, et inversement une chaîne qu'elle transforme en valeur Haskell. La première des choses à faire est de définir le type des valeurs que l'on va lire dans notre fichier :

data Champ = Pseudo String | Port Int | Serveur String
  deriving (Read,Show)

On définit ici un type champ, qui peut être : soit un pseudo (qui est une chaîne), soit un port (qui est un entier), soit un serveur (qui est une chaîne). Le « deriving Read » demande au compilateur que Champ appartienne à la classe4 Read. Pour simplifier à outrance, le compilateur va générer tout seul une fonction read, qui convertit une chaîne de caractères en Champ.

Le principe est le même pour Show, qui n'est pas vraiment utile dans notre cas. On s'en serivra pour visualiser le résultat de nos tests dans GHCi.

Maintenant, on écrit notre fichier de configuration, comme d'habitude :

[Pseudo "Dark-Side",
 Serveur "irc.epiknet.org",
 Port 6667]

Comme vous pouvez le voir, il s'agit d'une liste de Champ, le type que nous avons défini juste avant. Il est à noter que si Haskell sait lire les valeurs Champ (par exemple), il sait automatiquement lire les listes de Champs également, avec la syntaxe habituelle des listes.

On peut maintenant tester notre configuation dans GHCi :

*Prelude> :l Champ.hs
[1 of 1] Compiling Main             ( Champ.hs, interpreted )
Ok, modules loaded: Main.
*Main> monFichier <- readFile "configuration.txt"
*Main> let configuration = (read monFichier)::[Champ]
*Main> configuration
[Pseudo "Dark-Side",Serveur "irc.epiknet.org",Port 6667]
*Main> :t configuration
configuration :: [Champ]
*Main> (configuration !! 0)
Pseudo "Dark-Side"
*Main>

La première commande charge le fichier Champ.hs, qui contient uniquement la déclaration du type champ. La deuxième lit le contenu du fichier configuration.txt (donné ci-dessus) et le stoque une chaîne appelée « monFichier ».

Ensuite vient la partie intéressante : on appelle fonction read sur la chaîne monFichier. Le « :: [Champ] » signifie que le résultat doit avoir pour type « liste de Champ ». Bien que les types soient habituellement inférés en Haskell, on le spécifie manuellement ici pour indiquer à read comment il doit parser la chaîne. Le résultat est nommé « configuration ».

On peut alors jouer avec : l'afficher (bien qu'à l'affichage, la configuration soit identique au contenu du fichier, ce qui est cohérent mais ne permet pas de voir grand chose), observer son type, qui est bien une liste de champs, ou encore extraire le premier élément, qui se révèle être le Pseudo.

Cette technique peut être étendue à tous les types dont vous pouvez spécifier comment les lire depuis une chaîne - ou dont le compilateur peut deviner tout seul comment faire, comme dans notre exemple simpliste.

Par ailleurs, dans le cadre d'un programme Haskell, la question « peut-on lire le fichier d'un coup ou bien champ par champ » n'as plus vraiment de sens. Soit on définira soit une liste de champs, soit on lira le fichier ligne à ligne avant de convertir chaque ligne.

2 : L'équivalent des symboles de scheme

3 : Où Liste est la liste de tous les termes du fichier

4 : à ne pas confondre avec les classes en Java. Ici, grossièrement, le sens du mot « classe » est plus proche des « interfaces » Java.

Conclusion

Vous avez donc pu voir différentes possibilités pour enregistrer la configuration de vos applications, et, plus généralement, enregistrer facilement n'importe quel type natif de nos langages dans un fichier - et les récupérer facilement.

Ceci nous permet de profiter de la détection des erreurs de nos langages favoris, qui signalera automatiquement toute erreur de syntaxe dans le fichier de configuration sans que nous n'ayons rien à faire, ce qui nous évite de devoir tout implémenter à la main ou passer par une bibliothèque comme on le ferait en PHP ou en C.

La liste des langages utilisés (pour nous exemples) n'est toutefois pas exhaustive. Plus généralement tous les langages homoiconiques5 offrent ce genre de service. On pourra ainsi citer Rebol, Io, etc...

La syntaxe des fichiers qui en résulte peut paraître assez étrange aux utilisateurs novices, mais, comme vous avez pu le voir dans nos exemples, elle demeure assez simple pour qu'une personne un peu curieuse puisse l'éditer facilement.

Pour finir, je vous invite à aller regarder l'article de spawn_link qui met en place un système de configuration bien plus poussé que ce qu'on a pu faire ici.

5 : « l´homoiconicité est une propriété de certains langages dans lesquels la principale représentation des programmes est aussi une structure de données d'un type primitif du langage. ». Cf: Wikipedia

source

#1 Par gnomnain, le 16 Août 2009 à 20h01

En Haskell, on peut aussi convertir en plein de formats louches (comme json) avec les trucs de Data.Data (faut rajouter un deriving Data quand on définit le type je pense). En cherchant 30 secondes sur hackage, j'ai trouvé ça pour json : http://hackage.haskell.org/package/json (mais j'ai pas de ghc sous la main donc je peux pas tester).

#2 Par gnomnain, le 16 Août 2009 à 20h13

Ah et mon commentaire du dessus ressemble à rien, et puis vous puez parce que je sais pas comment on met un lien dans un commentaire.

#3 Par Cygal, le 16 Août 2009 à 20h33

Franchement on s'en branle de savoir si on peut 'lire tout d'un coup' ou non. Si t'as trouvé que ça comme différence entre Scheme, Erlang et Haskell.

Enfin on reste un peu sur sa faim là. L'écriture n'est pas traitée du tout.

Et quant au stockage dans le programme en lui-même, ça semble être le point le plus intéressant. Les données doivent être accessible partout en lecture mais pas en écriture, comment gérer ça ? Un simple module ? On passe la structure de conf aux fonctions qui vont bien ? Comment gérer les accès concurrents ? Quelles différences etnre les langages ?

As-tu déjà utilisé ce système pour des vrais applications ?

gnomnain> On peut <strong>pas</strong>.

#4 Par Dark-Side, le 16 Août 2009 à 20h45

Bon, je n'ai rien à répondre au premier point, je vais m'occuper de la suite. Permet moi de te citer :

« Enfin on reste un peu sur sa faim là. L'écriture n'est pas traitée du tout.

Et quant au stockage dans le programme en lui-même, ça semble être le point le plus intéressant. Les données doivent être accessible partout en lecture mais pas en écriture, comment gérer ça ? Un simple module ? On passe la structure de conf aux fonctions qui vont bien ? Comment gérer les accès concurrents ? Quelles différences entre les langages ? »

C'est exact. On a choisi de ne pas traiter ça parce que (comme le titre l'indique) on ne souhaitait parler que de la lecture de tels fichier et des facilités offertes par certains langages pour cela. L'exploitation de ces données et l'écriture seront (peut-être) traités dans un article futur. Mais je ne vais pas trop m'avancer. Quoiqu'il en soit, Erlang offre aussi quelques outils assez cools de ce côté là.

Et pour ta dernière question : « As-tu déjà utilisé ce système pour des vrais applications ? » Honnêtement ? Non. Pas pour le moment. Mais c'est justement parce que j'y pensais que j'ai vu l'article de spawn_link et que j'ai écris cet article. Je pensais utiliser cette fonctionnalité non pas comme fichier de configuration mais pour stocker les méta-données relatives aux articles de BHM (dans l'hypothèse où on mettrait en place un système avec darcs). On aurait alors quelque chose comme :

{auteurs, ["Dark-Side", "GuilOooo"]}. {tags, ["lisp", "erlang", "haskell"]}. {date, "16/08/09"}. {contenu, "./contenu.text"}.

PS: Non on ne peut pas faire de lien dans les commentaires (oui oui, un jour ça viendra)

#5 Par Âne Onyme, le 16 Août 2009 à 21h41

Bordel et ça parle même pas d'Emacs quoi. Ya plein d'applis Elisp qui stockent leurs méta-données directos comme ça. Bon après pour la conf en général, on modifie directement les variables donc c'est pas exactement pareil non plus...

#6 Par rz0, le 16 Août 2009 à 21h42

Et merde j'ai oublié de foutre un pseudo.

#7 Par bluestorm, le 16 Août 2009 à 22h56

En Haskell on utiliserait plutôt un type produit qu'un type somme ici, ça colle mieux à la structure du problème (tu veux ces trois informations une seule fois, et pas un nombre indéterminé, potentiellement nul, de chaque). Le type somme peut aussi avoir son intérêt (pour des trucs genre "décrire un prédicat sur la structure"), et dans certains cas t'es poussé à avoir en même temps le type produit et le type somme correspondant, mais le type produit reste plus naturel et plus utile.

Par ailleurs c'est vrai que les langages homoiconiques permettent de faire ça, mais il me semble que ni Erlang ni Haskell ne sont homoïconiques : du code général dans la syntaxe habituelle ne constitue pas aussi une structure de donnée parsable telle quelle; évidemment, si on se restreint au subset du langage qui décrit les données et pas le code, c'est plutôt facile.

#8 Par Dark-Side, le 16 Août 2009 à 23h29

« il me semble que ni Erlang ni Haskell ne sont homoïconiques »

Certes et j'admets que la formulation est ambiguë. Toutefois quand j'ai évoqué les langages homoiconiques je faisais référence à lisp et je profitais de l'occasion pour citer Rebol et Io.


Pour rz0: effectivement avec emacs les fichiers de conf sont rédigés en elisp, et j'avais pensé à en parler. Sauf que pour emacs les fichiers sont "chargés", pas lu. C'est à dire que le code qui se trouve à l'intérieur est interprété. Là l'intérêt et justement que le code/les données soient stockées mais pas évaluées. Avec du code (en elisp donc) on peut trop facilement "foutre la merde". Je pense que mon choix à ce sujet (de ne pas évoquer le cas d'emacs quoi) est compréhensible. :)

#9 Par rz0, le 16 Août 2009 à 23h34

Je m'auto-quote : « Ya plein d'applis Elisp qui stockent leurs méta-données directos comme ça. » Tak tak. Genre dans Emacs-23, le fichier .dir-locals.el qui contient les vars locales au dossier est une liste qui est parsée, et paf, ça fait des chopcaps.

#10 Par GuilOooo, le 18 Août 2009 à 17h41

@Bluestorm : ce serait effectivement mieux d'utiliser un type produit, mais j'ai hésité parce qu'il faut préciser les champs dans l'ordre spécifé sous peine de bug, ce qui n'est pas très pratique pour un fichier de configuration.

Comme je voulais simplement montrer la classe Read, j'ai utilisé cet exemple simpliste.

Après, on peut manipuler des arbres syntaxiques via le langage lui-même, avec Template Haskell, mais il est vrai que c'est nettement moins accessible qu'en Lisp, par exemple.

#11 Par Asgeir, le 18 Août 2009 à 22h37

Tak tak. GNU Emacs 23 plz.

#12 Par zqvoNhKSqsi, le mercredi 05 janvier 2011, à 09:01

OdP2Az <a href="http://fkbvxvduxxyp.com/">fkbvxvduxxyp</a>, [url=http://khiuoafhulkj.com/]khiuoafhulkj[/url], [link=http://rhqdcycfmxip.com/]rhqdcycfmxip[/link], http://yhrmysixflhy.com/

#13 Par SSVHRnZJow, le mercredi 05 janvier 2011, à 18:01

QB5DG6 <a href="http://xbhpivthykzr.com/">xbhpivthykzr</a>, [url=http://jrsxgldcgqbp.com/]jrsxgldcgqbp[/url], [link=http://bzsngakjhmqv.com/]bzsngakjhmqv[/link], http://rjpnpxsoykpd.com/

Poster un commentaire