带有 Picocli 的 CLI:在调用子命令之前调用主命令

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

由于子命令支持(和基于注释的声明),我从 Apache Commons CLI 切换到 Picocli。

考虑像 git 这样的命令行工具,以及像 push 这样的子命令。 Git 有一个主开关 --verbose-v 用于在 all 子命令中启用详细模式。 如何实现在 任何子命令 之前执行的主开关?

这是我的测试

@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;
    }
}

输出是

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

我希望 GitApp.call 在子命令被调用之前被调用。但只有子命令被调用。

由于 Picocli 支持使用 Options 进行继承,因此我将 --help--verbose 选项提取到抽象 class BaseCommand 中,并从中调用 super.call子命令。

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;
    }
}

CommandLine.call(和 CommandLine.run)方法仅按设计调用 last subcommand,因此您在原始 post 中看到的是预期的行为。

callrun方法实际上是一个捷径。下面两行是等价的:

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).

还有一个 RunAll handler 运行 所有 匹配的命令。以下 main 方法给出了所需的行为:

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));
}

输出:

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

您可能还对 @ParentCommand 注释感兴趣。这告诉 picocli 将父命令的实例注入到子命令中。然后您的子命令可以调用父命令的方法,例如检查 verbose 是否为真。例如:

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);
    }
}

其他小改动:通过导入内部 类 使注释更加紧凑。您可能还喜欢有助于减少样板代码的 mixinStandardHelpOptions attribute and the built-in help 子命令。