Search code examples
javacommand-line-interfacepicocli

CLI with Picocli: Call main command before sub command get called


I switched from Apache Commons CLI to Picocli because of the sub command support (and annotation-based declaration).

Consider a command line tool like git, with sub commands like push. Git have a main switch --verbose or -v for enable verbose mode in all sub commands. How can I implement a main switch that is executed before any sub commands?

This is my test

@CommandLine.Command(name = "push",
        description = "Update remote refs along with associated objects")
class PushCommand implements Callable<Void> {
    @Override
    public Void call() throws Exception {
        System.out.println("#PushCommand.call");

        return null;
    }
}

@CommandLine.Command(description = "Version control", subcommands = {PushCommand.class})
public class GitApp implements Callable<Void> {
    @CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "Display this help message.")
    private boolean usageHelpRequested;

    @CommandLine.Option(names = {"-v", "--verbose"}, description = "Verbose mode. Helpful for troubleshooting.")
    private boolean verboseMode;

    public static void main(String[] args) {
        GitApp app = new GitApp();
        CommandLine.call(app, "--verbose", "push");
        System.out.println("#GitApp.main after. verbose: " + (app.verboseMode));
    }

    @Override
    public Void call() throws Exception {
        System.out.println("#GitApp.call");

        return null;
    }
}

Output is

#PushCommand.call
#GitApp.main after. verbose: true

I would expect, that GitApp.call get called before the sub command get called. But only the sub command get called.


Solution

  • The CommandLine.call (and CommandLine.run) methods only invoke the last subcommand by design, so what you are seeing in the original post is the expected behaviour.

    The call and run methods are actually a shortcut. The following two lines are equivalent:

    CommandLine.run(callable, args); // internally uses RunLast, equivalent to: 
    new CommandLine(callable).parseWithHandler(new RunLast(), args);
    

    Update: from picocli 4.0, the above methods are deprecated, and replaced with new CommandLine(myapp).execute(args). The "handler" is now called the "execution strategy" (example below).

    There is also a RunAll handler that runs all commands that were matched. The following main method gives the desired behaviour:

    public static void main(String[] args) {
        args = new String[] { "--verbose", "push" };
        GitApp app = new GitApp();
        // before picocli 4.0:
        new CommandLine(app).parseWithHandler(new RunAll(), args);
        // from picocli 4.0:
        //new CommandLine(app).setExecutionStrategy(new RunAll()).execute(args);
        System.out.println("#GitApp.main after. verbose: " + (app.verboseMode));
    }
    

    Output:

    #GitApp.call
    #PushCommand.call
    #GitApp.main after. verbose: true
    

    You may also be interested in the @ParentCommand annotation. This tells picocli to inject an instance of the parent command into a subcommand. Your subcommand can then call methods on the parent command, for example to check whether verbose is true. For example:

    Update: from picocli 4.0, use the setExecutionStrategy method to specify RunAll. The below example is updated to use the new picocli 4.0+ API.

    import picocli.CommandLine;
    import picocli.CommandLine.*;
    
    @Command(name = "push",
            description = "Update remote refs along with associated objects")
    class PushCommand implements Runnable {
    
        @ParentCommand // picocli injects the parent instance
        private GitApp parentCommand;
    
        public void run() {
            System.out.printf("#PushCommand.call: parent.verbose=%s%n",
                    parentCommand.verboseMode); // use parent instance
        }
    }
    
    @Command(description = "Version control",
            mixinStandardHelpOptions = true, // auto-include --help and --version
            subcommands = {PushCommand.class,
                           HelpCommand.class}) // built-in help subcommand
    public class GitApp implements Runnable {
        @Option(names = {"-v", "--verbose"},
                description = "Verbose mode. Helpful for troubleshooting.")
        boolean verboseMode;
    
        public void run() {
            System.out.println("#GitApp.call");
        }
    
        public static void main(String[] args) {
            args = new String[] { "--verbose", "push" };
    
            GitApp app = new GitApp();
            int exitCode = new CommandLine(app)
                .setExecutionStrategy(new RunAll())
                .execute(args);
    
            System.out.println("#GitApp.main after. verbose: " + (app.verboseMode));
            System.exit(exitCode);
        }
    }
    

    Other minor edits: made the annotations a bit more compact by importing the inner classes. You may also like the mixinStandardHelpOptions attribute and the built-in help subcommand that help reduce boilerplate code.