Elaborazione degli argomenti della riga di comando in Java: caso chiuso

Molte applicazioni Java avviate dalla riga di comando accettano argomenti per controllare il loro comportamento. Questi argomenti sono disponibili nell'argomento della matrice di stringhe passato al main()metodo statico dell'applicazione . In genere, ci sono due tipi di argomenti: opzioni (o opzioni) e argomenti di dati effettivi. Un'applicazione Java deve elaborare questi argomenti ed eseguire due attività di base:

  1. Verificare se la sintassi utilizzata è valida e supportata
  2. Recuperare i dati effettivi necessari all'applicazione per eseguire le proprie operazioni

Spesso, il codice che esegue queste attività è personalizzato per ciascuna applicazione e quindi richiede uno sforzo notevole sia per la creazione che per la manutenzione, soprattutto se i requisiti vanno oltre i semplici casi con solo una o due opzioni. La Optionsclasse descritta in questo articolo implementa un approccio generico per gestire facilmente le situazioni più complesse. La classe consente una semplice definizione delle opzioni e degli argomenti dei dati richiesti e fornisce controlli sintattici approfonditi e un facile accesso ai risultati di questi controlli. Per questo progetto sono state utilizzate anche nuove funzionalità di Java 5 come generics ed enumerazioni typesafe.

Tipi di argomenti della riga di comando

Nel corso degli anni, ho scritto diversi strumenti Java che utilizzano argomenti della riga di comando per controllare il loro comportamento. All'inizio trovavo fastidioso creare e mantenere manualmente il codice per l'elaborazione delle varie opzioni. Ciò portò allo sviluppo di una classe prototipo per facilitare questo compito, ma quella classe aveva certamente i suoi limiti poiché, a un'attenta ispezione, il numero di possibili varietà differenti per gli argomenti della riga di comando si rivelò significativo. Alla fine, ho deciso di sviluppare una soluzione generale a questo problema.

Nello sviluppare questa soluzione, ho dovuto risolvere due problemi principali:

  1. Identifica tutte le varietà in cui possono verificarsi le opzioni della riga di comando
  2. Trova un modo semplice per consentire agli utenti di esprimere queste varietà quando usano la classe ancora da sviluppare

L'analisi del problema 1 ha portato alle seguenti osservazioni:

  • Opzioni della riga di comando contrarie agli argomenti dei dati della riga di comando: iniziare con un prefisso che le identifica in modo univoco. Gli esempi di prefisso includono un trattino ( -) sulle piattaforme Unix per opzioni come -ao una barra ( /) sulle piattaforme Windows.
  • Le opzioni possono essere semplici interruttori (cioè, -apossono essere presenti o meno) o assumere un valore. Un esempio è:

    java MyTool -a -b logfile.inp 
  • Le opzioni che assumono un valore possono avere separatori diversi tra la chiave dell'opzione effettiva e il valore. Tali separatori possono essere uno spazio vuoto, due punti ( :) o un segno di uguale ( =):

    java MyTool -a -b logfile.inp java MyTool -a -b: logfile.inp java MyTool -a -b = logfile.inp 
  • Le opzioni che assumono un valore possono aggiungere un ulteriore livello di complessità. Considera il modo in cui Java supporta la definizione delle proprietà dell'ambiente come esempio:

    java -Djava.library.path = / usr / lib ... 
  • Quindi, oltre alla chiave di opzione effettiva ( D), al separatore ( =) e al valore effettivo dell'opzione ( /usr/lib), un parametro aggiuntivo ( java.library.path) può assumere qualsiasi numero di valori (nell'esempio sopra, numerose proprietà dell'ambiente possono essere specificate usando questa sintassi ). In questo articolo, questo parametro è denominato "dettaglio".
  • Le opzioni hanno anche una proprietà di molteplicità: possono essere obbligatorie o facoltative e anche il numero di volte in cui sono consentite può variare (ad esempio esattamente una volta, una o più volte o altre possibilità).
  • Gli argomenti dei dati sono tutti gli argomenti della riga di comando che non iniziano con un prefisso. Qui, il numero accettabile di tali argomenti di dati può variare tra un numero minimo e un numero massimo (che non sono necessariamente gli stessi). Inoltre, in genere un'applicazione richiede che questi argomenti di dati siano gli ultimi sulla riga di comando, ma non è sempre così. Per esempio:

    java MyTool -a -b = logfile.inp data1 data2 data3 // Tutti i dati alla fine 

    o

    java MyTool -a data1 data2 -b = logfile.inp data3 // Potrebbe essere accettabile per un'applicazione 
  • Le applicazioni più complesse possono supportare più di un set di opzioni:

    java MyTool -a -b datafile.inp java MyTool -k [-verbose] foo bar duh java MyTool -check -verify logfile.out 
  • Infine, un'applicazione potrebbe scegliere di ignorare eventuali opzioni sconosciute o potrebbe considerare tali opzioni come un errore.

Quindi, nell'ideare un modo per consentire agli utenti di esprimere tutte queste varietà, ho creato il seguente modulo di opzioni generali, che viene utilizzato come base per questo articolo:

[[]] 

Questa forma deve essere combinata con la proprietà molteplicità come descritto sopra.

Entro i vincoli della forma generale di un'opzione descritta sopra, la Optionsclasse descritta in questo articolo è progettata per essere la soluzione generale per qualsiasi esigenza di elaborazione della riga di comando che potrebbe avere un'applicazione Java.

Le classi helper

La Optionsclasse, che è la classe principale per la soluzione descritta in questo articolo, include due classi helper:

  1. OptionData: Questa classe contiene tutte le informazioni per un'opzione specifica
  2. OptionSet: Questa classe contiene una serie di opzioni. Optionsstesso può contenere un numero qualsiasi di tali insiemi

Prima di descrivere i dettagli di queste classi, è Optionsnecessario introdurre altri importanti concetti della classe.

Typesafe enumerazioni

Il prefisso, il separatore e la proprietà molteplicità sono stati catturati da enumerazioni, una funzionalità fornita per la prima volta da Java 5:

enumerazione pubblica Prefisso {DASH ('-'), SLASH ('/'); char privato c; Prefisso privato (char c) {this.c = c; } char getName () {return c; }} enumerazione pubblica Separatore {COLON (':'), EQUALS ('='), BLANK (''), NONE ('D'); char privato c; Separatore privato (char c) {this.c = c; } char getName () {return c; }} enumerazione pubblica Multiplicity {ONCE, ONCE_OR_MORE, ZERO_OR_ONE, ZERO_OR_MORE; }

L'utilizzo di enumerazioni presenta alcuni vantaggi: maggiore sicurezza dei tipi e controllo stretto e senza sforzo sull'insieme dei valori consentiti. Le enumerazioni possono anche essere convenientemente utilizzate con raccolte generiche.

Si noti che le enumerazioni Prefixe Separatorhanno i propri costruttori, consentendo la definizione di un carattere effettivo che rappresenta questa istanza di enum (rispetto al nome usato per fare riferimento alla particolare istanza di enum). Questi caratteri possono essere recuperati utilizzando i getName()metodi di queste enumerazioni ei caratteri vengono utilizzati per la java.util.regexsintassi del modello del pacchetto. Questo pacchetto viene utilizzato per eseguire alcuni dei controlli di sintassi nella Optionsclasse, i cui dettagli seguiranno.

L' Multiplicityenum attualmente supporta quattro diversi valori:

  1. ONCE: L'opzione deve verificarsi esattamente una volta
  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 è il numero massimo predefinito di argomenti di dati supportati passati a ciascun set di opzioni, ma ovviamente può essere sovrascritto quando si aggiunge un set. 0