Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll learn how to use JCommander to parse command-line parameters. We'll explore several of its features as we build a simple command-line application.

2. Why JCommander?

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

JCommander, created by Cédric Beust, is an annotation-based library for parsing command-line parameters. It can reduce the effort of building command-line applications and help us provide a good user experience for them.

With JCommander, we can offload tricky tasks such as parsing, validation, and type conversions, to allow us to focus on our application logic.

3. Setting up JCommander

3.1. Maven Configuration

Let's begin by adding the jcommander dependency in our pom.xml:

<dependency>
    <groupId>com.beust</groupId>
    <artifactId>jcommander</artifactId>
    <version>1.78</version>
</dependency>

3.2. Hello World

Let's create a simple HelloWorldApp that takes a single input called name and prints a greeting, “Hello <name>”.

Since JCommander binds command-line arguments to fields in a Java class, we'll first define a HelloWorldArgs class with a field name annotated with @Parameter:

class HelloWorldArgs {

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

Now, let's use the JCommander class to parse the command-line arguments and assign the fields in our HelloWorldArgs object:

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

Finally, let's invoke the main class with the same arguments from the console:

$ java HelloWorldApp --name JavaWorld
Hello JavaWorld

4. Building a Real Application in JCommander

Now that we're up and running, let's consider a more complex use case — a command-line API client that interacts with a billing application such as Stripe, particularly the Metered (or usage-based) Billing scenario. This third-party billing service manages our subscriptions and invoicing.

Let's imagine that we're running a SaaS business, in which our customers buy subscriptions to our services and are billed for the number of API calls to our services per month. We'll perform two operations in our client:

  • submit: Submit quantity and unit price of usage for a customer against a given subscription
  • fetch: Fetch charges for a customer based on the consumption on some or all of their subscriptions in the current month — we can get these charges aggregated over all the subscriptions or itemized by each subscription

We'll build the API client as we go through the library's features.

Let's begin!

5. Defining a Parameter

Let's begin by defining the parameters that our application can use.

5.1. The @Parameter Annotation

Annotating a field with @Parameter tells JCommander to bind a matching command-line argument to it. @Parameter has attributes to describe the main parameter, such as:

  • names – one or more names of the option, for example “–name” or “-n”
  • description – the meaning behind the option, to help the end user
  • required – whether the option is mandatory, defaults to false
  • arity – number of additional parameters that the option consumes

Let's configure a parameter customerId in our metered-billing scenario:

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

Now, let's execute our command with the new “–customer” parameter:

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

Likewise, we can use the shorter “-C” parameter to achieve the same effect:

$ java App -C cust0000001A
Read CustomerId: cust0000001A.

5.2. Required Parameters

Where a parameter is mandatory, the application exits throwing a ParameterException if the user does not specify it:

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

We should note that, in general, any error in parsing the parameters results in a ParameterException in JCommander.

6. Built-In Types

6.1. IStringConverter Interface

JCommander performs type conversion from the command-line String input into the Java types in our parameter classes. The IStringConverter interface handles the type conversion of a parameter from String to any arbitrary type. So, all of JCommander's built-in converters implement this interface.

Out of the box, JCommander comes with support for common data types such as String, Integer, Boolean, BigDecimal, and Enum.

6.2. Single-Arity Types

Arity relates to the number of additional parameters an option consumes. JCommander's built-in parameter types have a default arity of one, except for Boolean and List. Therefore, common types such as  String, Integer, BigDecimalLong, and Enum, are single-arity types.

6.3. Boolean Type

Fields of type boolean or Boolean don't need any additional parameter – these options have an arity of zero.

Let's look at an example. Perhaps we want to fetch the charges for a customer, itemized by subscription. We can add a boolean field itemized, which is false by default:

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

Our application would return aggregated charges with itemized set to false. When we invoke the command line with the itemized parameter, we set the field to true:

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

This works well unless we have a use case where we always want itemized charges, unless specified otherwise. We could change the parameter to be notItemized, but it might be clearer to be able to provide false as the value of itemized.

Let's introduce this behavior by using a default value true for the field, and setting its arity as one:

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

Now, when we specify the option, the value will be set to false:

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

7. List Types

JCommander provides a few ways of binding arguments to List fields.

7.1. Specifying the Parameter Multiple Times

Let's assume we want to fetch the charges of only a subset of a customer's subscriptions:

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

The field is not mandatory, and the application would fetch the charges across all the subscriptions if the parameter is not supplied. However, we can specify multiple subscriptions by using the parameter name multiple times:

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

7.2. Binding Lists using the Splitter

Instead of specifying the option multiple times, let's try to bind the list by passing a comma-separated String:

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

This uses a single parameter value (arity = 1) to represent a list. JCommander will use the class CommaParameterSplitter to bind the comma-separated String to our List.

7.3. Binding Lists using a Custom Splitter

We can override the default splitter by implementing the IParameterSplitter interface:

class ColonParameterSplitter implements IParameterSplitter {

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

And then mapping the implementation to the splitter attribute in @Parameter:

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

Let's try it out:

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

7.4. Variable Arity Lists

Variable arity allows us to declare lists that can take indefinite parameters, up to the next option. We can set the attribute variableArity as true to specify this behavior.

Let's try this to parse subscriptions:

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

And when we run our command:

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

JCommander binds all input arguments following the option “-S” to the list field, until the next option or the end of the command.

7.5. Fixed Arity Lists

So far we've seen unbounded lists, where we can pass as many list items as we wish. Sometimes, we may want to limit the number of items passed to a List field. To do this, we can specify an integer arity value for a List field to make it bounded:

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

Fixed arity forces a check on the number of parameters passed to a List option and throws a ParameterException in case of a violation:

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

The error message suggests that since JCommander expected only two arguments, it tried to parse the extra input parameter “subscriptionA003” as the next option.

8. Custom Types

We can also bind parameters by writing custom converters. Like built-in converters, custom converters must implement the IStringConverter interface.

Let's write a converter for parsing an ISO8601 timestamp:

class ISO8601TimestampConverter implements IStringConverter<Instant> {

    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");
        }
    }
}

This code will parse the input String and return an Instant, throwing a ParameterException if there's a conversion error. We can use this converter by binding it to a field of type Instant using the converter attribute in @Parameter:

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

Let's see it in action:

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

9. Validating Parameters

JCommander provides a few default validations:

  • whether required parameters are supplied
  • if the number of parameters specified matches the arity of a field
  • whether each String parameter can be converted into the corresponding field's type

In addition, we may wish to add custom validations. For instance, let's assume that the customer IDs must be UUIDs.

We can write a validator for the customer field that implements the interface IParameterValidator:

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();
    }
}

Then, we can hook it up with the validateWith attribute of the parameter:

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

If we invoke the command with a non-UUID customer Id, the application exits with a validation failure message:

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

10. Sub-Commands

Now that we've learned about parameter binding, let's pull everything together to build our commands.

In JCommander, we can support multiple commands, called sub-commands, each with a distinct set of options.

10.1. @Parameters Annotation

We can use @Parameters to define sub-commands. @Parameters contains the attribute commandNames to identify a command.

Let's model submit and fetch as sub-commands:

@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 uses the attributes in @Parameters to configure the sub-commands, such as:

  • commandNames – name of the sub-command; binds the command-line arguments to the class annotated with @Parameters
  • commandDescription – documents the purpose of the sub-command

10.2. Adding Sub-Commands to JCommander

We add the sub-commands to JCommander with the addCommand method:

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

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

The addCommand method registers the sub-commands with their respective names as specified in the commandNames attribute of @Parameters annotation.

10.3. Parsing Sub-Commands

To access the user's choice of command, we must first parse the arguments:

jc.parse(args);

Next, we can extract the sub-command with getParsedCommand:

String parsedCmdStr = jc.getParsedCommand();

In addition to identifying the command, JCommander binds the rest of the command-line parameters to their fields in the sub-command. Now, we just have to call the command we want to use:

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

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

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

11. JCommander Usage Help

We can invoke usage to render a usage guide. This is a summary of all the options that our application consumes. In our application, we can invoke usage on the main command, or alternatively, on each of the two commands “submit” and “fetch” separately.

A usage display can help us in a couple of ways: showing help options and during error handling.

11.1. Showing Help Options

We can bind a help option in our commands using a boolean parameter along with the attribute help set to true:

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

Then, we can detect if “–help” has been passed in the arguments, and call usage:

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

Let's see the help output for our “submit” sub-command:

$ 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

The usage method uses the @Parameter attributes such as description to display a helpful summary. Parameters marked with an asterisk (*) are mandatory.

11.2. Error Handling

We can catch the ParameterException and call usage to help the user understand why their input was incorrect. ParameterException contains the JCommander instance to display the help:

try {
  jc.parse(args);

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

12. Conclusion

In this tutorial, we used JCommander to build a command-line application. While we covered many of the major features, there's more in the official documentation.

As usual, the source code for all the examples is available over on GitHub.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

Leave a Reply

avatar
  Subscribe  
Notify of