Copyright © 2001 Yves Mettier
L'article original se trouve sur http://ymettier.free.fr/articles_lmag/.
Article publié dans le numéro 29 (juin 2001) de GNU/Linux France Magazine
Table des matières
Toute application propre dispose d'un fichier de configuration au moins. Elle doit pouvoir le lire, l'écrire, et proposer un moyen facile à l'utilisateur d'y accéder. Pour ces trois objectifs, une étude doit être faite afin de déterminer quelle solution utiliser. Voici les résultats pour une application ni trop simple ni trop compliquée.
Si le fichier est en mode texte, l'utilisateur pourra utiliser son éditeur de texte favori pour l'éditer. Sinon, il faudra une application spécifique pour y accéder. C'est souvent le cas de l'application que l'on programme, qui donne un moyen d'accéder au fichier de configuration (menu préférences) afin de rendre les options modifiables d'une manière plutôt conviviale. Mais si le fichier est corrompu et empêche le lancement de l'application, et que par dessus le marché c'était le seul moyen pour l'utilisateur d'atteindre le fichier de configuration, on se trouve dans une situation où il faut tout réinstaller.
Un format de fichier de configuration intermédiaire est le XML qui est en mode texte, et qui dispose d'outils pour le lire (cf LMAG 27 et son dossier spécial XML.)
Si l'application n'est pas distribuée, il est très facile pour le programmeur d'écraser le fichier de
configuration existant et de le remplacer par un nouveau que l'application écrira avec de simples
printf()
. Cette partie de l'application est relativement rébarbative à coder, mais une fois que
cela est fait, la convivialité est au rendez-vous aussi bien pour le programmeur que pour l'utilisateur.
Au vu de ce qui précède, nous avons déjà plus ou moins choisi un format de fichier texte. Il reste maintenant le format de ce fichier qui va conditionner son mode de lecture. Au premier abord, on a tendance à utiliser les fonctions C de base afin de lire le fichier. L'inconvénient majeur est qu'il est rébarbatif de coder des fonctions de vérification de la validité du format du fichier. Et s'il est corrompu, l'appli ne plantera pas forcément avant la fin de la lecture, mais peut-être après, pendant que des données sont traitées. On va donc éliminer le code basic et se servir d'outils qui font le travail de vérification.
Deux systèmes seront donc via lex & yacc avec un format de fichier quelconque, et via des fonctions xml (sax, dom...) avec un format de fichier xml.
Choisissons arbitrairement le format quelconque afin d'utiliser lex & yacc (lisez LMAG27 si vous préférez XML). Un avantage supplémentaire de l'utilisation de lex & yacc est que le format de fichier n'est pas figé. C'est à nous de l'inventer et donc de le rendre le plus convivial possible.
Les plus connus sont les formats à la windows : clé=valeur
; et à la apache : clé valeur(s)
. Choisissons le
format à la apache qui est plus logique dans le cas de plusieurs valeurs :
UnEntier 1 # un commentaire IntStr 2 bonjour Boolean True
Ces trois lignes constitueront notre fichier de configuration d'exemple, et nous saurons en tirer le contenu à la fin de l'article.
La syntaxe de ce fichier de configuration est facile : tout ligne commence par un mot-clé connu suivi des valeurs associées à ce mot-clé. Ou alors la ligne commence par un # pour signaler un commentaire.
Lex et yacc sont deux outils d'analyse de syntaxe (lex) et de grammaire (yacc). En d'autres termes, yacc va trouver des mots, demander à lex de les lui traduire, puis interpréter leur sens au vu de leur organisation.
En d'autres termes encore, yacc va lire la ligne IntStr 2 bonjour
. Puis il va demander à lex
ce que IntStr
veut dire, de même pour 2
et pour bonjour
. Lex va lui
répondre que IntStr
veut dire INTSTR
, que 2
est un entier et que
bonjour
est une chaîne de caractères. Et yacc sait ce que INTSTR
veut dire : une
ligne commençant par INTSTR
doit se continuer par un entier et par une chaîne de caractères.
Yacc sait donc qu'il doit attendre un entier puis une chaîne.
Le travail du programmeur est de définir ce qu'est IntStr
, un entier et une chaîne
quelconque. Puis il doit définir l'ordre de ces mots pour qu'ils aient une signification. Et enfin, quand il
a programmé lex et yacc pour reconnaître le vocabulaire et la grammaire, il ne reste plus qu'à récupérer ce
que lex et yacc ont reconnu afin de s'en servir par la suite dans son programme.
Pour bien enfoncer le clou, le helloworld de lex et yacc. La première application que l'on réalise avec lex et yacc est souvent une calculatrice 4 opérations. Ce helloworld mathématique se retrouve dans tous les cours sur lex et yacc en tant qu'illustration. Je me servirai de l'exemple juste pour signaler que lex sert à distinguer les nombres des signes et à rejeter tout autre symbole tel que les lettres. Yacc utilise ce que lex a reconnu afin d'avoir toujours nombre suivi de signe suivi de nombre, et de rejeter toutes les autres combinaisons tel que nombre puis signe, ou signe puis signe puis nombre etc. Enfin à l'intérieur du code qu'on fournit à yacc, on récupère les différents cas et pour chaque cas on effectue l'opération qu'il faut.
Pour ceux qui n'auraient pas fait de lex & yacc et qui s'y intéressent, c'est un petit exercice qui vaut
le détour. L'astuce, pour se simplifier le travail, est qu'il faut mettre la fonction main()
à
la place de la fonction init_config()
qui se trouve dans l'exemple plus bas. Et pas besoin de
fichier main.c
. Mais chut, je n'en dis pas plus...
En ce qui concerne lex et yacc, on en parle beaucoup, mais sur un système GNU, ce ne sont pas eux que
l'on utilise, mais leur implémentation GNU : flex
et bison -y
.
Le format du fichier pour flex est celui-ci :
définitions de mots-clés %% définitions des mots %% code C additionnel
%option nounput %{ #include "read_config_yy.h" %} separator [\t ]+ %% {separator} ; ^[#;].*$ ; ^{separator}*[Ii]nt[Ss]tr { yylval.string = yytext; return INTSTR;} ^{separator}*[Bb]oolean { yylval.string = yytext; return UNBOOLEEN;} ^{separator}*[Uu]n[Ee]ntier { yylval.string = yytext; return UNENTIER;} [Tt][rR][Uu][eE] { yylval.bool = 1; return BOOLEAN; } [Ff][Aa][Ll][Ss][eE] { yylval.bool = 0; return BOOLEAN; } [Yy][Ee][Ss] { yylval.bool = 1; return BOOLEAN; } [Nn][Oo] { yylval.bool = 0; return BOOLEAN; } [0-9]+ { yylval.val = atoi(yytext); return INTEGER; } \"[^"]*\" { yylval.string = yytext; return QSTRING;} '[^']*' { yylval.string = yytext; return QSTRING;} [^'" \t\n]+ { yylval.string = yytext; return STRING;} ^{separator}*\n ; {separator}*\n { return EOL; } <<EOF>> { return 0 ; } %%
Un peu d'explications car j'ai mis non pas du superflus, mais des choses intéressantes.
Tout d'abord, la première ligne indique que le fichier généré par flex ne comportera pas la fonction
unput
. Elle peut être génante pour la suite, donc exit la fonction.
Ensuite, le fichier read_config_yy.h
que nous n'écrirons pas est nécessaire ici. C'est bison
qui le génèrera pour nous. Notons que tout code encapsulé dans %{
et %}
est
recopié tel quel par flex. Attention à l'ordre des deux caractères : c'est toujours le signe %
qui précède l'accolade, qu'elle soit ouvrante ou fermante.
La définition de separator
nous évite de retaper ce code par la suite. C'est une sorte
d'alias pour la suite du fichier. Par contre, ce n'est pas là qu'on définit que separator
est
le séparateur. Ici, avant les %%
, on ne définit separator
qu'en tant qu'alias.
Puis, avant la reconnaissance des trois mots-clé, j'ai défini que separator
est un
séparateur. Eh oui, sinon, qui l'aurait dit a bison et à flex? Quand on définit le vocabulaire et la
grammaire, on définit tout, y compris le caractère de séparation!
Et du coup, dans la lignée des définitions évidentes mais qu'il faut quand même définir, on a sur ligne
suivante ce qu'est un commentaire, à savoir une ligne commençant par #
ou ;
.
Viennent ensuite les trois mots-clé qui nous intéressent. Ainsi que quelques autres mots-clé qui nous serviront pour le booléen. Notons qu'ils sont sensibles à la casse.
Et enfin nous avons les définitions les plus générales avec les entiers INTEGER
, les chaînes de
caractères entre guillemets QSTRING
(Q
pour Quotes) puis sans guillemets STRING
. Et c'est fini!
J'ai choisi ces noms moi-même; je l'indique car vu leur nom, ce n'était peut-être pas évident pour tout le monde.
Donc non, ces noms ne sont pas définis par défaut.
Nous compilerons ce fichier plus tard car nous avons besoin du fichier read_config_yy.h
généré par bison.
Voici le fichier read_config_yy.y
qui définit le format du fichier de config. Il a le format suivant :
Définitions des mots-clefs %% Définition de la syntaxe %% Code C additionnel.
%{ #include <stdlib.h> #include <stdio.h> #include <string.h> #include "my_config.h" static int unentier; static int intstr1; static char *intstr2; static int unbooleen; %} %union { int val; char *string; int bool; } %token <string> STRING %token <string> QSTRING %token <bool> BOOLEAN %token <val> INTEGER %token UNENTIER %token INTSTR %token UNBOOLEEN %token EOL %% config_file: lines ; lines: one_line lines | one_line ; one_line: line_UNENTIER EOL | line_INTSTR EOL | line_UNBOOLEEN EOL | EOL ; line_UNENTIER: UNENTIER INTEGER { unentier = $2; } line_INTSTR: INTSTR INTEGER STRING {{ intstr1 = $2; if(intstr2) free(intstr2); intstr2 = strdup($3); }} | INTSTR INTEGER QSTRING {{ intstr1 = $2; if(intstr2) free(intstr2); intstr2 = strdup($3 + 1); intstr2[strlen(intstr2)-1] = 0; }} ; line_UNBOOLEEN: UNBOOLEEN BOOLEAN { unbooleen = $2; } %% int yyerror (char *s) { printf ("Erreur de lecture du fichier de config : %s\n", s); exit (0); return (0); } MYCONFIG * init_config (char *config_file_name) { MYCONFIG *myconfig = NULL; FILE *rcfile; rcfile = fopen (config_file_name, "r"); if (rcfile) { unentier = 0; intstr1 = 0; intstr2 = NULL; unbooleen = 0; /* lit le fichier de config */ yyrestart (rcfile); yyparse (); fclose (rcfile); myconfig = (MYCONFIG*) malloc (sizeof (MYCONFIG)); myconfig->unentier = unentier; myconfig->intstr1 = intstr1; myconfig->intstr2 = intstr2; myconfig->unbooleen = unbooleen; } else { printf ("Impossible d'ouvrir le fichier de config\n"); } return (myconfig); }
Au début du fichier, on inclus des #include
et des définitions de variables statiques. Ce
sont ces variables qui vont contenir le fichier de configuration, lors de son traitement.
L'union, suivie des définitions des %token
, servent à définir les types C des différents
mots. Effectivement, pour l'instant, nous n'avions mis qu'une tonne de noms descriptifs dans le fichier
read_config_lex.l
, mais il n'y avait pas de C. Voila qui est fait!
Ensuite, entre les %%, se trouve la grammaire du parser.
La première règle sert à lire le fichier en entier. Elle a besoin de la règle lines
qui
suit.
Cette règle lines
définit l'ensemble des lignes, à savoir
soit une ligne définie par one_line
,
soit récursivement une ligne one_line
et l'ensemble des lignes lines
.
C'est cette récursion qui permet de lire toutes les lignes.
Puis one_line définit chaque type de ligne possible :
line_UNENTIER
line_INTSTR
line_UNBOOLEEN
Notons que les commentaires étant exclus par le lexer qui ne renvoit rien dans ce cas, le parser n'a pas à traiter ce genre de lignes.
La suite se comprend facilement : un ligne est constituée
de mots-clé. Et logiquement, le premier mot-clé est celui qui définit les besoins pour la suite; le(s) suivant(s) défini(ssen)t les
types des mots trouvés. Le premier mot, qui ne nous sert plus après, est contenu dans la variable $1
, le
suivant qui nous sert dans la variable $2
etc. Remarquez que pour le cas INTSTR INTEGER
QSTRING
, je prends la chaîne au second caractère et non pas au premier, et je supprime le dernier,
sans aucune vérification que ces caractères existent. En d'autres termes, je ne prend pas le premier
caractère qui est soit un guillemet soit une apostrophe (nous n'avons pas défini d'autre caractère dans
read_config_lex.l
pour les chaînes QSTRING
).
Effectivement, l'avantage du système est que je
suis certain à cet endroit d'avoir une telle chaîne, sinon le parser ne serait pas entré dans ces lignes de
code. Donc la vérification a déjà été faite; pas besoin de la refaire.
Enfin, après le dernier %%
se trouve du code C additionnel. C'est là que j'ai mis une sorte
d'interface pour cacher la lecture du fichier de configuration quand on voudra s'en servir. En effet, dans
les autres fichiers source du programme, on appellera la fonction init_config
qui renverra la
configuration de manière transparente.
La fonction init_config
est intéressante. Elle commence par ouvrir le fichier de
configuration. Puis elle initialise les variables si cela a bien marché. C'est ici qu'on place les valeurs
par défaut si les lignes correspondantes ne sont pas trouvées dans le fichier de config. Attention, le
parser ne trouvera aucune faute de syntaxe s'il manque une ligne. Il n'en trouvera que s'il trouve
une ligne qui ne respecte aucun format connu. Pour détecter si une ligne est manquante et agir en
conséquence, il faut initialiser la variable associée à la ligne à une valeur qu'elle ne peut pas prendre en
temps normal. Par exemple -4
pour le booléen ou NULL
pour la chaîne de caractères.
Et à la fin, il faut tester si cette variable est toujours dans un état impossible. Un autre moyen de faire
est d'initialiser une variable supplémentaire pour chaque variable et de la mettre à zéro. Ensuite, il faut
programmer le parser pour que quand il modifie une variable, il incrémente sa variable associée. A la
fin, si une variable associée est à zéro, c'est que la variable n'a pas été fixée. Si la variable associée
est à deux ou plus, c'est qu'il y a soit redondance dans les lignes du fichier de configuration et c'est la
dernière qui a été prise en compte, soit c'est qu'il y a un bug dans le parser.
Ensuite, yyrestart()
permet d'indiquer au parser qu'il ne doit pas lire sur l'entrée
standard comme c'est le cas par défaut, (pour la calculatrice, ne pas mettre ce yyrestart()
:
le but est de lire sur l'entrée standard) mais dans le fichier décrit par rcfile
. Puis
yyparse
fait tout le boulot. Et il ne reste qu'à fermer le fichier de configuration.
Si l'on est arrivé là, c'est que tout s'est bien passé et que les variables sont affectées aux bonnes valeurs (sauf s'il manquait des lignes dans le fichier de configuration. Mais ce point a été traité plus haut). Il ne reste plus qu'à les mettre dans une structure et à renvoyer cela au programmeur.
S'il y a eu un problème, c'est la fonction yyerror
qui est appelée, et elle fait quitter le
programme car j'ai mis un appel à exit()
dedans. Il peut être sympathique de générer une belle
fenêtre pour avertir l'utilisateur que son fichier de configuration est corrompu.
Le fichier lexer est généré par flex read_config_lex.l
. Il génère un fichier
lex.yy.c
qu'il est préférable de renommer en read_config_lex.c
pour avoir un nom
moins générique..
Le fichier parser est généré par bison -y -d read_config_yy.y
. De même, il est
préférable de renommer y.tab.c
en read_config_yy.c
et y.tab.h
en
read_config_yy.h
. D'ailleurs, le fichier source généré par flex
necéssite le fichier read_config_yy.h
: c'est ce que l'on a mis explicitement au début de
read_config_lex.l
dans une ligne avec #include "read_config_lex.l"
.
Ensuite, il ne reste qu'à compiler les fichier C comme d'habitude :
gcc -c read_config_lex.c gcc -c read_config_yy.c
Remarque : au début du fichier read_config_yy.y
se trouve une ligne #include "my_config.h"
.
Ce fichier my_config.h
ne contient qu'une définition de la structure que renvoit la fonction
init_config()
. Ce fichier contient aussi le prototype de la fonction init_config()
.
C'est un fichier d'en-tête général qu'il convient d'avoir séparément car toute fonction qui aura besoin de
la configuration du programme aura besoin de la définition de cette structure. Voici donc ce fichier :
typedef struct { int unentier; int intstr1; char *intstr2; int unbooleen; } MYCONFIG; MYCONFIG *init_config (char *config_file_name);
Nous avons beaucoup travaillé, mais il manque le principal; le programme de démonstration. Le voici; je ne le commente pas car il est simple à comprendre.
#include <stdio.h> #include <stdlib.h> #include "my_config.h" int main () { MYCONFIG *myconfig; myconfig = init_config ("configfile"); if (myconfig) { printf ("'%d'\n'%d' '%s' \n'%s'\n\n", myconfig->unentier, myconfig->intstr1, myconfig->intstr2, myconfig->unbooleen ? "TRUE" : "FALSE"); } exit (1); }
Il ne reste qu'à compiler main.c
et à le lier aux autres objets :
gcc -c main.c gcc -o demo main.o read_config_lex.o read_config_yy.o -lfl
Une remarque importante : il faut lier le programme de démonstration à la librairie libfl
, et
pour une compilation sans erreurs, mettre ce -lfl
après les objets.
Voici mon fichier de configuration, qui s'appelle configfile
.
Ce nom est codé en dur dans main.c
. Ce n'est pas propre du tout, mais cela simplifie amplement le code.
Dans un programme plus propre, il faudrait chercher ce fichier dans un sous-répertoire de l'utilisateur, et sinon
dans un répertoire système contenant un fichier de configuration générique. Il faudrait aussi tester que le fichier est bien
un fichier et non pas un répertoire. Et encore tester qu'il s'ouvre bien... Cela ne rentre pas dans le cadre de
cet article.
Le fichier de configuration :
# un commentaire Intstr 3 'bonjour le monde' boolean yes unEntier 13
Notons au passage que le nombre d'espaces n'a pas d'importance, que l'ordre des lignes n'a pas d'importance non plus, et que l'on peut mettre des commentaires où l'on veut.
De plus, ce fichier diffère au niveau contenu du fichier du début de l'article. Ce fichier qu'on avait plus haut doit également marcher!
Pour vous éviter d'avoir trop à taper pour tout recompiler à la moindre erreur, voici mon fichier Makefile
:
all: read_config_yy.h main.o read_config_lex.o read_config_yy.o gcc -o toto main.o read_config_yy.o read_config_lex.o -lfl clean: rm -f *.o rm -f read_config_lex.c read_config_yy.c read_config_yy.h rm -f toto read_config_lex.o: read_config_lex.c gcc -c ${CFLAGS} read_config_lex.c read_config_yy.o: read_config_yy.c gcc -c ${CFLAGS} read_config_yy.c read_config_lex.c: read_config_lex.l flex read_config_lex.l ; mv -f lex.yy.c read_config_lex.c read_config_yy.c: read_config_yy.y bison -y -d read_config_yy.y ; mv -f y.tab.c read_config_yy.c; mv -f y.tab.h read_config_yy.h read_config_yy.h: read_config_yy.y bison -y -d read_config_yy.y ; mv -f y.tab.c read_config_yy.c; mv -f y.tab.h read_config_yy.h
Rappel : en début de ligne de commande d'un Makefile
on n'a jamais d'espaces mais des
tabulations. Pour les gens qui ne me croiraient pas ou qui voudraient en savoir plus, l'éditeur favori des
linuxiens propose un livre Makefile d'excellente qualité.
Ne passons pas ce point sous silence : il n'est pas si facile d'inclure des fichiers .l
et
.y
dans les fichiers de configuration d'autoconf et automake.
Dans le fichier configure.in
, il y a peu à faire : il suffit juste d'ajouter les lignes suivantes :
dnl Checks for programs. dnl ==================== AM_PROG_LEX AC_PROG_YACC dnl Checks for libraries. dnl ===================== AC_CHECK_LIB(fl, yywrap)
Ces lignes vérifient la présence des programmes flex et bison ou compatibles.
Par contre, dans le fichier Makefile.am
qui se trouve dans le même répertoire que les
fichiers sources, il y a plus de travail.
Premièrement, il faut ajouter au début une ligne :
YACC=@YACC@ -d
Il faut en effet ajouter l'option -d
à yacc/bison pour qu'il génère le fichier d'en-têtes read_config_yy.h
.
Puis on ajoute dans appli_SOURCES
les fichier read_config_lex.l
et
read_config_yy.y
. Il ne faut surtout pas ajouter les fichier C générés à partir de ces deux
fichiers dans la liste. Attention : il est obligatoire de mettre le fichier read_config_yy.y
AVANT le fichier
read_config_lex.l
dans cette liste car il doit être compilé avant : c'est à l'aide de ce fichier qu'on génère
le fichier d'en-têtes read_config_yy.h
dont le fichier read_config_lex.l
a besoin.
C'est plus loin qu'on ajoute les noms des fichiers C générés :
CLEANFILES = read_config_lex.c read_config_yy.c
En effet, ces fichiers ne font pas partie des sources car ils sont générés lors de la compilation. Par
contre, si l'on fait un make dist
, vu que ce sont des fichiers avec une extension
.c
, ils feront partie du package. Cela est préférable car une personne voulant recompiler
l'appli n'aura pas besoin d'installer flex et bison ou compatibles pour la recompiler. Dans ce
but, il est nécessaire d'ajouter le fichier read_config_yy.h
à la liste des fichiers à conserver :
EXTRA_DIST = configfile read_config_yy.h
J'ai aussi mis le fichier configfile
dans la liste pour qu'il ne soit pas perdu si l'on fait un
make distclean
!
Et voilà, il ne me reste plus qu'à vous livrer mon fichier configure.in
qui m'a servi pour
faire les tests :
dnl Process this file with autoconf to produce a configure script. AC_INIT(src/my_config.h) AM_INIT_AUTOMAKE(demo_lexyacc, 0.1) dnl Checks for programs. AC_PROG_CC AC_PROG_YACC AM_PROG_LEX dnl Checks for libraries. AC_CHECK_LIB(fl, yywrap) dnl Checks for header files. AC_HEADER_STDC dnl Checks for typedefs, structures, and compiler characteristics. dnl Checks for library functions. AC_CHECK_FUNCS(strdup) AC_OUTPUT(Makefile src/Makefile)
Et le fichier Makefile.am
dans le répertoire src/
n'est plus si compliqué
maintenant qu'il a été expliqué :
YACC=@YACC@ -d bin_PROGRAMS = demo_lexyacc demo_lexyacc_SOURCES = main.c read_config_yy.y read_config_lex.l my_config.h CLEANFILES = read_config_lex.c read_config_yy.c EXTRA_DIST = configfile read_config_yy.h
Avant de finir cet article, j'aimerais signaler un projet de plus grande envergure que celui qui nous a servi d'exemple. C'est GTKtalog, qui lit son fichier de configuration de cette manière.
Les fichiers config_lex.l
et config_parse.y
sont forcément plus longs, mais
n'en sont pas moins intéressants. De plus, on peut y trouver une astuce pour scanner une ou plusieurs
chaînes de caractères, ce qui est intéressant lorsqu'on doit donner par exemple une liste de chemins. Cette
astuce est la même pour les entiers, mais elle n'est utilisée que pour les chaînes dans GTKtalog.
Etant le coordinateur de GTKtalog (qui ne s'en doutait pas à ce point?), je me suis donc très
fortement inspiré de mes précédents développements. Cela vous permettra, si vous êtes intéressés, de décoder
le lexer et le parser de GTKtalog plus facilement afin d'aller plus loin dans l'utilisation de lex et yacc
dans le cadre de la lecture d'un fichier de configuration. Par contre, tout le code de cet article est indépendant et se trouve dans une
mini application qui sera présente, je l'espère, sur le CD, sous le nom poëtique de demo_lexyacc
:)
flex : man flex et http://www.gnu.org/manual/flex-2.5.4/flex.html ;
bison : man bison et http://www.gnu.org/manual/bison/bison.html ;
GTKtalog : http://gtktalog.sourceforge.net.
© 2001 Yves Mettier