1

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?

Jan Nielsen
  • 10,892
  • 14
  • 65
  • 119

1 Answers1

1

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);
        // ...
    }
}
Remko Popma
  • 35,130
  • 11
  • 92
  • 114