cli
An annotation based CLI command framework for Java.
@Cli.Command(description = "Minimal example")
public class HelloWorld {
public static void main(String[] args) {
ProgramRunner.run(HelloWorld.class, args);
}
public void run() {
System.out.println("Hello World");
}
}
Available from maven central with the following coordinates:
<dependency>
<groupId>net.kleinhaneveld.cli</groupId>
<artifactId>cli</artifactId>
<version>0.1.0</version>
</dependency>
Contents
Greeter example
Below is a more extensive example, showcasing all annotations.
@Cli.Command(name = "hello", description = "Example command using all cli annotations.")
public class Greeter {
@Cli.Option(description = "some option", shortName = 'U')
private boolean uppercase;
@Cli.Argument(description = "some argument")
private String who;
public static void main(String[] args) {
ProgramRunner.run(Greeter.class, args);
}
@Cli.Run
public void perform() {
String value = "Hello " + who;
if (uppercase) {
value = value.toUpperCase();
}
System.out.println(value);
}
}
This defines a program with one command, a mandatory argument and one option. The ProgramRunner.run(...)
method will parse the arguments and set values to the corresponding fields.
The compiled binary, for example hello
, can now be invoked in the following way:
$ hello World Hello World $ hello -U Earth HELLO EARTH $ hello World --uppercase=false Hello World $ hello Earth -U=false Hello Earth $ hello --uppercase People HELLO PEOPLE
When run without arguments, this will produce the following output:
$ hello Error: Expected argument who hello Example command using all cli annotations. USAGE: hello [OPTION...] who WHERE: who: some argument OPTION: -U,--uppercase=boolean ('true', 'false') some option
Run Command
ProgramRunner
searches for a void method without arguments annotated with @Cli.Run
or, if not found, with the name run
.
Arguments
An argument can have a name
, a description
, default values
and a value parser
.
The argument's name
is only used for display purposes in the generated help output. If the name
is not supplied in the argument, then the field name will be used. description
is mandatory, it is used in the generated help output. values
are the accepted values for the argument. When any other value is supplied, an error is displayed with usage. parser
can be used to specify a parser for the type, see ValueParser.
@Cli.Argument(
name = "argument_name",
description = "explains the purpose of this argument",
values = {"all", "allowed", "values"},
parser = MyTypeParser.class
)
private MyType argument;
Options
Next to the name
, description
, values
and parser
attributes, a @Cli.Option
can have a single character shortName
and a defaultValue
.
The option's name
and shortName
are used to parse options from the command line. On the command line the name
is prefixed with --
, the shortName
with -
.
All options, except boolean options, take an argument that must be provided after an =
sign.
When an option is not supplied on the command line, the defaultValue
is applied if present, or the default from source code is used. The defaultValue
is matched against the accepted values
, if present.
Options can be placed anywhere on the commandline (after the binary.)
@Cli.Option(
name = "option-name",
shortName = 'o',
description = "explains the purpose of this option",
values = {"all", "allowed", "values"},
parser = MyTypeParser.class,
defaultValue = "allowed"
)
private MyType option;
Boolean Options
Boolean options don't need to be given a value. If the option is present on the command line, but the value is not specified, the value will be set to true
.
Composite Commands
Composite commands can be created by defining subCommands
on a command without any arguments.`
@Cli.Command(
description = "Composite command example",
subCommands = {HelloWorld.class, Greeter.class, GreeterMyType.class}
)
public class ExampleProgram {
public static void main(String[] args) {
ProgramRunner.run(ExampleProgram.class, args);
}
}
Composite commands support the help
command, which generates usage information about the composite command, or any of it's subcommands. For example when invoked with ExampleProgram help
, it generates the following output.
ExampleProgram Composite command example USAGE: ExampleProgram COMMAND COMMAND: HelloWorld Minimal example hello Example command using all cli annotations. GreeterMyType some command
When invoked with ExampleProgram help hello
it generates the following output.
hello Example command using all cli annotations. USAGE: hello [OPTION...] who WHERE: who: some argument OPTION: -U,--uppercase=boolean ('true', 'false') some option
Composite commands only take one argument, but can have options and a run method. The run method for composite commands is a void method that can take a Runner
argument that corresponds to invoking the subcommand. Consider the following example.
@Cli.Command(
description = "Transaction subcommands example",
subCommands = {HelloWorld.class, Greeter.class, GreeterMyType.class}
)
public class TransactionalCommand {
public static void main(String[] args) {
ProgramRunner.run(TransactionalCommand.class, args);
}
void run(Runner subCommand) {
Transaction transaction = Transaction.begin();
try {
subCommand.run();
transaction.commit();
} catch (RuntimeException e) {
transaction.rollback();
}
}
}
This command will wrap the subcommand in a transaction that will be committed upon success, and rolledback upon failure.
ValueParser
Next to supporting some standard types as arguments and options, ProgramRunner is extensible with additional parsers. Additional value parsers are discovered using ServiceLoader
, or via the parser
option at the @Cli.Argument
and @Cli.Option
annotations.
Value parsers must implement the ValueParser
interface:
package net.kleinhaneveld.cli.parser;
public interface ValueParser {
Class[] getSupportedClasses();
T parse(String argument) throws ValueParseException;
}
As an example, consider the following custom type.
public class MyType {
private final String value;
public MyType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
The value parser for this type would look like this.
public class MyTypeParser implements ValueParser {
@Override
public Class[] getSupportedClasses() {
return new Class[]{ MyType.class };
}
@Override
public MyType parse(String argument) throws ValueParseException {
return new MyType(argument);
}
}
Value parsers can be registered in the @Cli.Argument
annotation (see the bold sections.)
@Cli.Command(name = "GreeterMyType", description = "some command")
public class GreeterMyType {
@Cli.Option(description = "some option", shortName = 'U')
private boolean uppercase;
@Cli.Argument(description = "some argument")
private MyType who;
public static void main(String[] args) {
ProgramRunner.run(GreeterMyType.class, args);
}
@Cli.Run
public void perform() {
String value = "Hello " + who.getValue();
if (uppercase) {
value = value.toUpperCase();
}
System.out.println(value);
}
}
Value parsers can also be registered via ServiceLoader
. To do that add the a file named META-INF/services/net.kleinhaneveld.cli.parser.ValueParser
to the classpath with the class name of the parser:
net.kleinhaneveld.cli.examples.MyTypeParser
Then the specific value parser will be used automatically when arguments or options with any type in the supportedClasses
are used.
@Cli.Command(name = "HelloWorldArg", description = "some command")
public class HelloWorldArg {
@Cli.Option(description = "some option", shortName = 'U')
private boolean uppercase;
@Cli.Argument(description = "some argument")
private MyType who;
public static void main(String[] args) {
ProgramRunner.run(HelloWorldArg.class, args);
}
@Cli.Run
public void perform() {
String value = "Hello " + who.getValue();
if (uppercase) {
value = value.toUpperCase();
}
System.out.println(value);
}
}
Instantiator
There are several cases where ProgramRunner
must create instances of classes. These are custom value parsers, commands and subcommands.
It does this via an Instantiator
.
package net.kleinhaneveld.cli.instantiator;
public interface Instantiator {
T instantiate(Class aClass);
}
A custom Instantiator
can be registered via ServiceLoader
in the file META-INF/services/net.kleinhaneveld.cli.instantiator.Instantiator
.
This mechanism can be used to hook up specific injection framework.
I18n
Internationalization is supported for the descriptions of commands, options and arguments by specifying resourceBundle
in the @Cli.Command
annotation. The values of the description
attributes are used as keys for the bundle.
The following example shows an internationalized variant of the Greeter we saw before. Note that the same resource bundle is also used in the run method.
@Cli.Command(name = "hello", description = "command.hello", resourceBundle = "greeter")
public class InternationalizedGreeter {
@Cli.Option(description = "option.uppercase", shortName = 'U')
private boolean uppercase;
@Cli.Argument(description = "argument.who")
private String who;
public static void main(String[] args) {
ProgramRunner.run(InternationalizedGreeter.class, args);
}
public void run() {
ResourceBundle bundle = ResourceBundle.getBundle("greeter", Locale.getDefault());
String value = bundle.getString("hello") + " " + who;
if (uppercase) {
value = value.toUpperCase();
}
System.out.println(value);
}
}
An example resource bundle would be the following greeter.properties
.
option.uppercase =Generate output in uppercase.
argument.who =Who to greet.
command.hello =Friendly greeter application.
hello =Hi
Example output would be
$ hello there Hi there
Generated help output would be
hello Friendly greeter application. USAGE: hello [OPTION...] who WHERE: who: Who to greet. OPTION: -U,--uppercase=boolean ('true', 'false') Generate output in uppercase.
TODO
- Internationalized error messages.
- Parsing of combined shortNames of boolean options, as in
tar -xzvf file.tar.gz
. - Detecting undefined options.
- Analyzing whether subcommands don't override options.
- Bash autocompletion.