Reputation: 2712
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.
Upvotes: 3
Views: 3838
Reputation: 36754
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 specifyRunAll
. 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.
Upvotes: 5
Reputation: 2712
As Picocli supports inheritance with Options I've extracted the --help
and --verbose
Option into an abstract class BaseCommand
and invoke super.call
from the subcommands.
abstract class BaseCommand 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;
@Override
public Void call() throws Exception {
if (verboseMode) {
setVerbose();
}
return null;
}
private void setVerbose() {
System.out.println("enter verbose mode");
}
}
@CommandLine.Command(name = "push",
description = "Update remote refs along with associated objects")
class PushCommand extends BaseCommand {
@Override
public Void call() throws Exception {
super.call();
System.out.println("Execute push command");
return null;
}
}
@CommandLine.Command(description = "Version control", subcommands = {PushCommand.class})
public class GitApp extends BaseCommand {
public static void main(String[] args) {
GitApp app = new GitApp();
CommandLine.call(app, "push", "--verbose");
}
@Override
public Void call() throws Exception {
super.call();
System.out.println("GitApp.call called");
return null;
}
}
Upvotes: 0