Apache Commons CLI

Développer une application en ligne de commande avec Apache Commons CLI

Ou comment gérer les paramètres passés au programme…​

Problématique

Développer une application en ligne de commande en Java ne pose pas de problème en soit. En effet, il suffit de développer une méthode main et de traiter les arguments reçus dans la variable args.

 public static void main(String[] args) {
   String premiereOption = args[0];
   ...
 }

Dans une application de ce type, les arguments servent à paramétrer le comportement du programme et à injecter des valeurs à utiliser dans le traitement. Le problème, c’est que cette façon de procéder ne permet pas facilement de gérer des passages d’arguments nommés, optionnels ou obligatoire, à la Posix ou à la GNU. Il est bien possible de passer des propriétés, par l’option -D, mais là encore, le coté verbeux peut rebuter plus d’un utilisateur.

On notera que passer des arguments au programme n’est pas la seule façon de procéder. On peut aussi utiliser des fichiers properties, exploiter les possibilités de l’API Preference de Java ou utiliser des variables d’environnement.

Formats d’arguments

Options et données

Tout d’abord, il faut bien distinguer dans la pratique le passage d’option et le passage de données en argument. Une donnée serait par exemple le nom d’un fichier, alors que l’option serait par exemple -f pour indiquer qu’il faut prendre en compte un fichier. On voit ici que l’option n’est qu’un flag transmis à l’application.

Généralement, la donnée est passée de façon brute alors que l’option est préfixée, par un tiret ou un double tiret, sous Linux, ou par un slash sous Windows. Les données et options peuvent être associées dans une liste d’arguments, en les juxtaposant ou en les séparant par un signe prédéfini, comme =, par exemple.

Dans ce premier exemple, la donnée foo.tar.gz concerne l’option f uniquement parce qu’elle lui succède dans la liste des arguments :

~# tar -f foo.tar.gz

Dans ce deuxième exemple, le fait que la donnée foo.tar.gz concerne l’option file est indiqué par le signe =. On notera que le choix de ce signe dépend uniquement d’une convention et qu’il pourrait être replacé par :, >, ou tout autre signe.

~# tar --file=foo.tar.gz

Propriétés Java

Les propriétés Java sont préfixées par les deux caractère "-D" et utilisent le séparateur "=" entre l’option et la valeur. L’option est souvent constituée d’un ensemble de mots séparés par des points (".") passés au format -Djtips.doit=true. Ce type d’argument est géré automatique lorsqu’il est passé à la JVM, mais pas quand il est passé au programme.

Avec ce premier format, l’argument peut être lu dans les system properties.

~# java -Djtips.doit=true info.jtips.commons.CLI
 public static void main(String[] args) {
   System.out.println(System.getProperty("jtips.doit"));
 }

Par contre, avec ce format-ci, l’argument est passé dans le tableau args et doit être traité manuellement.

~# java info.jtips.commons.CLI -Djtips.doit=true
 public static void main(String[] args) {
   System.out.println(args[0]);
 }

Options Posix

Le format d’option Posix est court, sur un caractère, avec un tiret ('-') devant chaque option ou devant un ensemble d’option. Par exemple, pour la commande tar, chaque lettre derrière le tiret correspond à une option :

~# tar -zxvlf foo.tar.gz

Options GNU

Le format d’option GNU est plus long, mais plus lisible pour un humain. Chaque option est précédée par un double tiret et est écrite sous forme d’un mot ou d’une suite de motes séparés par des tirets simples.

~# tar --gzip --verbose --extract --check-links --file=foo.tar.gz

Apache Commons CLI

L’utilitaire Commons CLI a été développer pour faciliter le travail du développeur. Il se charge de vérifier la liste des arguments, nous aide à présenter un message d’erreur en cas de problème, puis nous assiste dans la lecture des options et des valeurs associées.

Créer les options

Avant de créer les options, il faut instancier leur conteneur, de type Options.

 Options options = new Options();

Puis chaque option peut être créé directement en appelant la méthode addOption(…​), dont il existe trois variantes.

 // Ajoute une option -a, sans argument, avec une description
 options.addOption("a", false, "Option a");

 // Ajoute une option -b, ou --blabla, avec un argument, avec une description
 options.addOption("b", "blabla", true, "Option a");

La troisième variante prend un objet Option en paramètre. Celui-ci doit être instancié via la méthode Option.builder(). Une partie de sa configuration peut se faire avec le builder, le reste de la configuration se fait ultérieurement sur l’option. Cette façon de procéder est plus verbeuse, mais apporte plus de souplesse.

 // Création de l'option -o, ou --out-file, avec un argument et obligatoire
 Option outFileOption = Option.builder()
            .option("o")
            .longOpt("out-file")
            .desc("URL du fichier cible")
            .required()
            .hasArg()
            .build();

 // L'option est ajoutée à la liste
 options.addOption(outFileOption);

Analyser les options et arguments

L’analyse des options et arguments transmis se fait par un CommandLineParser. Commons CLI en proposait trois implémentations qui se différenciaient essentiellement par leur traitement des options longues. Depuis la version 1.3, ça a été simplifié au profit d’une classe unique DefaultParser.

Hiérarchie des parsers de Apache Common CLI"

La méthode parse() renvoie un objet de type CommandLine qui nous permettra de lire toutes les options et tous les arguments.

CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);

Lecture des options et arguments

Le plus facile reste à faire : lire les options et arguments…​ Pour cela, on utilise l’objet de type CommandLine produit par le parser, et on lui demande les options, avec le méthode hasOption(…​) ou getOptions(…​), puis les valeurs avec la méthode getOptionValue(…​) de CommandLine ou getValue() de Option.

 // Option obligatoire, je ne teste donc pas sa présence
 outFile = cmd.getOptionValue('o');

 // Option facultative, je teste sa présence
 if (cmd.hasOption('a')) {
   aValue = cmd.getOptionValue('a');
 }
 // Option facultative, je passe une valeur par défaut
 bValue = cmd.getOptionValue('b', "Valeur par défaut pour b") ;

Erreurs de validation

En cas d’erreur lors de l’analyse des arguments, une ParseException se produit et fournit un message d’erreur qui indique quelles options posent problème.

 System.err.println("Parsing failed : " + e.getMessage());

La ParseException a cependant le défaut de ne proposer qu’un message en anglais, sans donner de détail sur les options incriminées. Pour avoir plus de détails, il faut accéder aux sous-classes.

Hiérarchie d’exceptions de Apache Common CLI"
  • MissingOptionException est levée lorsqu’il manque une ou plusieurs options obligatoires. La méthode getMissingOptions() permet d’avoir la liste des options manquantes.

  • MissingArgumentException est levée lorsqu’il manque un argument à une option. La méthode getOption() permet de connaître l’option qui nécessite un argument.

  • UnrecognizedOptionException est levée lorsqu’une option non prévue est passée. La méthode getOption() permet de connaître l’option inconnue.

  • AlreadySelectedException est levée lorsque plusieurs options d’un même groupe exclusif sont passées. La méthode getOption() permet de connaître l’option qui a déclenché l’exception et getOptionGroup() permet de connaître le groupe..

  • AmbiguousOptionException est levée lorsqu’on utilise un nom d’option partiel, qui peut correspondre à plusieurs options. La mémthode getMatchingOptions() permet de connaître les options qui correspondent.

Afficher l’aide

La classe HelpFormatter permet d’afficher un texte d’aide qui liste les options et arguments attendus, ainsi que le texte d’accompagnement.

 HelpFormatter formatter = new HelpFormatter();
 formatter.printHelp( "CLI", options );

Cette aide est affichée, de façon classique, sur les options --help ou --usage, et, éventuellement, quand l’utilisateur fait une erreur de saisie.

Conclusion

Cet exemple a été réalisé avec Apache Commons CLI 1.5.

Références :