JCommander + AutoService to create a Java command line application with subcommands

need

Recently, I wanted to package one of my Java applications into a command-line tool. After reading several libraries, I finally chose JCommander, combined with the AutoService library, to implement a tool with sub-commands, which is convenient for expanding new sub-commands.

Subcommands are placed under the same package, implement the same interface, and are loaded through java.util.ServiceLocator.

Java Command Line Tool Library

Several commonly used libraries are:

  • JCommander
    project address: https://github.com/cbeust/jcommander
    Star: 1010 Fork: 227
    Document address: http://jcommander.org/

  • picocli
    address: https://github.com/remkop/picocli
    Star: 336, Fork: 32
    Example: https://github.com/kakawait/picocli-spring-boot-starter

  • Commons CLI
    address: https://commons.apache.org/proper/commons-cli/
    address: Open source project from apache common
    Update: The last update was 1.5-SNAPSHOT on June 8, 2017

  • Args4j
    project address: https://0github.com/kohsuke/args4j
    Star: 570 Fork: 151
    Document address: http://args4j.kohsuke.org/sample.html
    Activity: The last update was 2 years ago

dependent library

        <!-- https://mvnrepository.com/artifact/com.google.auto.service/auto-service -->
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.1.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.beust/jcommander -->
        <dependency>
            <groupId>com.beust</groupId>
            <artifactId>jcommander</artifactId>
            <version>1.82</version>
        </dependency>

Define individual subcommands

Create child instruction:

@AutoService(Command.class)
@Parameters(
        commandNames = {
    
     CREATE_CMD },
        commandDescription = "create a new ebook."
)
@Getter
public class CreateCommand implements Command{
    
    

    @Parameter(names = {
    
     "--indexUrl", "-u" })
    public String indexUrl;

    @Override
    public void execute() throws CommandException {
    
    
        System.out.println("create a new book from " + indexUrl);
    }
}

Fetch subcommand:

@AutoService(Command.class)
@Parameters(
        commandNames = {
    
    FETCH_CMD},
        commandDescription = "fetch some articles from website."
)
@Getter
public class FetchCommand implements Command {
    
    

    @Parameter(names = {
    
    "--indexUrl", "-u"})
    public String indexUrl;

    @Override
    public void execute() throws CommandException {
    
    
        System.out.println("fetch articles from " + indexUrl);
    }
}

Both implement the interface Command:

public interface Command {
    
    
    default Collection<Command> commands() {
    
    
        return null;
    }

    void execute() throws CommandException;
}

Note that subcommands support nested subcommands. Of course, it is generally not used.

main class CLI

public class CLI {
    
    

    static final String CREATE_CMD = "create";
    static final String FETCH_CMD = "fetch";

    @Parameter(names = {
    
     "-h", "--help" }, help = true)
    private boolean help;

    public void exec(String[] args) {
    
    
        final JCommander.Builder builder = JCommander.newBuilder().addObject(new CLI());       
        final JCommander jCommander = builder.build();
        ServiceLoader.load(Command.class).forEach(command -> CLI.registerCommand(jCommander, command));

        JCommander leafCommander = jCommander;
        try {
    
    
            jCommander.parse(args);

            final String rootVerb = jCommander.getParsedCommand();
            final JCommander rootCommander = jCommander.getCommands().get(rootVerb);

            if (rootCommander == null) {
    
    
                jCommander.usage();
                System.exit(1);
            }

            leafCommander = rootCommander;
            do {
    
    
                final String subVerb = leafCommander.getParsedCommand();
                final JCommander subCommander = leafCommander.getCommands().get(subVerb);
                if (subCommander != null)
                    leafCommander = subCommander;
                else
                    break;
            } while (true);

            final Command command = (Command) leafCommander.getObjects().get(0);
            command.execute();
        } catch (final CommandException e) {
    
    
            System.err.printf("%1$s: %2$s. See '%1$s --help'.%n", leafCommander.getProgramName(), e.getMessage());
            System.exit(e.getStatus());
        } catch (final Exception e) {
    
    
            System.err.printf("%1$s: %2$s. See '%1$s --help'.%n", leafCommander.getProgramName(), e.getMessage());
            System.exit(1);
        }
    }

    private static final void registerCommand(final JCommander jCommander, final Command command) {
    
    
        jCommander.addCommand(command);

        final Parameters commandParameters = command.getClass().getAnnotation(Parameters.class);
        if (commandParameters == null || commandParameters.commandNames().length == 0)
            return;
        final JCommander subCommander = jCommander.getCommands().get(commandParameters.commandNames()[0]);
        final Collection<Command> subCommands = command.commands();
        if (subCommands != null)
            subCommands.forEach(subCommand -> CLI.registerCommand(subCommander, subCommand));
    }

    public static void main(final String[] args) {
    
    
        CLI cli = new CLI();
        cli.exec(args);
    }
}

The principle is simple: ServiceLocator loads all the implementation classes of Command, and then calls the corresponding Command class according to the subcommand. The advantage of AutoService is that you don't have to create related files under META-INF/services yourself.

have a test

    @Test
    public void testCreateCommand() {
    
    
        CLI cli = new CLI();
        String[] argv = {
    
     "create",  "-u", "http://www.sina.com.cn"};
        cli.exec(argv);
    }

    @Test
    public void testFetchCommand() {
    
    
        CLI cli = new CLI();
        String[] argv = {
    
     "fetch",  "-u", "http://www.csdn.cn"};
        cli.exec(argv);
    }

reference documents

  • https://gist.github.com/mkarg/9d9ca23e6da32b47c7fadaf10ae16ba6
  • https://pedrorijo.com/blog/java-service-loader/

Guess you like

Origin blog.csdn.net/jgku/article/details/132110314