1. Overview
In this tutorial, we’ll explore the Java Apache Commons CLI library. It’s a framework that empowers developers by helping them build the command line interface (CLI) of existing software tools in an efficient and standard way.
The library can speed up the development of CLIs with its support for defining the CLI options and basic validation of them. It helps parse the command line arguments and their values. Finally, the argument values can be passed to the underlying services implementing the tool.
Notably, the Apache Commons CLI library is also used in several of Apache’s products, including Kafka, Maven, Ant, and Tomcat.
We’ll discuss a few important classes from Apache Commons CLI and then use them in sample programs to showcase its capabilities.
2. Key Concerns of a CLI
CLIs give an edge to tools by helping automate a series of tasks relevant to their domain. Moreover, in today’s world, it’s unimaginable for DevOps engineers to work without CLIs.
Apart from the challenge of the underlying implementation of the tool, all CLIs need to handle some basic requirements:
- Parse command line arguments, extract argument values, and pass them to the underlying services
- Display help information with a certain format
- Display version
- Handle missing required options
- Handle unknown options
- Handle mutually exclusive options
3. Important Classes
Let’s take a look at the important classes of the Apache Commons CLI library:
The classes Option, OptionGroup, and Options help define a CLI. The definitions of all the CLI options are wrapped into the Options class. The parse() method of the CommandLineParser class uses the Options class to parse the command line. In case of any deviation, appropriate exceptions are thrown by the parse() method. After parsing, the CommandLine class can be probed further to extract the values of the CLI options, if any.
Finally, the extracted values can be passed to the underlying services implementing the CLI tool.
Similar to the parse() method in the CommandLineParser class, the HelpFormatter also uses the Options Class to display the help text of a CLI tool.
4. Implementation
Let’s explore more about the Apache Commons CLI library classes and understand how they help create a CLI tool consistently and quickly.
4.1. Prerequisite Maven Dependency
Firstly, let’s add the necessary Maven dependency in the pom.xml file:
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.6.0</version>
</dependency>
4.2. Define, Parse, and Probe Command Line Arguments
Consider the command to connect to a PostgreSQL database using its psql CLI:
psql -h PGSERVER -U postgres -d empDB
Alternatively:
psql --host PGSERVER -username postgres -dbName empDB
Both commands require input parameters for the database server host, username, and database name. The first command uses short option names, while the second uses long option names. The username and dbName are required options, whereas the host is optional. If the host is missing, then by default, we consider the localhost as the host value.
Now, let’s define, parse, and probe the command line arguments:
@Test
void whenCliOptionProvided_thenParseAndExtractOptionAndArgumentValues() throws ParseException {
Options options = new Options();
Option hostOption = createOption("h", "host", "HOST", "Database server host", false);
Option userNameOption = createOption("U", "username", "USERNAME", "Database user name", true);
Option dbNameOption = createOption("d", "dbName", "DBNAME", "Database name to connect to", true);
options.addOption(hostOption)
.addOption(dbNameOption)
.addOption(userNameOption);
String[] commandWithShortNameOptions = new String[] { "-h", "PGSERVER", "-U", "postgres", "-d", "empDB" };
parseThenProcessCommand(options, commandWithShortNameOptions, "h", "U", "d" );
String[] commandWithLongNameOptions = new String[] { "--username", "postgres", "--dbName", "empDB" };
parseThenProcessCommand(options, commandWithShortNameOptions, "host", "username", "dbName" );
}
To define the options in the command, we created the Option objects corresponding to each of the input options by calling the method createOption():
Option createOption(String shortName, String longName, String argName, String description, boolean required) {
return Option.builder(shortName)
.longOpt(longName)
.argName(argName)
.desc(description)
.hasArg()
.required(required)
.build();
}
We used the Option.Builder class to set the short name, long name, argument name, and description of the input options in the CLI. Additionally, we consider the -U and -d options defined earlier as mandatory with the help of the required() method in the builder class.
Finally, we pass the arguments with short name options and long name options, respectively, to the method parseThenProcessCommand():
void parseThenProcessCommand(Options options, String[] commandArgs, String hostOption,
String usernameOption, String dbNameOption) throws ParseException {
CommandLineParser commandLineParser = new DefaultParser();
CommandLine commandLine = commandLineParser.parse(options, commandArgs);
String hostname = commandLine.hasOption("h") ? commandLine.getOptionValue(hostOption) : "localhost";
String username = commandLine.getOptionValue(usernameOption);
String dbName = commandLine.getOptionValue(dbNameOption);
if (commandLine.hasOption("h")) {
assertEquals("PGSERVER", hostname);
} else {
assertEquals("localhost", hostname);
}
assertEquals("postgres", userName);
assertEquals("empDB", dbName);
createConnection(hostname, username, dbName);
}
Interestingly, the method can handle both commands with short names and long names of options. The CommandLineParser class parses the arguments, and then we retrieve their values by calling the getOptionValue() method of the CommandLine object. Since the host is optional, we call the hasOption() method in the class CommandLine to probe if it’s present. If it’s not present, we replace its value with the default localhost.
Finally, we pass on the values to the underlying services by calling the method createConnection().
4.3. Handle Missing Mandatory Options
In most CLIs, an error should be displayed when a mandatory option is missing. Assume that the mandatory host option is missing in the psql command:
psql -h PGSERVER -U postgres
Let’s see how to handle this:
@Test
void whenMandatoryOptionMissing_thenThrowMissingOptionException() {
Options options = createOptions();
String[] commandWithMissingMandatoryOption = new String[]{"-h", "PGSERVER", "-U", "postgres"};
CommandLineParser commandLineParser = new DefaultParser();
assertThrows(MissingOptionException.class, () -> {
try {
CommandLine commandLine = commandLineParser.parse(options, commandWithMissingMandatoryOption);
} catch (ParseException e) {
assertTrue(e instanceof MissingOptionException);
handleException(new RuntimeException(e));
throw e;
}
});
}
When we invoke the parse() method in the CommandLineParser class, it throws MissingOptionException indicating the absence of the required option d. Following this, we call a method handleException() to manage the exception.
Suppose the –d option is present, but its argument is missing:
psql -h PGSERVER -U postgres -d
Now, let’s see how to handle this:
@Test
void whenOptionArgumentIsMissing_thenThrowMissingArgumentException() {
Options options = createOptions();
String[] commandWithOptionArgumentOption = new String[]{"-h", "PGSERVER", "-U", "postgres", "-d"};
CommandLineParser commandLineParser = new DefaultParser();
assertThrows(MissingArgumentException.class, () -> {
try {
CommandLine commandLine = commandLineParser.parse(options, commandWithOptionArgumentOption);
} catch (ParseException e) {
assertTrue(e instanceof MissingArgumentException);
handleException(new RuntimeException(e));
throw e;
}
});
}
When the parse() method is invoked on the CommandLineParser, a MissingArgumentException is thrown due to the absence of an argument next to the -d option. Further down, we call handleException() to manage the exception.
4.4. Handle Unrecognized Options
Sometimes, while running commands, we provide unrecognized options:
psql -h PGSERVER -U postgres -d empDB -y
We provided an incorrect non-existent -y option. Let’s see how we handle it in the code:
@Test
void whenUnrecognizedOptionProvided_thenThrowUnrecognizedOptionException() {
Options options = createOptions();
String[] commandWithIncorrectOption = new String[]{"-h", "PGSERVER", "-U", "postgres", "-d", "empDB", "-y"};
CommandLineParser commandLineParser = new DefaultParser();
assertThrows(UnrecognizedOptionException.class, () -> {
try {
CommandLine commandLine = commandLineParser.parse(options, commandWithIncorrectOption);
} catch (ParseException e) {
assertTrue(e instanceof UnrecognizedOptionException);
handleException(new RuntimeException(e));
throw e;
}
});
}
The parse() method throws UnrecogniedOptionException when it encounters the unknown -y option. Later, we call handleException() to manage the runtime exception.
4.5. Handle Mutually Exclusive Options
Consider the command using cp to copy files in Unix platforms:
cp -i -f file1 file2
The -i option prompts before overwriting files; however, the -f option overwrites the files without prompting. Both these options are conflicting and hence should not be used together.
Let’s try implementing this validation:
@Test
void whenMutuallyExclusiveOptionsProvidedTogether_thenThrowAlreadySelectedException() {
Option interactiveOption = new Option("i", false, "Prompts the user before overwriting the existing files");
Option forceOption = new Option("f", false, "Overwrites the existing files without prompting");
OptionGroup optionGroup = new OptionGroup();
optionGroup.addOption(interactiveOption)
.addOption(forceOption);
Options options = new Options();
options.addOptionGroup(optionGroup);
String[] commandWithConflictingOptions = new String[]{"cp", "-i", "-f", "file1", "file2"};
CommandLineParser commandLineParser = new DefaultParser();
assertThrows(AlreadySelectedException.class, () -> {
try {
CommandLine commandLine = commandLineParser.parse(options, commandWithConflictingOptions);
} catch (ParseException e) {
assertTrue(e instanceof AlreadySelectedException);
handleException(new RuntimeException(e));
throw e;
}
});
}
First, we created the relevant Option objects using its constructor instead of the Option.Builder class. This is also another way of instantiating the Option class.
The OptionGroup class helps group mutually exclusive options. Hence, we added the two options to the OptionGroup object. Then, we added the OptionGroup object to the Options object. Finally, when we called the parse() method on CommandLineParser class, it raised AlreadySelectedException, indicating the conflicting options.
4.6. Display Help Text
Formatting the help text and displaying it on the terminal is a common concern for all the CLI tools. Hence, the Apache Commons CLI addresses this as well with the help of the HelpFormatter class.
Let’s consider the psql CLI as an example:
@Test
void whenNeedHelp_thenPrintHelp() {
HelpFormatter helpFormatter = new HelpFormatter();
Options options = createOptions();
options.addOption("?", "help", false, "Display help information");
helpFormatter.printHelp("psql -U username -h host -d empDB", options);
}
The printHelp() method in the class HelpFormatter uses the Options object that holds the definition of the CLI to display the help text. The first argument of the method generates the usage text of the CLI shown at the top.
Let’s look at the output generated by the HelpFormatter class:
usage: psql -U username -h host -d empDB
-?,--help Display help information
-d,--dbName <DBNAME> Database name to connect to
-h,--host <HOST> Database server host
-U,--username <USERNAME> Database user name
5. Conclusion
In this article, we discussed the Apache Commons CLI library’s ability to help create CLIs quickly and efficiently in a standardized way. Moreover, the library is concise and easy to understand.
Notably, there are other libraries like JCommander, Airline, and Picocli that are equally efficient and worth exploring. Unlike Apache Commons CLI, all of them support annotation as well.
As usual, the code used is available over on GitHub.