Picocli – Sencillez para argumentos CLI en Java

CLI (Command Line Interface) es la forma en cómo un programa recibe entrada de datos cuando es ejecutado desde una linea de comandos o consola.

En nuestras aplicaciones es usual que debamos de requerir enviar parámetros y opciones adicionales al iniciarla. El punto de entrada de las aplicaciones Java (Scala y Kotlin también) es el famoso método public static main (String [] args), donde los parámetros y opciones de la aplicación son agregados como elementos en arreglo de String que recibe como argumento.

El cómo manejar esos argumentos, de una manera realmente funcional (indexados, con nombres, opciones opcionales, o con valores por defecto y más) siguiendo estandares que vemos en entornos UNIX, POSIX, va a requerir un considerable esfuerzo. Pero, para eso tenemos utilidades como Picocli.

Picocli, se define así mismo como la manera más fácil de crear una aplicación de comandos enriquecida para entornos JVM y no JVM. Veamos un poco más de esta utilidad.

Getting started

Comencemos creando una clase sencilla, esta va a recibir 3 argumentos, para realizar una operación matemática.

class MyApplication {
    public static void main(String... args) {
        
        if(args.length != 3){
          throw new IlegalArgumentException("3 arguments must be indicated");
        }
        
        Integer firstArg = Integer.parseInt(args[0]);
        Integer secondArg = Integer.parseInt(args[1]);
        String operator = args[2];
        
        if(operator == null || !(operator.equals("sum") || operator.equals("sub")){
          throw new IlegalArgumentException("operator argument should be sum or sub");
        }  
  	    new MyAplication().doLogic(firstArg,secondArg,operator);
    }
  
    public void doLogic(int a, int b, String operator){
            double result;
        switch (operator) {
          case "sum": 
       	    result = a + b; 
            break;
          case "sub": 
            result = a - b;
            break;
          default; 
            throw new IlegalArgumentException("Unexpected operation");
        }
        System.out.println("The result is "+result);
    }
}

y si llamamos ese código desde consola

java -jar myjar.jar 2 3 "sum" 
The result is 5

Como vemos, hemos dedicado algo de esfuerzo para obtener los parámetros, validar que el número de parámetros sea el esperado, además de castear los parametros String a otros tipos de datos, y validar que los datos recibidos sean los que esperamos.

Ahora bien, transformemos el código anterior usando picocli:

@Command(name = "myApplication", 
         mixinStandardHelpOptions = true, 
         version = "1.0",
         description = "Applies the indicated math operation to the arguments")
class MyApplication  implements Callable<Integer> {
    
    @Parameters(index = "0", description = "First argument of the operation")
    private int firstArg; 
  
    @Parameters(index = "1", description = "Second argument of the operation")
    private int secondArg; 
  
    @Option(names = {"-o", "--operator"}, description = "Operation to apply", default = "sum")
    private Operators operator;
    
    public enum Operators { sum, sub }
  
    public static void main(String... args) {
  	    int exitCode = new CommandLine(new MyApplication()).execute(args);
        System.exit(exitCode);
    }
  
    @Override
    public Integer call() throws Exception { // your business logic goes here...
         doLogic(firstArg, secondArg, operator); 
         return 0;
    }
  
    public void doLogic(int a, int b, Operators operator){
            double result;
        switch (operator) {
          case Operators.sum: 
       	    result = a + b; 
            break;
          case Operators.sub: 
            result = a - b;
            break;
          default; 
            throw new IlegalArgumentException("Unexpected operation");
        }
        System.out.println("The result is "+result);
    }
}

Se puede apreciar, que picocli se encarga de obtener los parámetros por nosotros, de castearlo al tipo de dato que necesitamos, incluso podemos usar enumerados para limitar las opciones disponibles. Si queremos llamar a nuestra aplicación tenemos una variedad de opciones para ejecutarla.

java -jar myjar.jar 2 3 -o sum 
The result is 5

java -jar myjar.jar 2 3 -o sub 
The result is -1

java -jar myjar.jar 2 3 --operator sum 
The result is 5

java -jar myjar.jar --operator sum 2 3
The result is 5

java -jar myjar.jar 2 3
The result is 5

Para la operación podemos usar el commando -o , --operator , podemos no indicar el operador y el código utilizará el valor por defecto, la opción -o puede ser indicado antes o después o antes de los otros argumentos.

Otra ventaja que nos proporciona Picocli, es que dado que nuestra clase tiene una descripción en la anotación Command y en las anotaciones Option y Parameter , esto nos genera automáticamente la documentación y la ayuda accesible también desde la linea de comandos. De manera que los usuarios puede ven esta documentación.

Options y Parameters

Un @Option es un argumento que debe tener uno o más nombres. Los nombres de las opciones son case-sensitive, pero esto es modificable si fuera necesario.

@Option(names = "-c", description = "create a new archive")
boolean create;

@Option(names = { "-f", "--file" }, paramLabel = "ARCHIVE", description = "the archive file")
File archive;

Además las opciones pueden ser “interactivas“, es decir, que si el parámetro no se indica en el comando, la aplicación va a solicitar este parámetro como un siguiente paso antes de continuar. Esto es bastante util para las contraseñas.

@Option(names = {"-p", "--password"}, description = "Passphrase", interactive = true)
char[] password;

Cualquier argumento que no es un subcomando o un @Option es entonces considerado como un parámetro. Los parámetros son interpretados de forma posicional, con un indice desde 0 hasta N.

@Parameter(index = "0", description = "Operand 1")
int operand;

@Parameter(index = "1", description = "Message to display")
String message; 

También se puede indicar un rango de indices, que son interpretados y mapeados a un arreglo en nuestro código.

@Parameter(index = "2..*", description = "Message to display")
File[] files;

Tipos de Datos soportados

Hay una gran variedad de tipos de datos soportados, los cuales son validados y por picocli, para que los usemos directo en nuestra lógica. No debemos de preocuparnos por transformar el argumento de String al tipo de dato que necesitamos.

  • Cualquier tipo de primitivo Java (boolean, short, char, int, double, float, long) y sus wrappers.
  • Cualquier enum enum
  • StringStringBuilderCharSequence
  • java.math.BigDecimaljava.math.BigInteger
  • java.nio.Charset
  • java.io.File
  • java.net.InetAddress
  • java.util.regex.Pattern
  • java.util.Date (en formato "yyyy-MM-dd")
  • java.net.URLjava.net.URI
  • java.util.UUID
  • java.lang.Class 
  • java.nio.file.Path 
  • java.time objetos de este paquete: 
    • DurationInstantLocalDateLocalDateTimeLocalTimeMonthDayOffsetDateTimeOffsetTimePeriodYearYearMonthZonedDateTimeZoneIdZoneOffset 
  • java.sql.Connection (con el patron de url -> jdbc:subprotocol:subname)
  • entre otros.

Además se pueden crear sus convertidores propios si fuera necesario. Ver más aquí

Agregando Picocli a mi código

Si maven o gradle es utilizado, entonces la siguiente dependencia debe ser agregada.

dependencies {
    implementation 'info.picocli:picocli:4.6.1'
}
<dependency>
  <groupId>info.picocli</groupId>
  <artifactId>picocli</artifactId>
  <version>4.6.1</version>
</dependency>

Además, los creadores de Picocli han creado esta utiliza como un Single class framework (es decir, todo el código está incluido en una sola clase). Por lo tanto, ellos permite que descarguemos el código del siguiente repositorio git repository, y lo agreguemos a nuestro código.

Referencias

Leave a Reply

Your email address will not be published. Required fields are marked *