Traitement des arguments de ligne de commande en Java: cas clos

De nombreuses applications Java démarrées à partir de la ligne de commande prennent des arguments pour contrôler leur comportement. Ces arguments sont disponibles dans l'argument tableau de chaînes transmis à la main()méthode statique de l'application . En règle générale, il existe deux types d'arguments: les options (ou commutateurs) et les arguments de données réels. Une application Java doit traiter ces arguments et effectuer deux tâches de base:

  1. Vérifiez si la syntaxe utilisée est valide et prise en charge
  2. Récupérer les données réelles nécessaires à l'application pour effectuer ses opérations

Souvent, le code qui effectue ces tâches est fait sur mesure pour chaque application et nécessite donc un effort substantiel à la fois pour créer et pour maintenir, surtout si les exigences vont au-delà de simples cas avec seulement une ou deux options. La Optionsclasse décrite dans cet article implémente une approche générique pour gérer facilement les situations les plus complexes. La classe permet une définition simple des options requises et des arguments de données, et fournit des vérifications de syntaxe approfondies et un accès facile aux résultats de ces vérifications. De nouvelles fonctionnalités Java 5 telles que les génériques et les énumérations de type sécurisé ont également été utilisées pour ce projet.

Types d'arguments de ligne de commande

Au fil des ans, j'ai écrit plusieurs outils Java qui prennent des arguments de ligne de commande pour contrôler leur comportement. Au début, j'ai trouvé ennuyeux de créer et de maintenir manuellement le code pour traiter les différentes options. Cela a conduit au développement d'une classe prototype pour faciliter cette tâche, mais cette classe avait certes ses limites car, en y regardant attentivement, le nombre de variétés différentes possibles pour les arguments de ligne de commande s'est avéré important. Finalement, j'ai décidé de développer une solution générale à ce problème.

En développant cette solution, j'ai dû résoudre deux problèmes principaux:

  1. Identifiez toutes les variétés dans lesquelles les options de ligne de commande peuvent apparaître
  2. Trouvez un moyen simple de permettre aux utilisateurs d'exprimer ces variétés lors de l'utilisation de la classe encore à développer

L'analyse du problème 1 a conduit aux observations suivantes:

  • Options de ligne de commande contrairement aux arguments de données de ligne de commande: commencez par un préfixe qui les identifie de manière unique. Les exemples de préfixes incluent un tiret ( -) sur les plates-formes Unix pour des options comme -aou une barre oblique ( /) sur les plates-formes Windows.
  • Les options peuvent être de simples commutateurs (c'est-à-dire, -apeuvent être présents ou non) ou prendre une valeur. Un exemple est:

    java MyTool -a -b logfile.inp 
  • Les options qui prennent une valeur peuvent avoir des séparateurs différents entre la clé d'option réelle et la valeur. Ces séparateurs peuvent être un espace, un deux-points ( :) ou un signe égal ( =):

    java MyTool -a -b logfile.inp java MyTool -a -b: logfile.inp java MyTool -a -b = logfile.inp 
  • Les options prenant une valeur peuvent ajouter un niveau de complexité supplémentaire. Considérez la façon dont Java prend en charge la définition des propriétés d'environnement à titre d'exemple:

    java -Djava.library.path = / usr / lib ... 
  • Ainsi, au-delà de la clé d'option réelle ( D), du séparateur ( =) et de la valeur réelle de l'option ( /usr/lib), un paramètre supplémentaire ( java.library.path) peut prendre n'importe quel nombre de valeurs (dans l'exemple ci-dessus, de nombreuses propriétés d'environnement peuvent être spécifiées à l'aide de cette syntaxe ). Dans cet article, ce paramètre est appelé «détail».
  • Les options ont également une propriété de multiplicité: elles peuvent être obligatoires ou facultatives, et le nombre de fois qu'elles sont autorisées peut également varier (par exemple, exactement une fois, une ou plusieurs, ou d'autres possibilités).
  • Les arguments de données sont tous des arguments de ligne de commande qui ne commencent pas par un préfixe. Ici, le nombre acceptable de ces arguments de données peut varier entre un nombre minimum et un nombre maximum (qui ne sont pas nécessairement identiques). De plus, une application nécessite généralement que ces arguments de données soient les derniers sur la ligne de commande, mais cela ne doit pas toujours être le cas. Par exemple:

    java MyTool -a -b = logfile.inp data1 data2 data3 // Toutes les données à la fin 

    ou

    java MyTool -a data1 data2 -b = logfile.inp data3 // Peut être acceptable pour une application 
  • Les applications plus complexes peuvent prendre en charge plusieurs ensembles d'options:

    java MyTool -a -b datafile.inp java MyTool -k [-verbose] foo bar duh java MyTool -check -verify logfile.out 
  • Enfin, une application peut choisir d'ignorer les options inconnues ou peut considérer ces options comme une erreur.

Ainsi, en concevant un moyen de permettre aux utilisateurs d'exprimer toutes ces variétés, j'ai proposé le formulaire d'options générales suivant, qui sert de base à cet article:

[[]] 

Ce formulaire doit être combiné avec la propriété de multiplicité comme décrit ci-dessus.

Dans les limites de la forme générale d'une option décrite ci-dessus, la Optionsclasse décrite dans cet article est conçue pour être la solution générale pour tous les besoins de traitement de ligne de commande qu'une application Java peut avoir.

Les classes d'assistance

La Optionsclasse, qui est la classe principale de la solution décrite dans cet article, est fournie avec deux classes d'assistance:

  1. OptionData: Cette classe contient toutes les informations pour une option spécifique
  2. OptionSet: Cette classe contient un ensemble d'options. Optionslui-même peut contenir n'importe quel nombre de ces ensembles

Avant de décrire les détails de ces classes, d'autres concepts importants de la Optionsclasse doivent être introduits.

Énumérations Typesafe

Le préfixe, le séparateur et la propriété de multiplicité ont été capturés par enums, une fonctionnalité fournie pour la première fois par Java 5:

public enum Prefix {DASH ('-'), SLASH ('/'); caractère privé c; Préfixe privé (car c) {this.c = c; } char getName () {return c; }} public enum Separator {COLON (':'), EQUALS ('='), BLANK (''), NONE ('D'); caractère privé c; Séparateur privé (car c) {this.c = c; } char getName () {return c; }} public enum Multiplicité {ONCE, ONCE_OR_MORE, ZERO_OR_ONE, ZERO_OR_MORE; }

L'utilisation d'énumérations présente certains avantages: une sécurité de type accrue et un contrôle précis et sans effort de l'ensemble des valeurs autorisées. Les énumérations peuvent également être utilisées avec des collections génériques.

Notez que les enums Prefixet Separatoront leurs propres constructeurs, permettant la définition d'un caractère réel représentant cette instance d'énumération (par opposition au nom utilisé pour faire référence à l'instance d'énumération particulière). Ces caractères peuvent être récupérés en utilisant les getName()méthodes de ces énumérations , et les caractères sont utilisés pour la java.util.regexsyntaxe de modèle du package. Ce package est utilisé pour effectuer certaines des vérifications de syntaxe dans la Optionsclasse, dont les détails suivront.

L' Multiplicityénumération prend actuellement en charge quatre valeurs différentes:

  1. ONCE: L'option doit se produire exactement une fois
  2. ONCE_OR_MORE: The option has to occur at least once
  3. ZERO_OR_ONCE: The option can either be absent or present exactly once
  4. ZERO_OR_MORE: The option can either be absent or present any number of times

More definitions can easily be added should the need arise.

The OptionData class

The OptionData class is basically a data container: firstly, for the data describing the option itself, and secondly, for the actual data found on the command line for that option. This design is already reflected in the constructor:

OptionData(Options.Prefix prefix, String key, boolean detail, Options.Separator separator, boolean value, Options.Multiplicity multiplicity) 

The key is used as the unique identifier for this option. Note that these arguments directly reflect the findings described earlier: a full option description must have at least a prefix, a key, and multiplicity. Options taking a value also have a separator and might accept details. Note also that this constructor has package access, so applications cannot directly use it. Class OptionSet's addOption() method adds the options. This design principle has the advantage that we have much better control on the actual possible combinations of arguments used to create OptionData instances. For example, if this constructor were public, you could create an instance with detail set to true and value set to false, which is of course nonsense. Rather than having elaborate checks in the constructor itself, I decided to provide a controlled set of addOption() methods.

The constructor also creates an instance of java.util.regex.Pattern, which is used for this option's pattern-matching process. One example would be the pattern for an option taking a value, no details, and a nonblank separator:

pattern = java.util.regex.Pattern.compile(prefix.getName() + key + separator.getName() + "(.+)$"); 

The OptionData class, as already mentioned, also holds the results of the checks performed by the Options class. It provides the following public methods to access these results:

int getResultCount() String getResultValue(int index) String getResultDetail(int index) 

The first method, getResultCount(), returns the number of times an option was found. This method design directly ties in with the multiplicity defined for the option. For options taking a value, this value can be retrieved using the getResultValue(int index) method, where the index can range between 0 and getResultCount() - 1. For value options that also accept details, these can be similarly accessed using the getResultDetail(int index) method.

The OptionSet class

The OptionSet class is basically a container for a set of OptionData instances and also the data arguments found on the command line.

The constructor has the form:

OptionSet(Options.Prefix prefix, Options.Multiplicity defaultMultiplicity, String setName, int minData, int maxData) 

Again, this constructor has package access. Option sets can only be created through the Options class's different addSet() methods. The default multiplicity for the options specified here can be overridden when adding an option to the set. The set name specified here is a unique identifier used to refer to the set. minData and maxData are the minimum and maximum number of acceptable data arguments for this set.

The public API for OptionSet contains the following methods:

General access methods:

String getSetName() int getMinData() int getMaxData() 

Methods to add options:

OptionSet addOption(String key) OptionSet addOption(String key, Multiplicity multiplicity) OptionSet addOption(String key, Separator separator) OptionSet addOption(String key, Separator separator, Multiplicity multiplicity) OptionSet addOption(String key, boolean details, Separator separator) OptionSet addOption(String key, boolean details, Separator separator, Multiplicity multiplicity) 

Methods to access check result data:

java.util.ArrayList getOptionData() OptionData getOption(String key) boolean isSet(String key) java.util.ArrayList getData() java.util.ArrayList getUnmatched() 

Note that the methods for adding options that take a Separator argument create an OptionData instance accepting a value. The addOption() methods return the set instance itself, which allows invocation chaining:

Options options = new Options(args); options.addSet("MySet").addOption("a").addOption("b"); 

After the checks have been performed, their results are available through the remaining methods. getOptionData() returns a list of all OptionData instances, while getOption() allows direct access to a specific option. isSet(String key) is a convenience method that checks whether an options was found at least once on the command line. getData() provides access to the data arguments found, while getUnmatched() lists all options found on the command line for which no matching OptionData instances were found.

The Options class

Options is the core class with which applications will interact. It provides several constructors, all of which take the command line argument string array that the main() method provides as the first argument:

Options(String args[]) Options(String args[], int data) Options(String args[], int defMinData, int defMaxData) Options(String args[], Multiplicity defaultMultiplicity) Options(String args[], Multiplicity defaultMultiplicity, int data) Options(String args[], Multiplicity defaultMultiplicity, int defMinData, int defMaxData) Options(String args[], Prefix prefix) Options(String args[], Prefix prefix, int data) Options(String args[], Prefix prefix, int defMinData, int defMaxData) Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity) Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity, int data) Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity, int defMinData, int defMaxData) 

The first constructor in this list is the simplest one using all the default values, while the last one is the most generic.

Table 1: Arguments for the Options() constructors and their meaning

Value Description Default
prefix This constructor argument is the only place where a prefix can be specified. This value is passed on to any option set and any option created subsequently. The idea behind this approach is that within a given application, it proves unlikely that different prefixes will need to be used. Prefix.DASH
defaultMultiplicity This default multiplicity is passed to each option set and used as the default for options added to a set without specifying a multiplicity. Of course, this multiplicity can be overridden for each option added. Multiplicity.ONCE
defMinData defMinData is the default minimum number of supported data arguments passed to each option set, but it can of course be overridden when adding a set. 0
defMaxData defMaxData est le nombre maximum par défaut d'arguments de données pris en charge transmis à chaque ensemble d'options, mais il peut bien sûr être remplacé lors de l'ajout d'un ensemble. 0