Retrouvez cet article dans : Linux Magazine 89
La dissection de GLib nous amène ce mois-ci à étudier le code de l’analyseur de ligne de commande. Nous alternons un peu avec les structures de données vues dans les numéros précédents. GLib n’est pas qu’une boite d’outils dédiés aux structures de données !
Introduction
Historiquement, l’analyse de la ligne de commande était réalisée avec les fonctions du C, d’où une multitude de formats possibles. Certains utilisaient des tirets pour indiquer un argument, d’autres un slash, d’autres encore rien du tout. Souvent, pour ne pas dire presque toujours, les arguments devaient être indiqués dans l’ordre défini par le programme, ne laissant aucune souplesse à l’utilisateur.
Pour faciliter la tâche aux programmeurs, le jeu de fonctions dont getopt() est la plus connue est apparu. La façon d’indiquer les arguments s’est normée : les options étaient indiquées sous la forme d’un tiret suivi d’une lettre. Ceci n’était pas très causant et tout le monde n’adopta pas ce jeu de fonctions. GNU a tenté d’améliorer les choses avec getopt_long() qui permet d’utiliser les options longues, celles commençant par un double tiret. Cette fonction n’est cependant pas vraiment plus souple que la getopt() originale.
Par ailleurs, certains développèrent une bibliothèque dédiée à l’analyse de la ligne de commande, popt, utilisée entre autres par le logiciel de gestion de paquets rpm. L’analyseur de Glib se veut en fait un outil plus simple pouvant remplacer popt.
Remarque :
Cet article est basé sur la version 2.12.4 de Glib.
Fonctionnalités
Jetons un coup d’œil à la documentation de GLib et de ce fameux analyseur de ligne de commande. Nous y trouvons trois structures de données : GOptionContext, GOptionGroup et GOptionEntry. Vu leurs noms, nous pouvons déjà deviner que nous allons décrire chacune de nos options dans des GOptionEntry, que nous allons grouper par GOptionGroup, et que nous allons insérer le tout dans un seul et unique GOptionContext.
Nous allons donc créer un jeu d’options principales et un autre jeu d’option secondaire :
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <string.h>
04
05 #include <glib.h>
06
07 static gboolean o_debug=FALSE;
08 static gboolean o_verbose=FALSE;
09 static gint o_nb=1;
10 static gchar *o_chaine=NULL;
11 static gchar *o_fichier=NULL;
12
13 static GOptionEntry mes_options_principales[] =
14 {
15 { «debug», 'd', 0, G_OPTION_ARG_NONE, &o_debug, «Activer le mode debug», NULL },
16 { «verbose», 'v', 0, G_OPTION_ARG_NONE, &o_verbose, «Activer le mode blabla», NULL },
17 { “nb”, 'n', 0, G_OPTION_ARG_INT, &o_nb, “Un nombre”, “N” },
18 { “chaine”, 0, 0, G_OPTION_ARG_STRING, &o_chaine, “Une chaîne de caractères», «plouf» },
19 { NULL }
20 };
21
22 static GOptionEntry mes_options_secondaires[] =
23 {
24 { «fichier», 'f', 0, G_OPTION_ARG_FILENAME, &o_fichier, «Un nom de fichier», NULL },
25 { NULL }
26 };
27
Il s’agira ensuite de les déclarer :
28 int main(int argc, char**argv) {
29
30 GError *error = NULL;
31 GOptionContext *context;
32 GOptionGroup *groupe_secondaire;
33
34 /* Création du contexte et ajout des entrées principales */
35 context = g_option_context_new («Programme de test d'analyse de ligne de commande»);
36 g_option_context_add_main_entries (context, mes_options_principales, NULL);
37
38 /* Création d'un groupe et ajout des entrées secondaires dans ce groupe */
39 groupe_secondaire = g_option_group_new(«fichiers», «Gestion des fichiers», «Options dédiées aux fichiers», NULL, NULL);
40 g_option_group_add_entries(groupe_secondaire, mes_options_secondaires);
41
42 /* Ajout du groupe dans le contexte */
43 g_option_context_add_group (context, groupe_secondaire);
44
Analysez maintenant argc et argv :
45 /* Analyse de la ligne de commande */ 46 g_option_context_parse (context, &argc, &argv, &error); 47 48 /* Affichage de choses et d'autres... */ 49 printf(«debug=%s verbose=%s nb=%d chaine='%s'\n», o_debug?»TRUE»:»FALSE»,o_verbose?»TRUE»:»FALSE», o_nb, o_chaine); 50 printf(“fichier='%s'\n”, o_fichier); 51
Un peu de nettoyage avant de finir ne fait jamais de mal !
52 /* Nettoyage */ 53 g_option_context_free(context); 54 55 /* Au revoir, et merci pour le poisson ! */ 56 exit(EXIT_SUCCESS); 57 }
Compilez et exécutez ce programme :
$ gcc `pkg-config --cflags --libs glib-2.0` test.c -o t $ ./t debug=FALSE verbose=FALSE nb=1 chaine='(null)' fichier='(null)' $ ./t --help Usage: t [OPTION...] Programme de test d'analyse de ligne de commande Help Options: -?, --help Show help options --help-all Show all help options --help-fichiers Options d?di?es aux fichiers Application Options: -d, --debug Activer le mode debug -v, --verbose Activer le mode blabla -n, --nb=N Un nombre --chaine=plouf Une cha?ne de caract?res $ ./t --help-all Usage: t [OPTION...] Programme de test d'analyse de ligne de commande Help Options: -?, --help Show help options --help-all Show all help options --help-fichiers Options d?di?es aux fichiers Gestion des fichiers -f, --fichier Un nom de fichier Application Options: -d, --debug Activer le mode debug -v, --verbose Activer le mode blabla -n, --nb=N Un nombre --chaine=plouf Une cha?ne de caract?res $ ./t --debug -n 3 --fichier='/bin/ls' debug=TRUE verbose=FALSE nb=3 chaine='(null)' fichier='/bin/ls'
Les structures
Attaquons les choses sérieuses. Que contiennent ces fameuses structures ? Ouvrez les fichiers glib/goptions.c et glib/goption.h.
GOptionContext
01 typedef struct _GOptionContext GOptionContext;
01 struct _GOptionContext
02 {
03 GList *groups;
04
05 gchar *parameter_string;
06 gchar *summary;
07 gchar *description;
08
09 GTranslateFunc translate_func;
10 GDestroyNotify translate_notify;
11 gpointer translate_data;
12
13 guint help_enabled : 1;
14 guint ignore_unknown : 1;
15
16 GOptionGroup *main_group;
17
18 /* We keep a list of change so we can revert them */
19 GList *changes;
20
21 /* We also keep track of all argv elements
22 * that should be NULLed or modified.
23 */
24 GList *pending_nulls;
25 };
Nous ne pouvons pas encore comprendre les tenants et aboutissants de cette structure. Remarquez cependant que nous avons ligne 16 notre groupe principal et ligne 3 une liste de groupes supplémentaires. La suite est éclairée par la lecture de g_option_context_new() :
01 GOptionContext *
02 g_option_context_new (const gchar *parameter_string)
03
04 {
05 GOptionContext *context;
06
07 context = g_new0 (GOptionContext, 1);
08
09 context->parameter_string = g_strdup (parameter_string);
10 context->help_enabled = TRUE;
11 context->ignore_unknown = FALSE;
12
13 return context;
14 }
Cette fonction ne fait qu’allouer de la mémoire (ligne 7) et initialiser trois champs (lignes 9 à 11). Passons à la suite. Nous reviendrons à cette structure plus loin.
GOptionGroup
01 typedef struct _GOptionGroup GOptionGroup;
01 struct _GOptionGroup
02 {
03 gchar *name;
04 gchar *description;
05 gchar *help_description;
06
07 GDestroyNotify destroy_notify;
08 gpointer user_data;
09
10 GTranslateFunc translate_func;
11 GDestroyNotify translate_notify;
12 gpointer translate_data;
13
14 GOptionEntry *entries;
15 gint n_entries;
16
17 GOptionParseFunc pre_parse_func;
18 GOptionParseFunc post_parse_func;
19 GOptionErrorFunc error_func;
20 };
Cette structure n’est pas beaucoup plus claire que la précédente. Remarquez cependant les premiers champs descriptifs (lignes 3 à 5) ainsi que l’emplacement où nous indiquerons nos entrées (lignes 14 et 15). Peut-être g_option_group_new() nous éclairera-t-il un peu plus ?
01 GOptionGroup *
02 g_option_group_new (const gchar *name,
03 const gchar *description,
04 const gchar *help_description,
05 gpointer user_data,
06 GDestroyNotify destroy)
07
08 {
09 GOptionGroup *group;
10
11 group = g_new0 (GOptionGroup, 1);
12 group->name = g_strdup (name);
13 group->description = g_strdup (description);
14 group->help_description = g_strdup (help_description);
15 group->user_data = user_data;
16 group->destroy_notify = destroy;
17
18 return group;
19 }
Cela ne s’améliore pas : nous allouons de la mémoire pour le GOptionGroup et y insérons une copie des arguments.
GOptionEntry
La structure GOptionEntry nous en dira-t-elle plus ?
01 typedef struct _GOptionEntry GOptionEntry;
01 struct _GOptionEntry
02 {
03 const gchar *long_name;
04 gchar short_name;
05 gint flags;
06
07 GOptionArg arg;
08 gpointer arg_data;
09
10 const gchar *description;
11 const gchar *arg_description;
12 };
Voilà, nous y sommes. Pour chaque argument, c’est ici que nous stockons les informations qui lui sont associées. Le GOptionArg est juste une énumération :
01 typedef enum
02 {
03 G_OPTION_ARG_NONE,
04 G_OPTION_ARG_STRING,
05 G_OPTION_ARG_INT,
06 G_OPTION_ARG_CALLBACK,
07 G_OPTION_ARG_FILENAME,
08 G_OPTION_ARG_STRING_ARRAY,
09 G_OPTION_ARG_FILENAME_ARRAY,
10 G_OPTION_ARG_DOUBLE,
11 G_OPTION_ARG_INT64
12 } GOptionArg;
Il indique vraisemblablement le type de l’argument attendu.
La création du contexte
g_option_context_add_main_entries()
Dans notre programme du début, après avoir utilisé g_option_context_new(), nous avons fait appel à g_option_context_add_main_entries() :
01 void
02 g_option_context_add_main_entries (GOptionContext *context,
03 const GOptionEntry *entries,
04 const gchar *translation_domain)
05 {
06 g_return_if_fail (entries != NULL);
07
08 if (!context->main_group)
09 context->main_group = g_option_group_new (NULL, NULL, NULL, NULL, NULL);
10
11 g_option_group_add_entries (context->main_group, entries);
12 g_option_group_set_translation_domain (context->main_group, translation_domain);
13 }
Cette fonction n’est finalement qu’une fonction d’appoint pour créer le groupe principal s’il n’existe pas déjà (lignes 8 et 9) et appeler g_option_group_add_entries() et g_option_group_set_translation_domain() (lignes 11 et 12). C’est exactement ce que nous avons fait pour notre groupe d’options secondaire à ceci près que l’internationalisation est prise en compte avec le domaine de traduction (ligne 12).
g_option_group_add_entries()
Ce qui nous intéresse donc, finalement, c’est g_option_group_add_entries() :
01 g_option_group_add_entries (GOptionGroup *group,
02 const GOptionEntry *entries)
03 {
04 gint i, n_entries;
05
06 g_return_if_fail (entries != NULL);
07
08 for (n_entries = 0; entries[n_entries].long_name != NULL; n_entries++) ;
09
10 group->entries = g_renew (GOptionEntry, group->entries, group->n_entries + n_entries);
11
12 memcpy (group->entries + group->n_entries, entries, sizeof (GOptionEntry) * n_entries);
13
14 for (i = group->n_entries; i < group->n_entries + n_entries; i++)
15 {
16 gchar c = group->entries[i].short_name;
17
18 if (c)
19 {
20 if (c == '-' || !g_ascii_isprint (c))
21 {
22 g_warning (G_STRLOC»: ignoring invalid short option '%c' (%d)», c, c);
23 group->entries[i].short_name = 0;
24 }
25 }
26 }
27
28 group->n_entries += n_entries;
29 }
Ligne 8, cette boucle sans contenu est un classique pour compter le nombre d’éléments d’une structure, ici de entries. La variable n_entries y est incrémentée jusqu’au dernier élément.
Ligne 10, la zone des entrées est agrandie de n_entries fois la taille d’un GOptionEntry. Le champ entries de cette structure correspond donc à un tableau. Vous devinez ligne 28 que la taille de ce tableau est group->n_entries et c’est pourquoi il est incrémenté de n_entries à cette ligne.
Ligne 12, le tableau des entrées indiqué en argument est concaténé aux entrées existantes grâce à un memcpy().
Cela devrait suffire, mais lignes 14 à 26, nous avons droit à une boucle de vérification. En effet, pour chaque entrée, l’option courte est testée. Elle ne doit pas valoir '-', ce qui signifierait -- et correspondrait à la fin des options. Elle doit par contre valoir un caractère faisant partie des caractères imprimables (fonction g_ascii_isprint()). Si ce test ligne 20 était positif, un message d’avertissement serait envoyé au développeur (ligne 22). Celui-ci devrait corriger l’option afin que ce message n’apparaisse pas à l’utilisateur final. L’option est de plus désactivée ligne 23.
g_option_group_set_translation_domain()
Intéressons-nous à cette fonction :
01 void
02 g_option_group_set_translation_domain (GOptionGroup *group,
03 const gchar *domain)
04 {
05 g_return_if_fail (group != NULL);
06
07 g_option_group_set_translate_func (group,
08 (GTranslateFunc)dgettext_swapped,
09 g_strdup (domain),
10 g_free);
11 }
Nous avons droit à nouveau, comme dans les articles précédents, à un jeu de piste !
01 void
02 g_option_group_set_translate_func (GOptionGroup *group,
03 GTranslateFunc func,
04 gpointer data,
05 GDestroyNotify destroy_notify)
06 {
07 g_return_if_fail (group != NULL);
08
09 if (group->translate_notify)
10 group->translate_notify (group->translate_data);
11
12 group->translate_func = func;
13 group->translate_data = data;
14 group->translate_notify = destroy_notify;
15 }
Cette fonction se contente de changer les fonctions et données de traduction. Si une fonction précédente avait été indiquée pour notifier quelqu’un du changement de fonction dans group->translate_notify, elle est appelée ligne 9. La nouvelle est indiquée ligne 14 après avoir changé les champs nécessaires lignes 12 et 13 pour la nouvelle fonction et la donnée utilisateur.
g_option_context_add_group()
Nous avons vu comment créer un groupe et, dans le cas du groupe principal, l’intégrer dans le contexte. Il ne nous reste plus qu’à étudier comment prendre également en compte des groupes secondaires. C’est le rôle de g_option_context_add_group() :
01 void
02 g_option_context_add_group (GOptionContext *context,
03 GOptionGroup *group)
04 {
05 GList *list;
06
07 g_return_if_fail (context != NULL);
08 g_return_if_fail (group != NULL);
09 g_return_if_fail (group->name != NULL);
10 g_return_if_fail (group->description != NULL);
11 g_return_if_fail (group->help_description != NULL);
12
13 for (list = context->groups; list; list = list->next)
14 {
15 GOptionGroup *g = (GOptionGroup *)list->data;
16
17 if ((group->name == NULL && g->name == NULL) ||
18 (group->name && g->name && strcmp (group->name, g->name) == 0))
19 g_warning («A group named \»%s\» is already” “part of this GOptionContext»,
20 group->name);
21 }
22
23 context->groups = g_list_append (context->groups, group);
24 }
Les groupes sont stockés dans le contexte en tant que liste chaînée. Nous l’avions déjà supposé en voyant le type du champ groups dans un GOptionContext. Nous en avons la preuve ici : notre groupe est inséré aux autres avec g_list_append() ligne 23. Le code précédent, lignes 13 à 21 est une petite vérification : tous les groupes existants sont parcourus (boucle ligne 13) et leur nom est comparé à celui du groupe que nous voulons ajouter (test lignes 17 et 18). Si tel était le cas, un petit message d’avertissement est produit lignes 19 et 20.
L’analyse de la ligne de commande
Nous voici arrivés à ce qui nous intéresse le plus : l’analyse de la ligne de commande. Malheureusement, les lignes de code sont trop nombreuses pour être reproduites ici. Nous allons donc devoir nous limiter à certaines parties. Nous ne montrerons donc en particulier pas les parties répétitives, ni celles ne présentant pas grand intérêt comme les tests des arguments ou les initialisations basiques.
01 gboolean
02 g_option_context_parse (GOptionContext *context,
03 gint *argc,
04 gchar ***argv,
05 GError **error)
06 {
Ce qui suit détermine le nom du programme et fait appel à g_set_prgname(). Puis une boucle parcourt la liste des groupes (context->groups) et y exécute, pour chacun, la fonction pre_parse_func() qui lui est associée (par le champ du même nom) si celle-ci est définie. Cette fonction est également exécutée pour le groupe principal context->main_group si elle est définie. Un code similaire se trouve plus loin pour la gestion des erreurs. Nous arrivons à la ligne 48 qui démarre l’analyse des arguments.
48 if (argc && argv)
49 {
50 gboolean stop_parsing = FALSE;
51 gboolean has_unknown = FALSE;
52 gint separator_pos = 0;
53
54 for (i = 1; i < *argc; i++)
55 {
56 gchar *arg, *dash;
57 gboolean parsed = FALSE;
58
59 if ((*argv)[i][0] == ‚-' && (*argv)[i][1] != ‚\0' && !stop_parsing)
60 {
61 if ((*argv)[i][1] == ‚-')
62 {
63 /* -- option */
64
Pour chaque argument, nous testons s’il commence par un tiret (boucle ligne 54 et test ligne 59). S’il n’y a pas de problème, nous avons deux cas à distinguer : les options courtes et les options longues. Nous allons voir les options longues et faire l’impasse sur les options courtes, le code étant relativement le même. En cas de problème, ce n’est pas vraiment un problème. Nous avons seulement affaire à un argument non référencé qui sera traité lignes 234 à 240.
65 arg = (*argv)[i] + 2;
66
67 /* '--' terminates list of arguments */
68 if (*arg == 0)
69 {
70 separator_pos = i;
71 stop_parsing = TRUE;
72 continue;
73 }
74
Ligne 65, nous faisons pointer arg sur le nom de l’option. Si celui-ci est nul, l’option était -- qui signifie que nous devons arrêter l’analyse. C’est l’objet de la ligne 71.
75 /* Handle help options */
76 if (context->help_enabled)
77 {
78 if (strcmp (arg, «help») == 0)
79 print_help (context, TRUE, NULL);
80 else if (strcmp (arg, «help-all») == 0)
81 print_help (context, FALSE, NULL);
82 else if (strncmp (arg, «help-», 5) == 0)
83 {
84 GList *list;
85
86 list = context->groups;
87
88 while (list)
89 {
90 GOptionGroup *group = list->data;
91
92 if (strcmp (arg + 5, group->name) == 0)
93 print_help (context, FALSE, group);
94
95 list = list->next;
96 }
97 }
98 }
99
Il est possible de désactiver l’aide en mettant FALSE dans context->help_enabled. C’est d’ailleurs l’objet de la fonction g_option_context_set_help_enabled() dont nous ne parlerons pas plus ici. Si elle est activée (test ligne 76), nous devons tester si l’option est un dérivé de help. Vous faites ici connaissance avec la fonction print_help() dont nous avons toutes les utilisations possibles lignes 79, 81 et 93. Son premier argument indique le contexte. Le deuxième indique si nous montrons l’aide du groupe principal. Le dernier correspond à un groupe dont il faut afficher l’aide. Vous verrez plus loin que la fonction print_help() se termine par un appel à exit().
100 if (context->main_group &&
101 !parse_long_option (context, context->main_group, &i, arg,
102 FALSE, argc, argv, error, &parsed))
103 goto fail;
104
105 if (parsed)
106 continue;
107
108 /* Try the groups */
109 list = context->groups;
110 while (list)
111 {
112 GOptionGroup *group = list->data;
113
114 if (!parse_long_option (context, group, &i, arg,
115 FALSE, argc, argv, error, &parsed))
116 goto fail;
117
118 if (parsed)
119 break;
120
121 list = list->next;
122 }
123
124 if (parsed)
125 continue;
126
Pour chaque groupe, et tant que nous n’avons pas trouvé l’entrée correspondante, nous cherchons si elle y est avec parse_long_option(). Nous commençons avec le groupe principal ligne 101. Si elle y est (test ligne 105), nous passons à la suite (ligne 106). Sinon, nous attaquons la liste des groupes secondaires (boucle ligne 110) dont nous sortons ligne 119 si nous avons trouvé. Lignes 124 et 125, nous passons à la suite si l’option était dans un groupe.
127 /* Now look for --<group>-<option> */
128 dash = strchr (arg, '-');
129 if (dash)
130 {
131 /* Try the groups */
132 list = context->groups;
133 while (list)
134 {
135 GOptionGroup *group = list->data;
136
137 if (strncmp (group->name, arg, ash - arg) == 0)
138 {
139 if (!parse_long_option (context, group, &i, dash + 1,
140 TRUE, argc, argv, error, &parsed))
141 goto fail;
142
143 if (parsed)
144 break;
145 }
146
147 list = list->next;
148 }
149 }
150
Vous découvrez ici qu’il est également possible de faire précéder le nom de l’option par le nom du groupe correspondant, avec un tiret séparateur. Si ce tiret existe (test ligne 128), nous recherchons le groupe portant le nom correspondant (boucle ligne 133 et test ligne 137). Pour ce groupe, nous faisons appel à la fonction parse_long_option() (ligne 139) comme précédemment.
151 if (context->ignore_unknown)
152 continue;
153 }
154 else
155 { /* short option */
Si les options inconnues sont ignorées, nous itérons de force (ligne 152). Sinon, nous trouverons du code dédié aux options inconnues plus loin. Nous passons aux options courtes, mais ce code ressemblant au précédent, nous allons utiliser le sécateur jusqu’à la ligne 217. Vous avez ci-dessous le code correspondant à une option non analysée. Si context->ignore_unknown est nul, nous générons une erreur et, comble de l’horreur, exécutons un goto !
218 }
219
220 if (!parsed)
221 has_unknown = TRUE;
222
223 if (!parsed && !context->ignore_unknown)
224 {
225 g_set_error (error,
226 G_OPTION_ERROR,
G_OPTION_ERROR_UNKNOWN_OPTION,
227 _(«Unknown option %s»), (*argv)[i]);
228 goto fail;
229 }
Ce goto est utilisé à bon escient. Les langages plus évolués que le C proposent un mécanisme qui, en soulevant une erreur, nous envoie directement et de façon inconditionnelle à une portion de code dédiée à la gestion des erreurs. C’est le cas avec try et catch en java par exemple. Ici, la gestion de l’erreur s’effectue au label fail.
230 }
231 else
232 {
233 /* Collect remaining args */
234 if (context->main_group &&
235 !parse_remaining_arg (context, context->main_group, &i,
236 argc, argv, error, &parsed))
237 goto fail;
238
239 if (!parsed && (has_unknown || (*argv)[i][0] == '-'))
240 separator_pos = 0;
241 }
242 }
Nous avons ci-dessus la gestion des arguments au-delà de l’option -- ou des arguments non précédés par un tiret. Ils correspondent à la condition négative du test ligne 59.
Nous avons ci-dessous, où plutôt nous avions avant que l’auteur n’efface les lignes, du code pour exécuter les fonctions de post-analyse, de la même façon que nous avions au début les fonctions de pré-analyse de la ligne de commande. Elles ne présentent pas d’intérêt. Vous trouverez un code similaire plus loin pour la gestion des erreurs. Nous allons directement à la fin où les arguments sont une nouvelle fois analysés afin de supprimer ceux qui ont été traités. Ne restent donc que ceux dont l’analyseur n’a su que faire. Par ailleurs, si context->ignore_unknown est vrai, ce code n’a pas lieu d’être exécuté.
272 if (argc && argv)
273 {
274 free_pending_nulls (context, TRUE);
275
276 for (i = 1; i < *argc; i++)
277 {
278 for (k = i; k < *argc; k++)
279 if ((*argv)[k] != NULL)
280 break;
281
282 if (k > i)
283 {
284 k -= i;
285 for (j = i + k; j < *argc; j++)
286 {
287 (*argv)[j-k] = (*argv)[j];
288 (*argv)[j] = NULL;
289 }
290 *argc -= k;
291 }
292 }
293 }
294
295 return TRUE;
296
Nous arrivons enfin à la gestion des erreurs et au label fail. Ce code est similaire à ceux qui font appel aux fonctions de pré-analyse au début et aux fonctions de post-analyse au milieu de la fonction g_option_context_parse(). Au lieu d’utiliser les champs pre_parse_func() ou post_parse_func(), il s’agit ici de error_func(). Pour chaque groupe (boucle ligne 301), nous testons si une telle fonction existe (ligne 305) et, le cas échéant, nous l’exécutons (ligne 306). Cette démarche est également effectuée pour le groupe principal (test ligne 312 et exécution ligne 313).
297 fail:
298
299 /* Call error hooks */
300 list = context->groups;
301 while (list)
302 {
303 GOptionGroup *group = list->data;
304
305 if (group->error_func)
306 (* group->error_func) (context, group,
307 group->user_data, error);
308
309 list = list->next;
310 }
311
312 if (context->main_group && context->main_group->error_func)
313 (* context->main_group->error_func) (context, context->main_group,
314 context->main_group->user_data, error);
315
316 free_changes_list (context, TRUE);
317 free_pending_nulls (context, FALSE);
318
319 return FALSE;
320 }
Un peu de nettoyage lignes 316 et 317 et nous voici avec quelques fonctions qui nous restent sur les bras.
parse_long_option()
Commençons avec cette fonction. Les curieux iront également voir parse_short_option() dont le code est proche de celle-ci. Juste avant, voyons une macro, NO_ARG dont nous allons avoir besoin très vite :
01 #define NO_ARG(entry) ((entry)->arg == G_OPTION_ARG_NONE || \ 02 ((entry)->arg == G_OPTION_ARG_CALLBACK && \ 03 ((entry)->flags & G_OPTION_FLAG_NO_ARG)))
Cette macro teste si l’option est censée prendre un argument ou non. Nous la retrouvons dans le code suivant, ligne 22.
Cette fonction n’est rien d’autre qu’une boucle (ligne 14) qui recherche la bonne option, ou plutôt l’entrée du groupe dont le nom (long, puisque nous ne nous occupons ici que des options longues) de l’option correspond à celle en cours d’analyse.
01 static gboolean
02 parse_long_option (GOptionContext *context,
03 GOptionGroup *group,
04 gint *index,
05 gchar *arg,
06 gboolean aliased,
07 gint *argc,
08 gchar ***argv,
09 GError **error,
10 gboolean *parsed)
11 {
12 gint j;
13
14 for (j = 0; j < group->n_entries; j++)
15 {
16 if (*index >= *argc)
17 return TRUE;
18
19 if (aliased && (group->entries[j].flags & G_OPTION_FLAG_NOALIAS))
20 continue;
21
Nous commençons par tester le cas d’un simple drapeau, soit d’une option au format --option sans argument supplémentaire. Le test suivant vérifie l’égalité entre l’entrée courante et l’option courante ainsi que le fait que l’option ne prenne pas d’argument. Si tel est le cas, nous exécutons parse_arg() dont nous verrons le code plus loin.
22 if (NO_ARG (&group->entries[j]) &&
23 strcmp (arg, group->entries[j].long_name) == 0)
24 {
25 gchar *option_name;
26
27 option_name = g_strconcat («--», group->entries[j].long_name, NULL);
28 parse_arg (context, group, &group->entries[j],
29 NULL, option_name, error);
30 g_free(option_name);
31
32 add_pending_null (context, &((*argv)[*index]), NULL);
33 *parsed = TRUE;
34 }
35 else
36 {
Nous n’avons pas affaire à une simple option, mais à une qui prend un argument. Nous testons si celui-ci a été indiqué sous la forme --option=argument ou --option argument ligne 39. Sinon, l’option n’est pas analysée (fin du bloc d’exécution ligne 105).
37 gint len = strlen (group->entries[j].long_name);
38
39 if (strncmp (arg, group->entries[j].long_name, len) == 0 &&
40 (arg[len] == '=' || arg[len] == 0))
41 {
42 gchar *value = NULL;
43 gchar *option_name;
44
45 add_pending_null (context, &((*argv)[*index]), NULL);
46 option_name = g_strconcat («--», group->entries[j].long_name, NULL);
47
Ci-dessus, nous avons obtenu le nom de l’option. Ci-après, nous allons chercher à obtenir sa valeur. Le cas --option=argument est simple et traité en deux lignes (48 et 49). Nous entrons ensuite dans divers autres cas que nous n’allons pas détailler :
- ligne 50 : nous ne sommes pas encore au dernier argument. Nous traitons le cas où l’argument est facultatif (test ligne 52). Si ce n’est pas le cas, nous avons la valeur ligne 54. S’il est effectivement facultatif, il faut vérifier l’argument suivant : commence-t-il par un tiret (test ligne 60) ? Si oui, nous analysons l’option directement ligne 63 et quittons ligne 67. Sinon, nous avons affaire à la valeur qui a été donnée sur la ligne de commande ;
- ligne 77 : nous sommes au dernier argument, mais pour cette option, la valeur est facultative (nous sommes donc dans le cas où cette valeur n’a pas été indiquée). La fonction
parse_arg()est appelée avecNULLen guise de quatrième argument, la valeur ligne 67. Sinon, nous avons affaire à la valeur qui a été donnée sur la ligne de commande. Ce code est identique aux lignes 62 à 67 ; - ligne 87 : tous les autres cas sont des cas d’erreur : il manque un argument. La fonction retourne
FALSEaprès avoir appelég_set_error().
48 if (arg[len] == '=')
49 value = arg + len + 1;
50 else if (*index < *argc - 1)
51 {
52 if (!(group->entries[j].flags & G_OPTION_FLAG_OPTIONAL_ARG))
53 {
54 value = (*argv)[*index + 1];
55 add_pending_null (context, &((*argv)[*index + 1]), NULL);
56 (*index)++;
57 }
58 else
59 {
60 if ((*argv)[*index + 1][0] == '-')
61 {
62 gboolean retval;
63 retval = parse_arg (context, group, &group->entries[j],
64 NULL, option_name, error);
65 *parsed = TRUE;
66 g_free (option_name);
67 return retval;
68 }
69 else
70 {
71 value = (*argv)[*index + 1];
72 add_pending_null (context, &((*argv)[*index + 1]), NULL);
73 (*index)++;
74 }
75 }
76 }
77 else if (*index >= *argc - 1 &&
78 group->entries[j].flags & G_OPTION_FLAG_OPTIONAL_ARG)
79 {
[ code identique aux lignes 62 à 67 ]
86 }
87 else
[ code de gestion d'erreur ]
Lorsque nous arrivons ici, nous avons une option nommée option_name et sa valeur value. Analysons cela avec parse_arg() avant de faire le ménage et de retourner d’où nous venons :
96 if (!parse_arg (context, group, &group->entries[j],
97 value, option_name, error))
98 {
99 g_free (option_name);
100 return FALSE;
101 }
102
103 g_free (option_name);
104 *parsed = TRUE;
105 }
106 }
107 }
108
109 return TRUE;
110 }
parse_remaining_arg()
Lorsque nous sommes au-delà de l’option -- ou que nous avons un argument non précédé par un tiret, nous avons vu plus haut que nous devions appeler parse_remaining_arg() :
static gboolean
parse_remaining_arg (GOptionContext *context,
GOptionGroup *group,
gint *index,
gint *argc,
gchar ***argv,
GError **error,
gboolean *parsed)
{
gint j;
for (j = 0; j < group->n_entries; j++)
{
if (*index >= *argc)
return TRUE;
if (group->entries[j].long_name[0])
continue;
g_return_val_if_fail (group->entries[j].arg == G_OPTION_ARG_STRING_ARRAY ||
group->entries[j].arg == G_OPTION_ARG_FILENAME_ARRAY, FALSE);
add_pending_null (context, &((*argv)[*index]), NULL);
if (!parse_arg (context, group, &group->entries[j], (*argv)[*index], «», error))
return FALSE;
*parsed = TRUE;
return TRUE;
}
return TRUE;
}
parse_arg()
Nous avons vu des appels à cette fonction qui, comme son nom l’indique, analyse un argument. En voici le code :
01 static gboolean
02 parse_arg (GOptionContext *context,
03 GOptionGroup *group,
04 GOptionEntry *entry,
05 const gchar *value,
06 const gchar *option_name,
07 GError **error)
08
09 {
10 Change *change;
11
12 g_assert (value || OPTIONAL_ARG (entry) || NO_ARG (entry));
13
14 switch (entry->arg)
15 {
16 case G_OPTION_ARG_NONE:
17 {
18 change = get_change (context, G_OPTION_ARG_NONE,
19 entry->arg_data);
20
21 *(gboolean *)entry->arg_data = !(entry->flags & G_OPTION_FLAG_REVERSE);
22 break;
23 }
24 case G_OPTION_ARG_STRING:
25 {
26 gchar *data;
27
28 data = g_locale_to_utf8 (value, -1, NULL, NULL, error);
29
30 if (!data)
31 return FALSE;
32
33 change = get_change (context, G_OPTION_ARG_STRING,
34 entry->arg_data);
35 g_free (change->allocated.str);
36
37 change->prev.str = *(gchar **)entry->arg_data;
38 change->allocated.str = data;
39
40 *(gchar **)entry->arg_data = data;
41 break;
42 }
43 case G_OPTION_ARG_STRING_ARRAY:
[...]
75 case G_OPTION_ARG_FILENAME:
[...]
98 case G_OPTION_ARG_FILENAME_ARRAY:
[...]
133 case G_OPTION_ARG_INT:
134 {
135 gint data;
136
137 if (!parse_int (option_name, value,
138 &data,
139 error))
140 return FALSE;
141
142 change = get_change (context, G_OPTION_ARG_INT,
143 entry->arg_data);
144 change->prev.integer = *(gint *)entry->arg_data;
145 *(gint *)entry->arg_data = data;
146 break;
147 }
148 case G_OPTION_ARG_CALLBACK:
[...]
180 case G_OPTION_ARG_DOUBLE:
[...]
197 case G_OPTION_ARG_INT64:
[...]
214 default:
215 g_assert_not_reached ();
216 }
217
218 return TRUE;
219 }
Le code se répétant plus ou moins, nous avons éliminé le code correspondant à la plupart des cas. Étudions néanmoins le cas d’une chaîne de caractères, puis celui d’un entier. Nous avons d’abord une pré-analyse via une transformation en UTF8 (ligne 28) pour une chaîne, alors que pour un entier, nous appelons parse_int() (ligne 137). Puis la fonction get_change() se charge de créer le nécessaire pour sauvegarder la valeur initiale de l’argument. La sauvegarde a lieu lignes 37 et 38 pour une chaîne, et ligne 144 pour un entier. Puis nous modifions la ligne entry correspondant à notre argument dans le tableau des options, lignes 40 ou 145.
Normalement, le cas par défaut ne devrait jamais survenir. Aussi, plutôt que de ne rien mettre, les développeurs préfèrent en général provoquer une erreur si cela arrivait, ce qui est le cas ligne 215.
Voyons encore la fonction get_change() dont nous avons parlé précédemment :
01 static Change *
02 get_change (GOptionContext *context,
03 GOptionArg arg_type,
04 gpointer arg_data)
05 {
06 GList *list;
07 Change *change = NULL;
08
09 for (list = context->changes; list != NULL; list = list->next)
10 {
11 change = list->data;
12
13 if (change->arg_data == arg_data)
14 goto found;
15 }
16
17 change = g_new0 (Change, 1);
18 change->arg_type = arg_type;
19 change->arg_data = arg_data;
20
21 context->changes = g_list_prepend (context->changes, change);
22
23 found:
24
25 return change;
26 }
Cette fonction recherche tout simplement si une sauvegarde a déjà eu lieu. Le cas échant, vous voyez alors un goto ligne 14 qui n’est pas moins propre que le return change que vous auriez pu lire à la place. Sinon, un nouveau changement est créé ligne 17 et pré-rempli lignes 18 et 19. Ce changement est ajouté au début de la liste des changements ligne 21.
L’intérêt de sauvegarder les valeurs précédentes est de pouvoir les restaurer. Si vous faites une recherche sur « changes » dans glib/goption.c, c’est dans get_change() que vous trouverez le plus d’occurrences évidemment, mais vous en trouvez également dans free_changes_list(). Le second argument de cette fonction est explicite : il s’appelle revert. Il s’agit donc bien d’un retour en arrière. Vous pouvez maintenant mieux comprendre la fin du code de g_option_context_parse() que nous avons vu précédemment : lors d’un échec, nous avions vu un appel à goto fail. A la suite du label fail, vous trouvez ligne 316 l’appel à free_changes_list().
print_help()
Enfin, nous arrivons à la fonction print_help() qui affiche l’aide. Mais avant de lire son code, voyez la macro TRANSLATE qui est utilisée plusieurs fois dans print_help() :
01 #define TRANSLATE(group, str) (((group)->translate_func ? (* (group)->translate_func) ((str), (group)->translate_data) : (str)))
S’il existe une fonction de traduction définie pour le groupe indiqué, elle est utilisée pour traduire str. Sinon, c’est str elle-même qui est utilisée.
Voici maintenant la tant attendue print_help(). Ce code est assez long, mais comme peu de choses sont redondantes, nous allons le voir en entier :
01 static void
02 print_help (GOptionContext *context,
03 gboolean main_help,
04 GOptionGroup *group)
05 {
06 GList *list;
07 gint max_length, len;
08 gint i;
09 GOptionEntry *entry;
10 GHashTable *shadow_map;
11 gboolean seen[256];
12 const gchar *rest_description;
13
Les options restantes et le résumé
Le code suivant analyse les options restantes. Nous découvrons cette fonctionnalité en lisant le code, et c’est en recherchant arg_description dans glib/goption.c puis G_OPTION_REMAINING dans ce même fichier et ensuite dans la documentation officielle que nous avons l’explication. Si dans vos entrées vous en définissez une de type G_OPTION_ARG_STRING_ARRAY ou G_OPTION_ARG_FILENAME_ARRAY, et que vous mettez G_OPTION_REMAINING en guise d’option longue (la chaîne correspondante est une chaîne vide : #define G_OPTION_REMAINING «»), alors sur la ligne votre_programme [OPTION...]... vous trouverez la description de ces options restantes. L’affichage de cette ligne a lieu lignes 29 à 34 et la recherche de la bonne description se trouve dans la boucle lignes 18 à 26.
Le résumé est affiché ligne 37 s’il existe (test ligne précédente).
14 rest_description = NULL;
15 if (context->main_group)
16 {
17
18 for (i = 0; i < context->main_group->n_entries; i++)
19 {
20 entry = &context->main_group->entries[i];
21 if (entry->long_name[0] == 0)
22 {
23 rest_description = TRANSLATE (context->main_group, entry->arg_description);
24 break;
25 }
26 }
27 }
28
29 g_print («%s\n %s %s%s%s%s%s\n\n»,
30 _(«Usage:»), g_get_prgname(), _(«[OPTION...]»),
31 rest_description ? « « : «»,
32 rest_description ? rest_description : «»,
33 context->parameter_string ? « « : «»,
34 context->parameter_string ? TRANSLATE (context, context->parameter_string) : «»);
35
36 if (context->summary)
37 g_print («%s\n\n», TRANSLATE (context, context->summary));
38
Résolution des conflits
Nous allons maintenant résoudre les conflits, à savoir les options portant le même nom long et celles le même nom court. Pour cela, nous effectuons une première passe lignes 42 à 56. Pour les options longues, nous nous contentons de les insérer dans une table de hachage. Pour les courtes, nous vérifions que nous ne les avons pas déjà vues, ce qui signifierait que l’entrée correspondante dans le tableau seen[] serait vraie (test ligne 51). Si tel est le cas, l’option est purement et simplement désactivée (ligne 52).
39 memset (seen, 0, sizeof (gboolean) * 256);
40 shadow_map = g_hash_table_new (g_str_hash, g_str_equal);
41
42 if (context->main_group)
43 {
44 for (i = 0; i < context->main_group->n_entries; i++)
45 {
46 entry = &context->main_group->entries[i];
47 g_hash_table_insert (shadow_map,
48 (gpointer)entry->long_name,
49 entry);
50
51 if (seen[(guchar)entry->short_name])
52 entry->short_name = 0;
53 else
54 seen[(guchar)entry->short_name] = TRUE;
55 }
56 }
57
Puis nous recommençons la même chose pour les groupes d’options. Mais est-ce vraiment le même code ? Pas tout à fait. En effet, il apparaît un test supplémentaire, si l’option longue a déjà été rencontrée. Dans ce cas, pas question de la désactiver. Mais nous la remplaçons par elle-même précédée du nom de son groupe (test lignes 65 et 66 ; remplacement ligne 67). Sinon, nous insérons l’option dans la table de hachage comme ci-dessus. Notez que le test n’a lieu que si vous n’avez pas une entrée flanquée du drapeau G_OPTION_FLAG_NOALIAS qui sert justement à cela.
Le principe est le même pour les options courtes, lignes 71 à 75. A la fin, la table de hachage ne nous sert plus (pas plus d’ailleurs que le tableau seen[]) et elle est détruite ligne 80.
58 list = context->groups;
59 while (list != NULL)
60 {
61 GOptionGroup *group = list->data;
62 for (i = 0; i < group->n_entries; i++)
63 {
64 entry = &group->entries[i];
65 if (g_hash_table_lookup (shadow_map, entry->long_name) &&
66 !(entry->flags && G_OPTION_FLAG_NOALIAS))
67 entry->long_name = g_strdup_printf («%s-%s», group->name, entry->long_name);
68 else
69 g_hash_table_insert (shadow_map, (gpointer)entry->long_name, entry);
70
71 if (seen[(guchar)entry->short_name] &&
72 !(entry->flags && G_OPTION_FLAG_NOALIAS))
73 entry->short_name = 0;
74 else
75 seen[(guchar)entry->short_name] = TRUE;
76 }
77 list = list->next;
78 }
79
80 g_hash_table_destroy (shadow_map);
81
Calcul de taille
Le code suivant calcule la taille nécessaire pour afficher la colonne de gauche, celle contenant les noms des options. Le résultat arrive dans max_length et indique la taille maximale d’une option. A la fin, ligne 114, 4 octets sont ajoutés pour constituer un peu d’espace entre l’option et son descriptif.
82 list = context->groups;
83
84 max_length = g_utf8_strlen («-?, --help», -1);
85
86 if (list)
87 {
88 len = g_utf8_strlen («--help-all», -1);
89 max_length = MAX (max_length, len);
90 }
91
92 if (context->main_group)
93 {
94 len = calculate_max_length (context->main_group);
95 max_length = MAX (max_length, len);
96 }
97
98 while (list != NULL)
99 {
100 GOptionGroup *group = list->data;
101
102 /* First, we check the --help-<groupname> options */
103 len = g_utf8_strlen («--help-», -1) + g_utf8_strlen (group->name, -1);
104 max_length = MAX (max_length, len);
105
106 /* Then we go through the entries */
107 len = calculate_max_length (group);
108 max_length = MAX (max_length, len);
109
110 list = list->next;
111 }
112
113 /* Add a bit of padding */
114 max_length += 4;
115
L’affichage des options
Remarquez les trois morceaux de code. A moins qu’un groupe n’ait été indiqué sur la ligne de commande avec --help-<group>, le code lignes 116 à 140 est exécuté pour afficher de l’aide sur l’aide, en particulier la liste des groupes et les fameuses options --help-<group>.
116 if (!group)
117 {
118 list = context->groups;
119
120 g_print («%s\n -%c, --%-*s %s\n»,
121 _(«Help Options:»), '?', max_length - 4, «help»,
122 _(«Show help options»));
123
124 /* We only want --help-all when there are groups */
125 if (list)
126 g_print (« --%-*s %s\n», max_length, «help-all»,
127 _(«Show all help options»));
128
129 while (list)
130 {
131 GOptionGroup *group = list->data;
132
133 g_print (« --help-%-*s %s\n», max_length - 5, group->name,
134 TRANSLATE (group, group->help_description));
135
136 list = list->next;
137 }
138
139 g_print (“\n”);
140 }
141
Puis, c’est le tour de deux cas, si un groupe a été demandé explicitement (lignes 142 à 150), et sinon, si nous n’avons pas de groupe principal, tous les groupes (lignes 151 à 170).
142 if (group)
143 {
144 /* Print a certain group */
145
146 g_print («%s\n», TRANSLATE (group, group->description));
147 for (i = 0; i < group->n_entries; i++)
148 print_entry (group, max_length, &group->entries[i]);
149 g_print («\n»);
150 }
151 else if (!main_help)
152 {
153 /* Print all groups */
154
155 list = context->groups;
156
157 while (list)
158 {
159 GOptionGroup *group = list->data;
160
161 g_print («%s\n», group->description);
162
163 for (i = 0; i < group->n_entries; i++)
164 if (!(group->entries[i].flags & G_OPTION_FLAG_IN_MAIN))
165 print_entry (group, max_length, &group->entries[i]);
166
167 g_print («\n»);
168 list = list->next;
169 }
170 }
171
Enfin, si nous avons un groupe principal, ou qu’un groupe n’a pas été indiqué spécifiquement, nous affichons le groupe principal (lignes 179 à 182) suivi des autres options (lignes 184 à 194).
172 /* Print application options if --help or --help-all has been specified */
173 if (main_help || !group)
174 {
175 list = context->groups;
176
177 g_print («%s\n», _(«Application Options:»));
178
179 if (context->main_group)
180 for (i = 0; i < context->main_group->n_entries; i++)
181 print_entry (context->main_group, max_length,
182 &context->main_group->entries[i]);
183
184 while (list != NULL)
185 {
186 GOptionGroup *group = list->data;
187
188 /* Print main entries from other groups */
189 for (i = 0; i < group->n_entries; i++)
190 if (group->entries[i].flags & G_OPTION_FLAG_IN_MAIN)
191 print_entry (group, max_length, &group->entries[i]);
192
193 list = list->next;
194 }
195
196 g_print (“\n”);
197 }
198
A la fin, avant de quitter, si une description existe dans le contexte, nous l’affichons.
199 if (context->description) 200 g_print (“%s\n”, TRANSLATE (context, context->description)); 201 202 exit (0); 203 }
g_option_context_set_description()
Une telle description peut être indiquée avec g_option_context_set_description() :
01 void
02 g_option_context_set_description (GOptionContext *context,
03 const gchar *description)
04 {
05 g_return_if_fail (context != NULL);
06
07 g_free (context->description);
08 context->description = g_strdup (description);
09 }
Cette fonction sert principalement à indiquer un message à afficher en fin de l’aide, et il est souhaitable d’y indiquer un site web où trouver plus d’informations sur le sujet, ainsi qu’une adresse électronique pour soumettre les bugs éventuels.
Conclusion
Cet article très long vous aura permis de voir comment une gestion complète des options peut se faire. Les programmeurs utilisent généralement une simple série de printf() dans leur code alors qu’avec de bonnes fonctions, vous pouvez arriver à un résultat plus abouti.
En l’occurrence, le système que propose GLib permet d’avoir un affichage de l’aide toujours à jour, car il fonctionne sur la base d’un référencement. Si vous n’avez pas indiqué l’option au système, il ne peut en tenir compte pour l’analyse de la ligne de commande.
Au contraire, si l’option est connue, non seulement elle pourra être analysée, mais, en plus, elle fera partie de l’aide. Enfin, GLib propose une notion de groupes d’options qui n’est pas négligeable, en particulier pour tous ceux qui programment des greffons. Ceux-ci ajoutent des options facultatives, dont la présence dépend de si le greffon est chargé ou non. Maintenant que vous avez pris connaissance de ce code, servez-vous-en !
Le mois prochain : les arbres binaires balancés de type GTree...
Référence :
Le site de GTK+ : http://www.gtk.org/
Retrouvez cet article dans : Linux Magazine 89

