Procesamiento de argumentos de línea de comando en Java: caso cerrado

Muchas aplicaciones Java iniciadas desde la línea de comandos toman argumentos para controlar su comportamiento. Estos argumentos están disponibles en el argumento de matriz de cadenas que se pasa al main()método estático de la aplicación . Normalmente, hay dos tipos de argumentos: opciones (o conmutadores) y argumentos de datos reales. Una aplicación Java debe procesar estos argumentos y realizar dos tareas básicas:

  1. Compruebe si la sintaxis utilizada es válida y compatible
  2. Recuperar los datos reales necesarios para que la aplicación realice sus operaciones.

A menudo, el código que realiza estas tareas se personaliza para cada aplicación y, por lo tanto, requiere un esfuerzo sustancial tanto para crear como para mantener, especialmente si los requisitos van más allá de los casos simples con solo una o dos opciones. La Optionsclase descrita en este artículo implementa un enfoque genérico para manejar fácilmente las situaciones más complejas. La clase permite una definición simple de las opciones y argumentos de datos requeridos, y proporciona comprobaciones de sintaxis exhaustivas y fácil acceso a los resultados de estas comprobaciones. También se utilizaron para este proyecto nuevas características de Java 5 como genéricos y enumeraciones de seguridad de tipos.

Tipos de argumentos de la línea de comandos

A lo largo de los años, he escrito varias herramientas Java que utilizan argumentos de línea de comandos para controlar su comportamiento. Al principio, me resultaba molesto crear y mantener manualmente el código para procesar las distintas opciones. Esto llevó al desarrollo de una clase prototipo para facilitar esta tarea, pero esa clase ciertamente tenía sus limitaciones ya que, en una inspección más cercana, el número de posibles variedades diferentes para los argumentos de la línea de comando resultó ser significativo. Finalmente, decidí desarrollar una solución general a este problema.

Al desarrollar esta solución, tuve que resolver dos problemas principales:

  1. Identificar todas las variedades en las que pueden aparecer opciones de línea de comandos.
  2. Encuentre una forma sencilla de permitir que los usuarios expresen estas variedades cuando usen la clase aún por desarrollar

El análisis del problema 1 condujo a las siguientes observaciones:

  • Opciones de línea de comando contrarias a los argumentos de datos de línea de comando: comience con un prefijo que las identifique de manera única. Los ejemplos de prefijos incluyen un guión ( -) en plataformas Unix para opciones como -ao una barra inclinada ( /) en plataformas Windows.
  • Las opciones pueden ser simples conmutadores (es decir, -apueden estar presentes o no) o tomar un valor. Un ejemplo es:

    java MyTool -a -b logfile.inp 
  • Las opciones que toman un valor pueden tener diferentes separadores entre la clave de opción real y el valor. Dichos separadores pueden ser un espacio en blanco, dos puntos ( :) o un signo igual ( =):

    Java MyTool -a -b logfile.inp java MyTool -a -b: logfile.inp java MyTool -a -b = logfile.inp 
  • Las opciones que toman un valor pueden agregar un nivel más de complejidad. Considere la forma en que Java admite la definición de propiedades del entorno como ejemplo:

    java -Djava.library.path = / usr / lib ... 
  • Entonces, más allá de la tecla de opción real ( D), el separador ( =) y el valor real de la opción ( /usr/lib), un parámetro adicional ( java.library.path) puede tomar cualquier número de valores (en el ejemplo anterior, se pueden especificar numerosas propiedades del entorno usando esta sintaxis ). En este artículo, este parámetro se denomina "detalle".
  • Las opciones también tienen una propiedad de multiplicidad: pueden ser obligatorias u opcionales, y el número de veces que se permiten también puede variar (como exactamente una vez, una vez o más, u otras posibilidades).
  • Los argumentos de datos son todos los argumentos de la línea de comandos que no comienzan con un prefijo. Aquí, el número aceptable de tales argumentos de datos puede variar entre un número mínimo y máximo (que no son necesariamente iguales). Además, normalmente una aplicación requiere que estos argumentos de datos estén en último lugar en la línea de comandos, pero no siempre es así. Por ejemplo:

    java MyTool -a -b = logfile.inp data1 data2 data3 // Todos los datos al final 

    o

    java MyTool -a data1 data2 -b = logfile.inp data3 // Podría ser aceptable para una aplicación 
  • Las aplicaciones más complejas pueden admitir más de un conjunto de opciones:

    java MyTool -a -b datafile.inp java MyTool -k [-verbose] foo bar duh java MyTool -check -verify logfile.out 
  • Finalmente, una aplicación puede optar por ignorar cualquier opción desconocida o puede considerar tales opciones como un error.

Entonces, al idear una forma de permitir a los usuarios expresar todas estas variedades, se me ocurrió el siguiente formulario de opciones generales, que se utiliza como base para este artículo:

[[]] 

Esta forma debe combinarse con la propiedad de multiplicidad descrita anteriormente.

Dentro de las limitaciones de la forma general de una opción descrita anteriormente, la Optionsclase descrita en este artículo está diseñada para ser la solución general para cualquier necesidad de procesamiento de línea de comandos que pueda tener una aplicación Java.

Las clases de ayuda

La Optionsclase, que es la clase principal de la solución descrita en este artículo, viene con dos clases auxiliares:

  1. OptionData: Esta clase contiene toda la información para una opción específica
  2. OptionSet: Esta clase tiene un conjunto de opciones. Optionssí mismo puede contener cualquier número de tales conjuntos

Antes de describir los detalles de estas clases, se Optionsdeben introducir otros conceptos importantes de la clase.

Enums Typesafe

El prefijo, el separador y la propiedad de multiplicidad han sido capturados por enumeraciones, una característica proporcionada por primera vez por Java 5:

Prefijo público de enumeración {DASH ('-'), SLASH ('/'); carácter privado c; Prefijo privado (carácter c) {this.c = c; } char getName () {return c; }} separador de enumeración pública {COLON (':'), IGUALES ('='), EN BLANCO (''), NINGUNO ('D'); carácter privado c; Separador privado (carácter c) {this.c = c; } char getName () {return c; }} multiplicidad de enumeración pública {UNA VEZ, UNA VEZ_O_MÁS, ZERO_OR_ONE, ZERO_OR_MORE; }

El uso de enumeraciones tiene algunas ventajas: mayor seguridad de tipos y control estricto y sin esfuerzo sobre el conjunto de valores permitidos. Las enumeraciones también se pueden usar convenientemente con colecciones genéricas.

Tenga en cuenta que las enumeraciones Prefixy Separatortienen sus propios constructores, lo que permite la definición de un carácter real que representa esta instancia de enumeración (frente al nombre utilizado para hacer referencia a la instancia de enumeración particular). Estos caracteres se pueden recuperar utilizando los getName()métodos de estas enumeraciones , y los caracteres se utilizan para la java.util.regexsintaxis del patrón del paquete. Este paquete se utiliza para realizar algunas de las comprobaciones de sintaxis en la Optionsclase, cuyos detalles seguirán.

La Multiplicityenumeración admite actualmente cuatro valores diferentes:

  1. ONCE: La opción debe ocurrir exactamente una vez
  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 es el número máximo predeterminado de argumentos de datos admitidos que se pasan a cada conjunto de opciones, pero, por supuesto, se puede anular al agregar un conjunto. 0