Commons CLI da Apache

Figura 1: Commons CLI da Apache


Introdução

O tratamento de parâmetros de linha de comando é uma atividade bastante rotineira em nossa área. Pensando nisso, foram criadas algumas APIs para implementar essa tarefa, facilitando a vida do desenvolvedor. Veremos nesse artigo como usar as APIs Commons CLI e JCommander.

Apache Commons CLI

O Commons CLI é uma biblioteca do grupo Apache, que realiza o parse de opções de entrada passadas ao programa. Essa biblioteca suporta os seguintes estilos de opções de linha:

  • Estilo POSIX: (tar -zxvf foo.tar.gz)
  • Estilo GNU: (du --human-readable --max-depth=1)
  • Propriedades Java: (java -Djava.awt.headless=true)
  • Opções curtas com valor anexado: (gcc -O2 foo.c)
  • Opçoes longas com um único hífen: (ant -projecthelp)

No processamento da linha de comando, temos 3 estágios: definição, parse e o famoso help/interrogação.

Na fase de definição, determinamos qual estilo será usado (POSIX, GNU, etc), quais parâmetros são suportados, tipo dos parâmetros, se são opcionais ou não, etc. Para definirmos os parâmetros, utilizamos as classes Option e Options.

Na fase de parse, realizamos o tratamento da linha de comando. Nesse estágio, utilizamos as classes CommandLineParser e CommandLine.

Na fase de help/interrogação, fornecemos informações detalhadas ao usuário sobre as opções disponíveis. Para isso usamos a classe HelpFormatter.

O download da biblioteca pode ser feita em: http://commons.apache.org/cli/.

Nota: Nesse artigo usaremos a versão 1.2.

Listagem 1: Classe de Teste para parâmetros no estilo GNU


import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;


/**
 *
 * @author marcelo
 */
public class GNUStyleTest {    
    
    /**
     * Define as opcoes que o programa aceita
     *
     * @return Options 
     */
    private static Options defineOptions() {
        final Options options = new Options();
        
        // Podemos adicionar uma opção através do método addOption
        options.addOption("d", "debug", false, "Opcao para habilitar modo debug");
        
        // Ou, criando um objeto Option
        Option option1 = new Option("h", "help", false, "Opcao para exibir help");
        options.addOption(option1);
        
        // Ou ainda, usando OptionBuilder - Padrão Builder
        Option option2 = OptionBuilder.withLongOpt("print").
                           hasArg(true).isRequired().withDescription("Opcao para imprimir um valor").create("p");
        options.addOption(option2);
        
        return options;
    }

    /**
     * Imprime Help
     */    
    private static void printHelp(final Options options) {        
        final String cmdLineSyntax = "java -cp CliTest.jar";
        final HelpFormatter helpFormatter = new HelpFormatter();
        helpFormatter.printHelp(cmdLineSyntax, options);
    }
    
    /**
     * Efetua o parse das opções
     */     
    private static void parseOptions(final Options options, final String[] args) {
        final CommandLineParser cmdLineGnuParser = new GnuParser();
        CommandLine commandLine;
        boolean debug;
        
        try {
            commandLine = cmdLineGnuParser.parse(options, args);
            
            // Exibir help?
            if (commandLine.hasOption("h")) {
                printHelp(options);
                System.exit(1);
            }       
            
            // Modo debug ativo?
            debug = commandLine.hasOption("d");
            
            // Opção p presente?
            if (commandLine.hasOption("p")) {
                
                if(debug) {
                    System.out.println("Ativando debug...");
                }
                
                // Recupera valor do parâmetro
                String argument = commandLine.getOptionValue("p");
                // Imprime
                System.out.println("Argumento = [" + argument + ']');
            }
        } catch (ParseException parseException) {
            System.err.println(parseException.getMessage());
        }        
    }
    
    public static void main(String ... args) {
        
        final Options options = defineOptions();
        if (args.length < 1) {
            printHelp(options);
        } else {
            parseOptions(options, args);
        }
    }
}

Analisando o código:

Fase de definição

Listagem 2: Definindo as opções


     private static Options defineOptions() {
        final Options options = new Options();
        
        // Podemos adicionar uma opção através do método addOption
        options.addOption("d", "debug", false, "Opcao para habilitar modo debug");
        
        // Ou, criando um objeto Option
        Option option1 = new Option("h", "help", false, "Opcao para exibir help");
        options.addOption(option1);
        
        // Ou ainda, usando OptionBuilder - Padrão Builder
        Option option2 = OptionBuilder.withLongOpt("print").
                           hasArg(true).isRequired().withDescription("Opcao para imprimir um valor").create("p");
        options.addOption(option2);
        
        return options;
    }

A classe Options representa a lista de todas as opções que a aplicação suporta. Cada opção é definida através da classe Option. Pelo código, verificamos 3 maneiras de se criar um Option:

Listagem 3: Alternativa 1: Através do método addOption da classe Options


    options.addOption("d", "debug", false, "Opcao para habilitar modo debug");

No caso, estamos dizendo que o nome curto do parâmetro é d, o nome longo é debug, esse parâmetro não contém argumento (false) e a descrição do parâmetro.

A classe Option possui 3 construtores:

  • Option(String opt, boolean hasArg, String description)
  • Option(String opt, String description)
  • Option(String opt, String longOpt, boolean hasArg, String description)

Onde:

  • opt – nome curto do parâmetro
  • hasArg – se o parâmetro possui valor/argumento associado
  • description – descrição do parâmetro
  • longOpt – nome longo do parâmetro

Listagem 4: Alternativa 2: Criando um objeto Option e adicionando-o


    Option option1 = new Option("h", "help", false, "Opcao para exibir help");
    options.addOption(option1);

Ao invés de usar o método addOption passando valores, criamos o objeto Option, e pelo seu construtor configuramos algumas de suas propriedades, e depois o adicionamos ao objeto Options.

Listagem 5: Alternativa 3: Usando o OptionBuilder


        Option option2 = OptionBuilder.withLongOpt("print").
                           hasArg(true).isRequired().withDescription("Opcao para imprimir um valor").create("p");
        options.addOption(option2);

Nesse caso, estamos informando que o nome curto do parâmetro é p, o nome longo é print, esse parâmetro requer argumento (hasArg(true)), o campo é obrigatório (isRequired()) e a descrição do parâmetro.

Note que através da classe OptionBuilder, podemos configurar todos os atributos possíveis de Option (obrigatório/não-obrigatório, com/sem argumentos, etc). Através do padrão Builder, utilizamos elegantemente interface fluente para construir/configurar o objeto.

Fase de Interrogação/Help

Listagem 6: Help do programa


    /**
     * Imprime Help
     */    
    private static void printHelp(final Options options) {        
        final String cmdLineSyntax = "java -cp CliTest.jar";
        final HelpFormatter helpFormatter = new HelpFormatter();
        helpFormatter.printHelp(cmdLineSyntax, options);
    }

Listagem 7: Saída (No caso, ao executar o programa sem passar nenhum parâmetro)


usage: java -cp CliTest.jar
 -d,--debug    Opcao para habilitar modo debug
 -h,--help     Opcao para exibir help
 -p,--print    Opcao para imprimir um valor
A classe HelpFormatter imprime todas as opções definidas e contidas no objeto Options, além de imprimir a linha de comando passada como argumento.

Fase de Parse

Listagem 8: Método de Parse no estilo GNU


    private static void parseOptions(final Options options, final String[] args) {
        final CommandLineParser cmdLineGnuParser = new GnuParser();
        CommandLine commandLine;
        boolean debug;
        
        try {
            commandLine = cmdLineGnuParser.parse(options, args);
            
            // Exibir help?
            if (commandLine.hasOption("h")) {
                printHelp(options);
                System.exit(1);
            }       
            
            // Modo debug ativo?
            debug = commandLine.hasOption("d");
            
            // Opção p presente?
            if (commandLine.hasOption("p")) {
                
                if(debug) {
                    System.out.println("Ativando debug...");
                }
                
                // Recupera valor do parâmetro
                String argument = commandLine.getOptionValue("p");
                // Imprime
                System.out.println("Argumento = [" + argument + ']');
            }
        } catch (ParseException parseException) {
            System.err.println(parseException.getMessage());
        }        
    }

Esse método é responsável por efetuar o parse dos parâmetros da linha de comando e realizar as ações necessárias.

Na linha:

Listagem 9: Definindo tipo de parse


     final CommandLineParser cmdLineGnuParser = new GnuParser();

Definimos o tipo de Parser a ser utilizado, no caso GnuParser. Além desse, temos também o PosixParse e o BasicParser. Todos os Parsers implementam a interface CommandLineParser.

Listagem 10: Criando objeto CommandLine


     commandLine = cmdLineGnuParser.parse(options, args);

A linha acima cria o objeto CommandLine, que é uma classe de conveniência que dá acesso ao resultado do parse, ou seja, todos os parâmetros e valores associados a eles.

O método parse, entre outras coisas, valida se todos os atributos obrigatórios estão presentes, se os parâmetros com argumento possuem valor, etc. Em caso de erro, ele lança a checked exception org.apache.commons.cli.ParseException.

Listagem 11: Processando parâmetros


    // Exibir help?
    if (commandLine.hasOption("h")) {
        printHelp(options);
        System.exit(1);
    }       
            
    // Modo debug ativo?
    debug = commandLine.hasOption("d");
           
    // Opção p presente?
    if (commandLine.hasOption("p")) {
                
        if(debug) {
            System.out.println("Ativando debug...");
        }
                
        // Recupera valor do parâmetro
        String argument = commandLine.getOptionValue("p");
        // Imprime
        System.out.println("Argumento = [" + argument + ']');
    }

Através do método hasOption, verificamos se uma opção está presente. E para acessar um valor de uma opção com argumento, utiliza-se o método getOptionValue().

Vejamos agora alguns exemplos de uso:

Listagem 12: Exemplos de uso



// Passando p sem argumento
java -jar CliTest.jar --p
Missing argument for option: p

// Passando p com argumento
java -jar CliTest.jar --p=teste
Argumento = [teste]

// Passando help: Observe que o parâmetro p é obrigatório, por isso o erro
java -jar CliTest.jar –h
Missing required option: p

// Passando help com p
java -jar CliTest.jar –h --p=teste
usage: java -cp CliTest.jar
 -d,--debug    Opcao para habilitar modo debug
 -h,--help     Opcao para exibir help
 -p,--print    Opcao para imprimir um valor

// Passando help com nome longo
java -jar CliTest.jar –help --p=teste
usage: java -cp CliTest.jar
 -d,--debug    Opcao para habilitar modo debug
 -h,--help     Opcao para exibir help
 -p,--print    Opcao para imprimir um valor

JCommander

Desenvolvido pelo criador do TestNG, Cédric Beust, o JCommander é uma alternativa simples e poderosa para implementar tratamento de parâmetros. Diferente do Commons CLI, nessa solução é usada Annotations para definir os parâmetros de entrada.

Para baixar a biblioteca:

Através do Git: https://github.com/cbeust/jcommander.

Ou via Maven:

<dependency><br/>
<groupId>com.beust</groupId><br/>
<artifactId>jcommander</artifactId><br/>
<version>1.30</version><br/>
</dependency><br/>

Listagem 13: Exemplo 1 de JCommander


import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import java.util.ArrayList;
import java.util.List;
 
public class JCommanderTest1 {
    
  @Parameter
  private List<String> parameters = new ArrayList<String>();
 
  @Parameter(names = { "-log", "-verbose" }, description = "Level of verbosity")
  private Integer verbose = 1;
 
  @Parameter(names = "-debug", description = "Debug mode")
  private boolean debug = false;
  
  @Parameter(names = "-print", description = "Print mode")
  private String print;
  
  public static void main(String ... args) {
      
    // Teste 1
    // O sinal de menos indica que eh parametro
    String[] argv = { "-log", "2", "-debug", "-print", "Hello World" };
    JCommanderTest1 jct = new JCommanderTest1();
    new JCommander(jct, argv);          
    System.out.println("Teste 1");
    System.out.println("jct.debug = " + jct.debug);
    System.out.println("jct.verbose = " + jct.verbose);
    System.out.println("jct.print = " + jct.print);
    System.out.println();
    
    // Teste 2
    // Sem o sinal de menos no log, ele ignora o valor 3 (imprime 1)
    argv = new String[] { "log", "3"};
    jct = new JCommanderTest1();
    new JCommander(jct, argv);          
    System.out.println("Teste 2");
    System.out.println("jct.verbose = " + jct.verbose); 
    System.out.println();
  
    // Teste 3
    // Usando verbose ao invés de log
    argv = new String[] { "-verbose", "3"};    
    jct = new JCommanderTest1();
    new JCommander(jct, argv);          
    System.out.println("Teste 3");
    System.out.println("jct.verbose = " + jct.verbose); 
    System.out.println();
    
    // Teste 4
    // Usando debug
    argv = new String[] { "-debug" };    
    jct = new JCommanderTest1();
    new JCommander(jct, argv);          
    System.out.println("Teste 4");
    System.out.println("jct.debug = " + jct.debug); 
    System.out.println();    
    
    // Teste 5
    // Todos os parametros que não são opções são gravados em parameters
    argv = new String[] { "-debug", "a", "b", "c" };    
    jct = new JCommanderTest1();
    new JCommander(jct, argv);          
    System.out.println("Teste 5");
    System.out.println("jct.parameters = " + jct.parameters); 
    System.out.println();        
  }    
}

Listagem 14: Saídas do Exemplo 1 de JCommander


Teste 1
jct.debug = true
jct.verbose = 2
jct.print = Hello World

Teste 2
jct.verbose = 1

Teste 3
jct.verbose = 3

Teste 4
jct.debug = true

Teste 5
jct.parameters = [a, b, c]

A API é realmente muito simples. Basta anotarmos os atributos da classe com a anotação @Parameter, configurando os diversos aspectos do parâmetro, como nome(s), descrição, etc.

Listagem 14: Exemplo 2 de JCommander


import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import java.util.ArrayList;
import java.util.List;

@Parameters(separators = "=")
public class JCommanderTest2 {

    @Parameter
    private List<String> parameters = new ArrayList<String>();
    @Parameter(names = {"-dir", "--directory"}, description = "Directory")
    private String dir;
    @Parameter(names = "-debug", description = "Debug mode", required = true)
    private boolean debug = false;
    @Parameter(names = "-age", description = "Age")
    private Integer age;

    public static void main(String... args) {

        // Teste 1
        // Usando dir (erro: debug é obrigatório)
        String[] argv = {"-dir=/etc"};
        JCommanderTest2 jct = new JCommanderTest2();
        System.out.println("Teste 1");
        try {
            new JCommander(jct, argv);
        } catch (ParameterException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("jct.dir = " + jct.dir);
        System.out.println();

        // Teste 2
        // Usando dir
        argv = new String[]{"-dir=/etc", "-debug=true"};
        jct = new JCommanderTest2();
        System.out.println("Teste 2");
        try {
            new JCommander(jct, argv);
        } catch (ParameterException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("jct.dir = " + jct.dir);
        System.out.println("jct.debug = " + jct.debug);
        System.out.println();

        // Teste 3
        // Usando directory
        argv = new String[]{"--directory=/etc", "-debug=true"};
        jct = new JCommanderTest2();
        System.out.println("Teste 3");
        try {
            new JCommander(jct, argv);
        } catch (ParameterException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("jct.dir = " + jct.dir);
        System.out.println("jct.debug = " + jct.debug);
        System.out.println();

        // Teste 4
        // Passando um valor não-inteiro para age (erro)
        argv = new String[]{"-age=ABC", "-debug=true"};
        jct = new JCommanderTest2();
        System.out.println("Teste 4");
        try {
            new JCommander(jct, argv);
        } catch (ParameterException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("jct.age = " + jct.age);
        System.out.println("jct.debug = " + jct.debug);
        System.out.println();
    }
}

Listagem 15: Saídas do Exemplo 2 de JCommander


Teste 1
The following option is required: -debug 
jct.dir = /etc

Teste 2
jct.dir = /etc
jct.debug = true

Teste 3
jct.dir = /etc
jct.debug = true

Teste 4
"-age": couldn't convert "ABC" to an integer
jct.age = null
jct.debug = false

Fácil, não? Os exemplos são bem intuitivos e auto-explicativos.

A seguir, um resumo de tudo que a API fornece:

  • Definir novos tipos de opções, via Annotation ou Factory
  • Customização de validação de opções
  • Definir separadores de valor: “=”, “:”
  • Múltiplos nomes para uma opção
  • Múltiplos valores para uma opção
  • Múltiplos descrições
  • Campos obrigatórios
  • Valores default
  • Definição de tipos de valores: Integer, String, Long, etc
  • Parâmetros help
  • Parâmetros do tipo senha
  • Usage
  • Internacionalização

Para ver todos os recursos disponíveis: http://jcommander.org/.

Conclusão

Vimos nesse artigo duas ótimas alternativas para realizar o parse de opções de linha de comando. Apesar de ser menos conhecido que o CLI, o JCommander se mostrou uma ótima opção também (na minha opinião oferece até mais recursos).

Obrigado pessoal, e até a próxima!

Referências: