Analisando Parâmetros da Linha de Comandos com o JCommander

Analisando Parâmetros da Linha de Comandos com o JCommander

1. Visão geral

Neste tutorial,we’ll learn how to use JCommander to parse command-line parameters. Vamos explorar vários de seus recursos enquanto construímos um aplicativo de linha de comando simples.

2. Por que o JCommander?

“Because life is too short to parse command line parameters” - Cédric Beust

JCommander, criado por Cédric Beust, é umannotation-based library forparsing command-line parameters. Isso pode reduzir o esforço de criação de aplicativos de linha de comando e nos ajudar a fornecer uma boa experiência para o usuário.

Com o JCommander, podemos descarregar tarefas complicadas, como análise, validação e conversões de tipo, para nos permitir focar em nossa lógica de aplicativo.

3. Configurando o JCommander

3.1. Configuração do Maven

Vamos começar adicionando a dependênciajcommander em nossopom.xml:


    com.beust
    jcommander
    1.78

3.2. Olá Mundo

Vamos criar umHelloWorldApp simples que pega uma única entrada chamadanamee imprime uma saudação,“Hello <name>”.

ComoJCommander binds command-line arguments to fields in a Java class, primeiro definiremos uma classeHelloWorldArgs com um camponame anotado com@Parameter:

class HelloWorldArgs {

    @Parameter(
      names = "--name",
      description = "User name",
      required = true
    )
    private String name;
}

Agora, vamos usar a classeJCommander para analisar os argumentos da linha de comando e atribuir os campos em nosso objetoHelloWorldArgs:

JCommander helloCmd = JCommander.newBuilder()
  .addObject(new HelloWorldArgs())
  .build();
helloCmd.parse(args);
System.out.println("Hello " + jArgs.getName());

Finalmente, vamos invocar a classe principal com os mesmos argumentos do console:

$ java HelloWorldApp --name JavaWorld
Hello JavaWorld

4. Construindo um aplicativo real no JCommander

Agora que estamos funcionando, vamos considerar um caso de uso mais complexo - um cliente de API de linha de comando que interage com um aplicativo de faturamento comoStripe, especialmente oMetered (ou baseado em uso) Cenário de faturamento. Este serviço de cobrança de terceiros gerencia nossas assinaturas e faturamento.

Suponhamos que estejamos administrando um negócio SaaS, no qual nossos clientes compram assinaturas de nossos serviços e são cobrados pelo número de chamadas de API para nossos serviços por mês. Vamos realizar duas operações em nosso cliente:

  • submit: enviar quantidade e preço unitário de uso para um cliente em relação a uma determinada assinatura

  • fetch: buscar cobranças para um cliente com base no consumo de algumas ou todas as suas assinaturas no mês atual - podemos obter essas cobranças agregadas a todas as assinaturas ou discriminadas por cada assinatura

Construiremos o cliente API à medida que analisamos os recursos da biblioteca.

Vamos começar!

5. Definindo um parâmetro

Vamos começar definindo os parâmetros que nosso aplicativo pode usar.

5.1. A anotação@Parameter

Anotando um campo com@Parameter tells JCommander to bind a matching command-line argument to it. @Parameter tem atributos para descrever o parâmetro principal, como:

  • names – um ou mais nomes da opção, por exemplo “–name” ou “-n”

  • description – o significado por trás da opção, para ajudar o usuário final

  • required – se a opção é obrigatória, o padrão éfalse

  • arity - número de parâmetros adicionais que a opção consome

Vamos configurar um parâmetrocustomerId em nosso cenário de faturamento medido:

@Parameter(
  names = { "--customer", "-C" },
  description = "Id of the Customer who's using the services",
  arity = 1,
  required = true
)
String customerId;

Agora, vamos executar nosso comando com o novo parâmetro “–customer”:

$ java App --customer cust0000001A
Read CustomerId: cust0000001A.

Da mesma forma, podemos usar o parâmetro "-C" mais curto para obter o mesmo efeito:

$ java App -C cust0000001A
Read CustomerId: cust0000001A.

5.2. Parâmetros necessários

Quando um parâmetro é obrigatório, o aplicativo sai lançando umParameterException se o usuário não especificá-lo:

$ java App
Exception in thread "main" com.beust.jcommander.ParameterException:
  The following option is required: [--customer | -C]

Devemos notar que, em geral,any error in parsing the parameters results in a ParameterException no JCommander.

6. Tipos incorporados

6.1. InterfaceIStringConverter

JCommander executa a conversão de tipo da entradaString da linha de comando para os tipos Java em nossas classes de parâmetro. The IStringConverter interface handles type conversion of a parameter from String to any arbitrary type. Portanto, todos os conversores integrados do JCommander implementam esta interface.

Pronto para uso, JCommander vem com suporte para tipos de dados comuns, comoString,Integer,Boolean,BigDecimal eEnum.

6.2. Tipos de aridade única

Arity refere-se ao número de parâmetros adicionais que uma opção consome. built-in parameter types have a default arity of one do JCommander, exceto paraBoolean eList. Portanto, tipos comuns comoString,Integer,BigDecimal,Long, e Enum, são tipos de aridade única.

6.3. Boolean Tipo

Fields of type boolean or Boolean don’t need any additional parameter – essas opções têmarity igual a zero.

Vamos ver um exemplo. Talvez desejemos buscar as cobranças de um cliente, discriminadas por assinatura. Podemos adicionar um campobooleanitemized, que éfalse por padrão:

@Parameter(
  names = { "--itemized" }
)
private boolean itemized;

Nosso aplicativo retornaria cobranças agregadas comitemized definido comofalse. Quando invocamos a linha de comando com o parâmetroitemized, definimos o campo comotrue:

$ java App --itemized
Read flag itemized: true.

Isso funciona bem, a menos que tenhamos um caso de uso em que sempre desejamos cobranças discriminadas,, a menos que especificado de outra forma. Poderíamos alterar o parâmetro paranotItemized,, mas pode ser mais claro ser capaz de fornecerfalse como o valor deitemized.

Vamos apresentar esse comportamento usando um valor padrãotrue para o campo e definindo seuarity como um:

@Parameter(
  names = { "--itemized" },
  arity = 1
)
private boolean itemized = true;

Agora, quando especificarmos a opção, o valor será definido comofalse:

$ java App --itemized false
Read flag itemized: false.

7. TiposList

JCommander fornece algumas maneiras de vincular argumentos aList fields.

7.1. Especificando o parâmetro várias vezes

Vamos supor que desejamos obter as cobranças de apenas um subconjunto das assinaturas de um cliente:

@Parameter(
  names = { "--subscription", "-S" }
)
private List subscriptionIds;

O campo não é obrigatório e o aplicativo buscará as cobranças em todas as assinaturas se o parâmetro não for fornecido. No entanto, podemos especificar várias assinaturasby using the parameter name multiple times:

$ java App -S subscriptionA001 -S subscriptionA002 -S subscriptionA003
Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003].

7.2. VinculandoLists usando o Divisor

Em vez de especificar a opção várias vezes, vamos tentar vincular a lista passandoString separados por vírgula:

$ java App -S subscriptionA001,subscriptionA002,subscriptionA003
Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003].

Isso usa um único valor de parâmetro (arity = 1) para representar uma lista. O JCommander usará a classeCommaParameterSplitter para vincular oString separado por vírgula ao nossoList.

7.3. VinculandoLists usando um divisor personalizado

Podemos substituir o divisor padrão implementando a interfaceIParameterSplitter:

class ColonParameterSplitter implements IParameterSplitter {

    @Override
    public List split(String value) {
        return asList(value.split(":"));
    }
}

E, em seguida, mapeando a implementação para o atributosplitter em@Parameter:

@Parameter(
  names = { "--subscription", "-S" },
  splitter = ColonParameterSplitter.class
)
private List subscriptionIds;

Vamos experimentar:

$ java App -S "subscriptionA001:subscriptionA002:subscriptionA003"
Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003].

7.4. Variável ArityLists

Variable arity allows us to declarelists that can take indefinite parameters, up to the next option. Podemos definir o atributovariableArity comotrue para especificar esse comportamento.

Vamos tentar fazer isso para analisar assinaturas:

@Parameter(
  names = { "--subscription", "-S" },
  variableArity = true
)
private List subscriptionIds;

E quando executamos nosso comando:

$ java App -S subscriptionA001 subscriptionA002 subscriptionA003 --itemized
Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003].

O JCommander vincula todos os argumentos de entrada após a opção “-S” ao campo da lista, até a próxima opção ou o final do comando.

7.5. ArityLists fixo

Até agora, vimos listas ilimitadas, onde podemos passar quantos itens de lista desejarmos. Às vezes, podemos querer limitar o número de itens passados ​​para um campoList. Para fazer isso, podemosspecify an integer arity value for a List field to make it bounded:

@Parameter(
  names = { "--subscription", "-S" },
  arity = 2
)
private List subscriptionIds;

A aridade fixa força uma verificação do número de parâmetros passados ​​para uma opçãoList e lança umParameterException em caso de violação:

$ java App -S subscriptionA001 subscriptionA002 subscriptionA003
Was passed main parameter 'subscriptionA003' but no main parameter was defined in your arg class

A mensagem de erro sugere que, como o JCommander esperava apenas dois argumentos, ele tentou analisar o parâmetro de entrada extra "subscriptionA003" como a próxima opção.

8. Tipos personalizados

Também podemos vincular parâmetros escrevendo conversores personalizados. Como os conversores embutidos, os conversores personalizados devem implementar a interfaceIStringConverter.

Vamos escrever um conversor para analisar umISO8601 timestamp:

class ISO8601TimestampConverter implements IStringConverter {

    private static final DateTimeFormatter TS_FORMATTER =
      DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss");

    @Override
    public Instant convert(String value) {
        try {
            return LocalDateTime
              .parse(value, TS_FORMATTER)
              .atOffset(ZoneOffset.UTC)
              .toInstant();
        } catch (DateTimeParseException e) {
            throw new ParameterException("Invalid timestamp");
        }
    }
}

Este código irá analisar a entradaStringe retornar umInstant, lançando umParameterException se houver um erro de conversão. Podemos usar este conversor vinculando-o a um campo do tipoInstant usando o atributoconverter em@Parameter:

@Parameter(
  names = { "--timestamp" },
  converter = ISO8601TimestampConverter.class
)
private Instant timestamp;

Vamos ver em ação:

$ java App --timestamp 2019-10-03T10:58:00
Read timestamp: 2019-10-03T10:58:00Z.

9. Validando parâmetros

O JCommander fornece algumas validações padrão:

  • se os parâmetros necessários são fornecidos

  • se o número de parâmetros especificados corresponder à aridade de um campo

  • se cada parâmetroString pode ser convertido no tipo de campo correspondente

Além disso,we may wish to add custom validations. Por exemplo, vamos supor que os IDs do cliente devem serUUIDs.

Podemos escrever um validador para o campo do cliente que implementa a interfaceIParameterValidator:

class UUIDValidator implements IParameterValidator {

    private static final String UUID_REGEX =
      "[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}";

    @Override
    public void validate(String name, String value) throws ParameterException {
        if (!isValidUUID(value)) {
            throw new ParameterException(
              "String parameter " + value + " is not a valid UUID.");
        }
    }

    private boolean isValidUUID(String value) {
        return Pattern.compile(UUID_REGEX)
          .matcher(value)
          .matches();
    }
}

Então, podemos conectá-lo com o atributovalidateWith do parâmetro:

@Parameter(
  names = { "--customer", "-C" },
  validateWith = UUIDValidator.class
)
private String customerId;

Se chamarmos o comando com um ID de cliente que não seja UUID, o aplicativo sairá com uma mensagem de falha de validação:

$ java App --C customer001
String parameter customer001 is not a valid UUID.

10. Subcomandos

Agora que aprendemos sobre vinculação de parâmetro, vamos juntar tudo para construir nossos comandos.

No JCommander, podemos suportar vários comandos, chamados subcomandos, cada um com um conjunto distinto de opções.

10.1. @Parameters Anotação

Podemos usar@Parameters para definir subcomandos. @Parameters contém o atributocommandNames para identificar um comando.

Vamos modelarsubmitefetch como subcomandos:

@Parameters(
  commandNames = { "submit" },
  commandDescription = "Submit usage for a given customer and subscription, " +
    "accepts one usage item"
)
class SubmitUsageCommand {
    //...
}

@Parameters(
  commandNames = { "fetch" },
  commandDescription = "Fetch charges for a customer in the current month, " +
    "can be itemized or aggregated"
)
class FetchCurrentChargesCommand {
    //...
}

JCommander usa os atributos em@Parameters para configurar os subcomandos, como:

  • commandNames - nome do subcomando; vincula os argumentos da linha de comando à classe anotada com@Parameters

  • commandDescription - documenta o propósito do subcomando

10.2. Adicionando subcomandos aJCommander

Adicionamos os subcomandos aJCommander com o métodoaddCommand:

SubmitUsageCommand submitUsageCmd = new SubmitUsageCommand();
FetchCurrentChargesCommand fetchChargesCmd = new FetchCurrentChargesCommand();

JCommander jc = JCommander.newBuilder()
  .addCommand(submitUsageCmd)
  .addCommand(fetchChargesCmd)
  .build();

O métodoaddCommand registra os subcomandos com seus respectivos nomes, conforme especificado no atributocommandNames da anotação@Parameters.

10.3. Analisando subcomandos

Para acessar a escolha de comando do usuário, devemos primeiro analisar os argumentos:

jc.parse(args);

A seguir, podemos extrair o subcomando comgetParsedCommand:

String parsedCmdStr = jc.getParsedCommand();

Além de identificar o comando, o JCommander vincula o restante dos parâmetros da linha de comandos aos seus campos no subcomando. Agora, basta chamar o comando que queremos usar:

switch (parsedCmdStr) {
    case "submit":
        submitUsageCmd.submit();
        break;

    case "fetch":
        fetchChargesCmd.fetch();
        break;

    default:
        System.err.println("Invalid command: " + parsedCmdStr);
}

11. Ajuda de uso do JCommander

Podemos invocarusage para renderizar um guia de uso. Este é um resumo de todas as opções que nosso aplicativo consome. Em nosso aplicativo, podemos invocar o uso no comando principal ou, alternativamente, em cada um dos dois comandos "enviar" e "buscar" separadamente.

Uma exibição de uso pode nos ajudar de várias maneiras: mostrando as opções de ajuda e durante o tratamento de erros.

11.1. Mostrando opções de ajuda

Podemos vincular uma opção de ajuda em nossos comandos usando um parâmetroboolean junto com o atributohelp definido comotrue:

@Parameter(names = "--help", help = true)
private boolean help;

Então, podemos detectar se “–help” foi passado nos argumentos e chamarusage:

if (cmd.help) {
  jc.usage();
}

Vejamos a saída de ajuda para nosso subcomando “submit”:

$ java App submit --help
Usage: submit [options]
  Options:
  * --customer, -C     Id of the Customer who's using the services
  * --subscription, -S Id of the Subscription that was purchased
  * --quantity         Used quantity; reported quantity is added over the
                       billing period
  * --pricing-type, -P Pricing type of the usage reported (values: [PRE_RATED,
                       UNRATED])
  * --timestamp        Timestamp of the usage event, must lie in the current
                       billing period
    --price            If PRE_RATED, unit price to be applied per unit of
                       usage quantity reported

O métodousage usa os atributos@Parameter comodescription para exibir um resumo útil. Os parâmetros marcados com asterisco (*) são obrigatórios.

11.2. Manipulação de erros

Podemos pegarParameterExceptione chamarusage para ajudar o usuário a entender por que sua entrada estava incorreta. ParameterException contém a instânciaJCommander para exibir a ajuda:

try {
  jc.parse(args);

} catch (ParameterException e) {
  System.err.println(e.getLocalizedMessage());
  jc.usage();
}

12. Conclusion

Neste tutorial, usamos o JCommander para criar um aplicativo de linha de comando. Embora tenhamos abordado muitos dos principais recursos, há mais nodocumentation oficial.

Como de costume, o código-fonte de todos os exemplos está disponívelover on GitHub.