5. Définition de la syntaxe des lignes : read_config_yy.y

5.1. Format du fichier

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.

5.2. Le fichier read_config_yy.y

%{
#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);
}

5.3. Les explications

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.

création est mise à disposition sous un contrat Creative Commons