Skip to content

peterpaul/cli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

86 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages