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
.
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.
-
MissingOptionException
est levée lorsqu’il manque une ou plusieurs options obligatoires. La méthodegetMissingOptions()
permet d’avoir la liste des options manquantes. -
MissingArgumentException
est levée lorsqu’il manque un argument à une option. La méthodegetOption()
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éthodegetOption()
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éthodegetOption()
permet de connaître l’option qui a déclenché l’exception etgetOptionGroup()
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émthodegetMatchingOptions()
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 :