OCaml : Parser les arguments d’un programme avec le module Arg

English version here

Aujourd’hui, du fonctionnel, car je refais un peu d’Objective Caml ces temps-ci…

OCaml possède un assez grand nombre de modules dans sa bibliothèque standard, servant à faire un peu tout et n’importe quoi. Dans la suite, on va voir en vitesse comment utiliser le module Arg pour déclarer et gérer les paramètres optionnels d’un programme.

Un des logos sexy d’OCaml que la plupart des autres langages lui envient

L’autre motivation de cet article est que j’ai moi-même été infoutu de retrouver rapidement comment on faisait… ce qui m’a demandé pas mal de recherches.

Cet article s’adresse comme d’habitude aux gens qui connaissent déjà un peu le langage. Je ne réexplique donc pas comment fonctionne ce dernier (la documentation sur Internet est abondante, ceci dit).

Le module Arg(h)

Un étudiant en informatique n’arrivant pas à utiliser le module Arg

Pour parser les arguments en ligne de commande, Arg est le module qu’il nous faut ! Lisons la documentation du module sur le site de l’INRIA:

Parsing of command line arguments.

This module provides a general mechanism for extracting options and arguments from the command line to the program.

Syntax of command lines: A keyword is a character string starting with a -. An option is a keyword alone or followed by an argument. The types of keywords are: Unit, Bool, Set, Clear, String, Set_string, Int, Set_int, Float, Set_float, Tuple, Symbol, and Rest. Unit, Set and Clear keywords take no argument. A Rest keyword takes the remaining of the command line as arguments. Every other keyword takes the following word on the command line as argument. Arguments not preceded by a keyword are called anonymous arguments.

Que nous apprend-il ?

  • Qu’il s’agit du module pour parser des arguments de ligne de commande (ouf, nous sommes au bon endroit !)
  • Qu’en utilisant ce module, on appelle une chaîne commençant par un tiret (-) un keyword
  • Qu’à l’intérieur, les keywords sont séparés en types (on va revenir dessus)
  • Que les arguments qui ne sont pas précédés par un keyword sont dits anonymes.

Ok, bon, en continuant de lire la documentation, on apprend plein de choses intéressantes sur les types définis par le module. On aura l’occasion de revenir sur certains d’entre eux.

La fonction qui va nous intéresser ici est Arg.parse. Pas Arg.parse_argv comme on pourrait s’y attendre, car en lisant la documentation de celle-ci, nous apprenons que Arg.parse_argv sert en fait à parser un array de strings args comme si c’était le vrai argv, l’array de strings contenant les vrais paramètres du programme. Non: la fonction qui sert vraiment à parser les arguments du programme, c’est Arg.parse.

Arg.parse

La documentation de cette dernière est assez dense:

Arg.parse speclist anon_fun usage_msg parses the command line. speclist is a list of triples (key, spec, doc). key is the option keyword, it must start with a ‘-‘ character. spec gives the option type and the function to call when this option is found on the command line. doc is a one-line description of this option. anon_fun is called on anonymous arguments. The functions in spec and anon_fun are called in the same order as their arguments appear on the command line.

If an error occurs, Arg.parse exits the program, after printing to standard error an error message as follows:

  • The reason for the error: unknown option, invalid or missing argument, etc.
  • usage_msg
  • The list of options, each followed by the corresponding doc string. Beware: options that have an empty doc string will not be included in the list.

For the user to be able to specify anonymous arguments starting with a -, include for example (« -« , String anon_fun, doc) in speclist. By default, parse recognizes two unit options, -help and –help, which will print to standard output usage_msg and the list of options, and exit the program. You can override this behaviour by specifying your own -help and –help options in speclist.

et pas forcément très compréhensible au premier abord. Décomposons point par point ce qu’elle fait et ce qu’elle est. D’abord sa signature:

nous apprend qu’il s’agit d’une fonction prenant trois paramètres: une liste de triplets de key, spec et doc, une anon_fun et un usage_msg, et ne retournant rien en particulier (le type unit).

Oula, c’est quoi tous ces types bizarres ?

Pour mieux comprendre cette déclaration, il faut lire avec attention la doc du module Arg, on se rend alors compte que tous ces types sont juste des alias créés par le module Arg:

  • key, doc et usage_msg ne sont autres que des strings normales
  • spec est un type variant un peu particulier, qui permet de spécifier quoi faire quand on tombe sur un argument en particulier. On y revient juste après
  • anon_fun est déclaré dans la doc comme étant de type string -> unit. Ce qui signifie qu’il décrit en fait une fonction prenant une string en paramètre et ne retournant rien (le nom du type nous invite ici à la déclarer anonyme, directement dans l’appel de Arg.parse avec le mot-clé fun par exemple, mais ce n’est pas obligé).

Quel est le langage de programmation le plus rigolo ? OCaml, parce que c’est plein de fun !

Comment on s’en sert ?

Ces éléments en main, nous pouvons relire la définition de la fonction en la comprenant un peu mieux.

La liste de triplets (key * spec * doc) que Arg.parse attend en premier argument est en fait l’endroit où nous allons définir nos keywords (pour reprendre le vocabulaire du module), mettre une courte explication de ce à quoi ils servent, et dire à Arg.parse quoi faire si elle rencontre ce keyword. C’est ici que le variant spec devient important: en regardant la doc du module Arg, on se rend compte qu’il s’agit d’un ensemble d’alias (un variant) nous permettant de lancer une action différente selon le type que nous voulons que ce paramètre représente. Par exemple, la spec Arg.Int va nous permettre d’appeler une fonction prenant un int en paramètre, après que Arg.parse ait converti l’argument du keyword spécifié en int. Même chose pour Arg.String, mais pour une string… Avec Arg.Set et Arg.Clear, le module Arg se propose même de setter un flag booléen à vrai ou faux pour nous lorsqu’il trouve le keyword approprié.

La fonction anon_fun, par ailleurs, est appelée à chaque fois que Arg.parse rencontre un argument anonyme, c’est-à-dire, si vous avez bien suivi, un argument qui n’est pas précédé d’un keyword.

La documentation de la fonction dit aussi qu’elle comporte par défaut les keywords -help et –help. Ces deux options, qui font la même chose, se serviront de la string usage_msg pour afficher l’usage du programme avant la liste des options disponibles.

L’exemple !

Prenons un exemple pour mettre en pratique tout ça : nous voulons créer MyLs2000, un programme révolutionnaire qui va lister les fichiers à l’intérieur d’un dossier (wahou !). Il aura trois options de lancement:

  • -v, pour activer le mode verbeux
  • -n, pour spécifier le nombre maximum de fichiers à afficher (on veut que ce programme soit révolutionnaire !)
  • -d, pour donner un nom de dossier où lister les fichiers.

Pour l’instant, nous ne voulons pas gérer les arguments anonymes, nous allons donc nous contenter de les afficher sur la sortie standard avec print_endline (après tout, c’est une fonction correspondant à la signature string -> unit demandée par le prototype de Arg.parse !).

Ce qui nous donnerait alors cette première version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(* exemple.ml *)

let verbose = ref false
let max_files_to_list = ref 42
let dir_to_list = ref "."

let set_max_files nbr_of_files = max_files_to_list := nbr_of_files

let set_directory dir = dir_to_list := dir

let main =
begin
let speclist = [("-v", Arg.Set verbose, "Enables verbose mode");
("-n", Arg.Int (set_max_files), "Sets maximum number of files to list");
("-d", Arg.String (set_directory), "Names directory to list files");
]
in let usage_msg = "MyLs2000 is a revolutionary file listing tool. Options available:"
in Arg.parse speclist print_endline usage_msg;
print_endline ("Verbose mode: " ^ string_of_bool !verbose);
print_endline ("Max files to list: " ^ string_of_int !max_files_to_list);
print_endline ("Directory to list files: " ^ !dir_to_list);
end

let () = main

Compilons notre fichier pour tester notre code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(baron_a @ ~/Projects/ocaml ) $ ocamlc exemple.ml -o MyLs2000
(baron_a @ ~/Projects/ocaml ) $ ./MyLs2000 # Affiche les valeurs par défaut
Verbose mode: false
Max files to list: 1
Directory to list files: .

(baron_a @ ~/Projects/ocaml ) $ ./MyLs2000 -help # Affiche notre usage_msg, puis la liste des options dispo
MyLs2000 is a revolutionary file listing tool. Options available:
-v Enables verbose mode
-n Sets maximum number of files to list
-d Names directory to list files
-help Display this list of options
--help Display this list of options

(baron_a @ ~/Projects/ocaml ) $ ./MyLs2000 -v -n 84 -d toto Hello World # essayons de setter nos arguments, avec des arguments anonymes en plus
Hello
World # les arguments anonymes sont bien affichés par la fonction print_endline.
Verbose mode: true
Max files to list: 84
Directory to list files: toto # ça a marché !

Notez que grâce à son mécanisme de spec, Arg.parse nous évite d’avoir à vérifier par nous-même que ce qu’entre l’utilisateur est valide par rapport à ce que nous attendons. Elle vérifie bien aussi qu’une option attendant normalement un paramètre en a bien un:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(baron_a @ ~/Projects/ocaml ) $ ./MyLs2000 -n toto
./MyLs2000: wrong argument <code>toto'; option </code>-n' expects an integer. # il nous envoie chier car nous n'avons pas mis un entier
MyLs2000 is a revolutionary file listing tool. Options available:
-v Enables verbose mode
-n Sets maximum number of files to list
-d Names directory to list files
-help Display this list of options
--help Display this list of options

(baron_a @ ~/Projects/ocaml ) $ ./MyLs2000 -n
./MyLs2000: option `-n' needs an argument.
MyLs2000 is a revolutionary file listing tool. Options available:
-v Enables verbose mode
-n Sets maximum number of files to list
-d Names directory to list files
-help Display this list of options
--help Display this list of options

 

Allons plus loin avec notre exemple

Les plus attentifs auront remarqué que notre exemple est largement améliorable.

Par exemple, pour ce qu’elles font, nos fonctions set_max_files nbr_of_files et set_directory sont tout à fait inutiles, on pourrait les remplacer par la spec Set_int et Set_string, respectivement. Nous pourrions d’ailleurs changer la fonction qui gère les arguments anonymes: plutôt que de juste les afficher, nous pourrions aussi afficher qu’il s’agit bien d’arguments anonymes avant.

En plus de faire cette petite modification, nous allons maintenant étudier trois spec un peu spéciales, sur lesquelles la doc passe très vite mais qui peuvent se révéler des atouts très puissants: Arg.Tuple, Arg.Symbol et Arg.Rest. Énonçons d’abord à quoi ils servent:

  • Arg.Tuple : c’est la spec qui nous permet de faire prendre à un keyword plusieurs paramètres
  • Arg.Symbol: c’est la spec qui va nous permettre de n’accepter que certains paramètres pour un keyword
  • Arg.Rest: c’est la spec qui nous permet de dire « après ce keyword, arrête de les parser et envoie tout le reste à telle fonction ».

Pour montrer comment on peut s’en servir, nous allons étoffer notre exemple. Supposons qu’on veuille rajouter trois options à MyLs2000:

  • l’option -t, qui permettra de ne lister que les fichiers créés à une heure donnée. L’option prend en paramètre une heure et un nombre de minutes, séparés par un espace
  • l’option -s, qui permettra de trier les fichiers listés selon trois méthodes, alphabétiquement, chronologiquement, ou par propriétaire.
  • et enfin l’option «  » (double tiret), qui permettra de dire « Stop ! » et d’afficher tout ce qui la succède sur la sortie standard.

Les trois specs qu’on a vu précédemment servent exactement à faire ce qu’on veut faire ! Notre exemple modifié pour les intégrer ressemble maintenant à ça:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
(* exemple.ml *)

let verbose = ref false
let max_files_to_list = ref 1
let dir_to_list = ref "."
let time_hours = ref 0
let time_minutes = ref 0

let sort_files = function
"alpha" -> print_endline "Alpha sort"
| "chrono" -> print_endline "Chrono sort"
| "owner" -> print_endline "Owner sort"
| _ -> raise (Arg.Bad("Shouldn't happen"))

let main =
begin
let speclist = [("-v", Arg.Set verbose, "Enables verbose mode");
("-n", Arg.Set_int max_files_to_list, "Sets maximum number of files to list");
("-d", Arg.Set_string dir_to_list, "Names directory to list files");
("-t", Arg.Tuple ([Arg.Set_int time_hours ; Arg.Set_int time_minutes]), "Sets creation hours and minutes listed files have to match");
("-s", Arg.Symbol (["alpha"; "chrono"; "owner"], sort_files), " Allows to sort listed files alphabetically, chronologically, or by owner");
("--", Arg.Rest (fun arg -> print_endline ("The rest contains: " ^ arg)), "Stop interpreting keywords and prints the rest");
]
in let usage_msg = "MyLs2000 is a revolutionary file listing tool. Options available:"
in Arg.parse speclist (fun anon -> print_endline ("Anonymous argument: " ^ anon)) usage_msg;
print_endline ("Verbose mode: " ^ string_of_bool !verbose);
print_endline ("Max files to list: " ^ string_of_int !max_files_to_list);
print_endline ("Directory to list files: " ^ !dir_to_list);
print_endline ("Time of files to list: " ^ string_of_int(!time_hours) ^ ":" ^ string_of_int(!time_minutes));
end

let () = main

Plusieurs trucs à noter ici:

  • comme le type Arg.Tuple n’est finalement qu’une spec list , il suffit d’insérer à cet endroit la liste des opérations que nous voulons réaliser avec les arguments du keyword, dans l’ordre où ils apparaissent.
  • Arg.Symbol est de type string list * (string -> unit), ce qui signifie qu’il attend une liste de strings contenant les options acceptées, et la fonction appelée si l’argument apporté fait partie de cette liste. Vous pouvez voir que dans notre cas, c’est la fonction sort_files, que le module oblige à être de type string -> unit, qui sera appelée dans pareille situation.
  • petit exemple en passant de l’utilisation d’une des exceptions que nous procure le module, Arg.Bad, dans un cas qui normalement ne devrait jamais être atteint (car Arg.parse aura vérifié pour nous)
  • Arg.Rest est assez simple à comprendre: lorsqu’on rencontrera le keyword « –« , on arrête tout, et le reste des keywords suivants seront passés, un par un, à la fonction print_endline.
  • on a bien remplacé juste print_endline par une fonction anonyme pour les arguments anonymes, pour plus de clarté

On peut refaire quelques tests pour se convaincre que nos nouveaux apports marchent comme prévu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(baron_a @ ~/Projects/ocaml ) $ ./MyLs2000 -help # On vérifie bien que nos nouveaux keywords apparaissent dans l'aide
MyLs2000 is a revolutionary file listing tool. Options available:
-v Enables verbose mode
-n Sets maximum number of files to list
-d Names directory to list files
-t Sets creation hours and minutes listed files have to match
-s {alpha|chrono|owner} Allows to sort listed files alphabetically, chronologically, or by owner
-- Stop interpreting keywords and prints the rest
-help Display this list of options
--help Display this list of options

(baron_a @ ~/Projects/ocaml ) $ ./MyLs2000 -v -n 42 -d "Sting & The Police" -t 24 42 -s alpha walking on -- the moon
Alpha sort
Anonymous argument: walking
Anonymous argument: on
The rest contains: the
The rest contains: moon
Verbose mode: true
Max files to list: 42
Directory to list files: Sting & The Police
Time of files to list: 24:42

Je ne fais pas non plus un test exhaustif, je pense que vous avez compris comment ça marche et pourquoi ça marche comme ça. Je vous laisse copier-coller le code chez vous si vous avez un doute ou si vous voulez tester vous-même 😉

Pour aller (encore) plus loin

Oui, enfin, pas trop loin non plus… Hé, non ! Revenez !

Voilà, nous avons à peu près fait le tour de la très puissante fonction parse du module Arg. Bien entendu, il en comporte beaucoup d’autres, mais c’est en général pour celle-là qu’on l’utilise.

En matière de parsing d’arguments, il existe également des modules comme Getopt ou OptParse. Je ne les ai jamais utilisés, mais en gros, ce sont des alternatives au module Arg au fonctionnement plus proche de la commande GNU getopt. Si vous vous retrouvez un jour dans une impasse avec le module Arg, vous voudrez peut-être donner leur chance à ces deux-là.

Un peu au hasard de mes recherches, je suis tombé sur une présentation de l’université de Valenciennes qui fait une utilisation assez complète de la fonction parse (environ à la moitié).

Pour le mot de la fin, je vais laisser la parole à Joe :

« À plus, les touaregs ! Et n’oubliez pas: fumer, c’est mal ! »

2 réflexions au sujet de « OCaml : Parser les arguments d’un programme avec le module Arg »

  1. BenzoX

    Salut,
    Merci beaucoup pour cette introduction claire et précise à la gestion des arguments. Une question demeure : dans l’esprit du fonctionnel, utiliser tout un tas de refs pour les options n’est pas très propre, aussi ma question est-elle : n’y a-t-il pas une astuce pour parser les arguments et appeler une application partielle sur lesdites options ?
    Par exemple, si notre exécutable toto prend en arguments (a:int), (b:string) et (c:bool), n’est-il pas possible de faire en sorte que toto a b c soit parsé ainsi : (toto a) (parse b c), et récursivement ?
    De plus, de cette manière, on pourrait élaguer l’arbre des possibles, par exemple si les options a et b sont incompatibles, faire en sorte que (toto a) b renvoie « Mauvais argument ».
    Cependant, je ne sais pas si cette approche est robuste face au changement d’ordre dans les arguments.

    Répondre
    1. Pando Auteur de l’article

      Bonjour,
      oui, c’est une bonne question. Je me suis aussi dit en écrivant l’article que tout un tas de refs c’est pas très très fonctionnel comme approche, d’où le fait qu’à mon avis c’est un peu « legacy » comme module 🙂
      Bon, au niveau du code, je pense que ce serait mieux si on déclarait les refs dans un « let … in » de l’expression qui parse les args, plutôt qu’un let « global » (comme dans l’article), mais c’est à peu près tout.
      Pas sûr qu’on pourrait s’en servir de manière plus « intelligente », ce serait peut-être plus facile avec parse_argv, la version qui prend l’array des args en paramètre. Ça m’intéresserait, le cas échéant 🙂
      (Désolé pour l’absence de réponse, en fait…)

      Répondre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.