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.