Search code examples
javacommand-line-interfacepicocli

Bridging CLI argument parsing with application setup


My PicoCLI-based application has multiple commands and sub-commands with general options that apply to all commands, and some options which apply to the specific command. The general options are used for all the commands.

My PicoCLI (sub-)commands are similar to this example:

@Command(name = "country", description = "Resolve ISO country code (ISO-3166-1, Alpha-2 code)")
static class Subcommand1 implements Runnable {

    @Parameters(arity = "1..*", paramLabel = "<country code>", description = "country code(s) to be resolved")
    private String[] countryCodes;

    @Override
    public void run() {
        for (String code : countryCodes) {
            System.out.println(String.format("%s: %s", code.toUpperCase(), new Locale("", code).getDisplayCountry()));
        }
    }
}

but the each (sub-)command needs to run some general setup code first similar to:

@Override
public void run() {
    try (Channel channel = _establishChannel(generalConfiguration)) {
        // do sub-command work
    }
}

where the generalConfiguration is an example of the general parameters and options used for all (sub-)commands. So, this general setup block of code will be duplicated in each command:

try (Channel channel = _establishChannel(generalConfiguration)) {
     // do sub-command work
}

but I'd like it expressed in a single spot, instead. Today, I basically duplicate the (sub-)parameters and options and invoke a common helper:

void runCommand(String command, String c1Param, bool c1AllOption, String c2Filename, String c3Param /*...*/) {
    try (Channel channel = _establishChannel(generalConfiguration)) {
        switch(command) {
        case "COMMAND_1":
            doCommand1(c1Param,c1AllOption);
            break;
        case "COMMAND_2":
            doCommand2(c2Filename);
            break;
        case "COMMAND_3":
            doCommand3(c3Param);
            break;
        // ...
        }
    }
}

That's pretty ugly, and fragile. Is there a cleaner/better way?


Solution

  • One idea is to use a custom Execution Strategy.

    The Initialization Before Execution section of the picocli user manual has an example. Let's try to modify that example for your use case. I arrive at something like this:

    @Command(subcommands = {Sub1.class, Sub2.class, Sub3.class})
    class MyApp implements Runnable {
    
        Channel channel; // initialized in executionStrategy method
    
        // A reference to this method can be used as a custom execution strategy
        // that first calls the init() method,
        // and then delegates to the default execution strategy.
        private int executionStrategy(ParseResult parseResult) {
    
            // custom initialization to be done before executing any command or subcommand
            try (this.channel = _establishChannel(generalConfiguration)) {
    
                // default execution strategy
                return new CommandLine.RunLast().execute(parseResult); 
            }
        }
    
        public static void main(String[] args) {
            MyApp app = new MyApp();
            new CommandLine(app)
                    // wire in the custom execution strategy
                    .setExecutionStrategy(app::executionStrategy) // Java 8 method reference syntax
                    .execute(args);
        }
    
        // ...
    }
    

    This custom execution strategy ensures that the channel field of the top-level commmand is initialized before any command is executed.

    The next piece is, how can subcommands access this channel field (since this field is in the top-level command)? This is what the @ParentCommand annotation is for.

    When subcommands have a @ParentCommand-annoted field, picocli will inject the user object of the parent command into that field, so that subcommands can reference state in the parent command. For example:

    @Command(name = "country", description = "Resolve ISO country code (ISO-3166-1, Alpha-2 code)")
    static class Subcommand1 implements Runnable {
    
        @ParentCommand
        private MyApp parent; // picocli injects reference to parent command
    
        @Parameters(arity = "1..*", paramLabel = "<country code>", description = "country code(s) to be resolved")
        private String[] countryCodes;
    
        @Override
        public void run() {
            Channel channel = parent.channel;
            doSomethingWith(channel);
            // ...
        }
    }