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?
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);
// ...
}
}