Dieser Artikel ist auch auf Deutsch verfügbar
We already looked at command-line applications in general in this column four years ago. The fundamental principles described there are still valid today. And not a lot has changed either with regard to the distribution of command-line applications written in Java.
There are nonetheless reasons for implementing these applications with Java. By doing so, a team can take advantage of existing knowledge and known libraries. In addition, the disadvantage of a longer start time can nowadays be circumvented, for example through the use of native compiling with GraalVM.
But whatever the reasons for writing command-line applications with Java, they differ with respect to the reading out and the processing of the provided arguments and options from other application types. We therefore take a closer look at this in this article. After a brief general introduction to arguments and options, we will look at four libraries that we can use to significantly reduce our workload.
Configuration of Command-Line Applications
There are three options for configuring the execution of a command-line application.
The first option is that the application reads out the configuration values. For this purpose usually either files that are found in specific locations in the file system or defined system environment variables are read out. This form of configuration is static and is therefore more suitable for global and long-term settings. Examples of this include the files ~/.gitconfig for Git or ~/.m2/settings.xml for Maven.
As a second option it is possible for the application to query the values itself by means of the standard input. This form of querying has the advantage that the input is not saved in the history of the command line. This approach is therefore particularly suitable for the inputting of passwords or other secrets. The disadvantage however is that it is less suitable for the writing of scripts.
The third possibility is the transfer of arguments and options upon start-up
of the application. These are written after the call-up of the application.
So the request curl --request get -v https://innoq.com
consists of the two
options --request
with the value get
and -v
without value, as well as the
argument https://innoq.com
.
Arguments and Options
Arguments are used above all when imperative for the primary function of the
application. For example, the command git add
expects the files or directories
that are to be added to be specified. However, it is not always imperative to
specify the arguments as they can be assigned by appropriate defaults. The tool
therefore uses ls
in order to indicate the current folder to files when no
argument is provided.
If an application requires multiple arguments, such as mv
in order to move
files, these are always to be specified in the order defined by the application.
Depending on the application it can be imperative that all arguments come after
the options.
Options in contrast are used for optional configuration and are therefore
rarely imperative. An option always consists of a name and a value. For the
name, the use of a short and a long form has become established. In the short
form, the option is specified with a hyphen -
and only one alphabetic
character. The long form in contrast begins with a double hyphen --
and
consists of multiple letters, usually even a whole word. The value of an option
is either the argument following the option name or is written by a separator,
usually the equals sign =
, directly to the option name. For logical values,
it is often not necessary to specify the value. The value is then set to true
when the option names are specified.
Especially for options, we should define defaults that are defaulted to when the option is not specified.
In Java, we can access the arguments passed to the application via the string
array declared as an argument in the main
method. However, Java does not
understand the concept of options. In the case of the cURL request described
above, an array with the four strings --request
, get
, -v
, and
https://innoq.com
are provided. It is now the job of the application to
interpret these appropriately. As this can quickly become complicated, the
use of an appropriate library is recommended.
Commons CLI
One of the oldest libraries I know for dealing with arguments and options is Apache Commons CLI. The implementation uses a purely programmatic programming model that can be logically subdivided into the following three phases:
- Definition of the options,
- Processing of the command line, and
- Evaluation of the applied options
Listing 1 shows a very simple example of how the code required for this looks.
For the defining of the options we use the options
class with the available
and overloaded addOption
methods. In addition to the main variant, which
receives an instance of the option
class, the overloaded methods offer us
shorter variants for the generation of such options. In the example this is
used for the verbose
option.
One option consists of a short or a long name. In addition, we can specify
whether a value has to be specified for this option or whether it is sufficient
to find out that the option has veen specified. With argName
and description
the generation of the help texts can also be influenced. In order to then
generate such an option, we use the available builder.
After the definition of the options we use the DefaultParser
in order to
process the options given in the command line with those defined by us. The
parse
method triggers a ParseException
in the case of error, for example
when an undefined option is specified.
Once the processing is successfully completed, we can then analyze the specified
options on the CommandLine
returned by parse
. For this purpose we use the
hasOption
and getOptionValue
methods. The provided arguments are accessed
using getArgs
. Commons CLI does not convert the option values into specific
data types and only returns a string. We can specify a type
in the definition
of our options and then use getOptionObject
to query the values converted
to this type. However, only a handful of types from the JDK, such as URL
,
file
, and number
, are supported. It is not possible to extend this mechanism
to different types.
Finally, Commons CLI also offers us the option using the HelpFormatter
class
of displaying help text in which all possible options are displayed. This
typically occurs in the event of an error or for a specific help option.
Commons CLI is thus very helpful and reduces our workload significantly. There are however some features lacking, such as support for converting different types. Moreover, although we can access the arguments, they do not appear in the help text.
So let’s look at args4j as an additional option.
args4j
In principle, args4j works exactly as Commons CLI. We define the options, and here also the arguments, start the processing, and can then work with the arguments and options.
In contrast to Commons CLI however, args4j uses an annotation-based mechanism for the definition. To this end, the fields defined in a particular class are annotated. Listing 2 shows the implementation with identical functionality to the previous Commons CLI example.
After the definition we use CmdLineParser
to process the specified options.
These are provided to the annotated classes through the generation of an
instance. If parseArgument
is now called, the parser binds the values of the
provided arguments and options to the fields defined in the class. In the event
of an error an exception is created. Here too, the issuing of a help text is
supported. For this purpose we directly use the generated instance of the parser
and the methods printSingleLineUsage
and printUsage
.
In contrast to Commons CLI, args4j allows us to use any data type we want. In
order to do so we have to implement the OptionHandler
class and register it in
the OptionHandlerRegistry
before parsing.
JCommander
A further implementation, similar to args4j, is JCommander. Here too, options and arguments are defined by means of annotations, and different types are also supported.
JCommander offers us additional features though. For example, we can explicitly mark an option as a password. If this option is then specified without a value, it is queried after the application has started up by means of an interactive input (see Listing 3).
In addition, JCommander also provides the possibility to implement personalized validations for options. In this way we can ensure that the inputted password must comprise at least eight characters (see Listing 4).
An additional, powerful feature is that JCommander also supports command-line
applications that comprise multiple commands, similar to Git. For this purpose
we use addCommand
to register, as can be seen in Listing 5, our commands
under a specific name and can then find out with getParsedCommand
which
command is specified.
As a final extension, JCommander allows us to write all options and arguments, separated by line breaks, in a file and then, upon start-up of our application, to specify this file with an @ as prefix as the only argument. JCommander recognizes such an @-argument and reads out the options and arguments from the specified file.
Even if it seems that JCommander already offers us everything we need, with picocli there is a library that has an even greater scope than JCommander.
picocli
picocli, like args4j and JCommander, uses the declarative annotation-based programming model. It supports all above-mentioned functionalities and others.
For example, it supports --
as an argument to show that all following values
are arguments, even if one of these arguments has the same name as an option.
Therefore, in Listing 6, although --recursive
is specified, the option
recursive
remains false
and --recursive
appears in the arguments.
Because picocli not only processes options and arguments, but we also use it to
define commands that picocli can now also execute, the implementation of multiple
commands in one application is even easier than with JCommander. As can be seen
in Listing 7, we define in our application the available commands and implement
them in their own class annotated with @Command
. To get global options, we can
use @ParentCommand
to inject the application class and query it.
Listing 7 also shows that instead of displaying our result via
System.out.println
, we use an indirection via the CommandSpec
class.
This allows us to exchange in tests the PrintWriter
obtained in this way
and thus also to test the outputs of our application.
Another neat feature is that with Boolean options picocli can also generate
a negated variant for us. If we additionally extend the annotation for
interactive
by the value negatable=true
, --no-verbose
is then also
parsed and processed.
In addition to these four features, picocli offers us many more wide-ranging conveniences. It is possible for example to generate man pages for our application, and autocompletion with TAB for Bash and ZSH is also possible. By means of an annotation processor we can check the defined commands and compile-time options for accuracy, and at the same time generate the metadata required for GraalVM native images.
Finally, for picocli there are also ready-made integrations in the most common dependency-injection-based frameworks such as Guice, Spring, Micronaut, and Quarkus.
Title photo by Joel Mbugua on Unsplash.