使用Picocli创建Java命令行程序

Create a Java Command Line Program with Picocli

1.简介

在本教程中,我们将使用picocli库,该库使我们可以轻松地用Java创建命令行程序。

首先,我们将创建一个Hello World命令。 然后,我们将通过部分复制gitcommand来深入了解该库的关键功能。

2.你好世界命令

让我们从简单的事情开始:Hello World命令!

首先,我们需要将依赖项添加到thepicocli项目中:

1
2
3
4
5
<dependency>
    <groupId>info.picocli</groupId>
    <artifactId>picocli</artifactId>
    <version>3.9.6</version>
</dependency>

如我们所见,我们将使用库的3.9.6版本,尽管a4.0.0版本正在构建中(当前在alpha测试中可用)。

现在已经建立了依赖关系,让我们创建我们的Hello World命令。 为此,我们将使用库中的@Command注释:

1
2
3
4
5
6
@Command(
  name ="hello",
  description ="Says hello"
)
public class HelloWorldCommand {
}

如我们所见,注释可以带有参数。 我们在这里只使用其中两个。 它们的目的是提供有关自动帮助消息的当前命令和文本的信息。

目前,使用此命令我们无能为力。 为了使其能够执行某些操作,我们需要添加一个调用便捷CommandLine.run(Runnable,String [])方法的主方法。 这需要两个参数:一个命令实例,因此必须实现Runnable接口;一个String数组,表示命令参数(选项,参数和子命令):

1
2
3
4
5
6
7
8
9
10
public class HelloWorldCommand implements Runnable {
    public static void main(String[] args) {
        CommandLine.run(new HelloWorldCommand(), args);
    }

    @Override
    public void run() {
        System.out.println("Hello World!");
    }
}

现在,当我们运行main方法时,我们将看到控制台输出" Hello World!"。

打包到jar中后,我们可以使用java命令运行Hello World命令:

1
java -cp"pathToPicocliJar;pathToCommandJar" com.baeldung.picoli.helloworld.HelloWorldCommand

毫不奇怪,它还会输出" Hello World!" 字符串到控制台。

3.具体的用例

现在,我们已经了解了基础知识,我们将深入研究picocli库。 为了做到这一点,我们将部分复制一个流行的命令:git。

当然,其目的不是实现git命令的行为,而是重现git命令的可能性-存在哪些子命令以及哪些选项可用于特殊的子命令。

首先,我们必须像对Hello World命令那样创建一个GitCommand类:

1
2
3
4
5
6
7
8
9
10
11
@Command
public class GitCommand implements Runnable {
    public static void main(String[] args) {
        CommandLine.run(new GitCommand(), args);
    }

    @Override
    public void run() {
        System.out.println("The popular git command");
    }
}

4.添加子命令

gitcommand提供了许多子命令-add,commit,remote等。 在这里,我们将重点介绍添加和提交。

因此,我们的目标是在主命令中声明这两个子命令.Picocli提供了三种实现方法。

4.1。 在类上使用@Command注释

@Command批注提供了通过subcommands参数注册子命令的可能性:

1
2
3
4
5
6
@Command(
  subcommands = {
      GitAddCommand.class,
      GitCommitCommand.class
  }
)

在本例中,我们添加了两个新类:GitAddCommand和GitCommitCommand。 两者都用@Command和ImplementRunnable进行注释。 给他们起一个名字很重要,因为picocli将使用这些名字来识别要执行的子命令:

1
2
3
4
5
6
7
8
9
@Command(
  name ="add"
)
public class GitAddCommand implements Runnable {
    @Override
    public void run() {
        System.out.println("Adding some files to the staging area");
    }
}

1
2
3
4
5
6
7
8
9
@Command(
  name ="commit"
)
public class GitCommitCommand implements Runnable {
    @Override
    public void run() {
        System.out.println("Committing files in the staging area, how wonderful?");
    }
}

因此,如果我们使用add作为参数运行main命令,则控制台将输出"将某些文件添加到暂存区"。

4.2。 在方法上使用@Command注释

声明子命令的另一种方法是创建@Command注释的方法,该方法表示GitCommand类中的那些命令:

1
2
3
4
5
6
7
8
9
@Command(name ="add")
public void addCommand() {
    System.out.println("Adding some files to the staging area");
}

@Command(name ="commit")
public void commitCommand() {
    System.out.println("Committing files in the staging area, how wonderful?");
}

这样,我们可以将业务逻辑直接实现到方法中,而无需创建单独的类来处理它。

4.3。 以编程方式添加子命令

最后,picocli为我们提供了以编程方式注册子命令的可能性。 这有点棘手,因为我们必须创建一个包装命令的CommandLine对象,然后向其中添加子命令:

1
2
3
CommandLine commandLine = new CommandLine(new GitCommand());
commandLine.addSubcommand("add", new GitAddCommand());
commandLine.addSubcommand("commit", new GitCommitCommand());

在那之后,我们仍然必须运行命令,但是不能再使用CommandLine.run()方法了。 现在,我们必须在新创建的CommandLine对象上调用theparseWithHandler()方法:

1
commandLine.parseWithHandler(new RunLast(), args);

我们应该注意RunLast类的使用,该类告诉picocli运行最特定的子命令。 picocli还提供了其他两个命令处理程序:RunFirst和RunAll。 前者运行最高命令,而后者运行所有命令。

使用便捷方法CommandLine.run()时,默认情况下使用RunLast处理程序。

5.使用@Option注释管理选项

5.1。 无参数期权

现在让我们看看如何在命令中添加一些选项。 确实,我们想告诉ouradd命令它应该添加所有修改过的文件。 为此,我们将在@GitAddCommand类中添加一个@Option注释字段:

1
2
3
4
5
6
7
8
9
10
11
@Option(names = {"-A","--all"})
private boolean allFiles;

@Override
public void run() {
    if (allFiles) {
        System.out.println("Adding all files to the staging area");
    } else {
        System.out.println("Adding some files to the staging area");
    }
}

如我们所见,注释采用了一个名称参数,该参数给出了选项的不同名称。 因此,使用-A或–all调用add命令会将allFiles字段设置为true。 因此,如果我们运行带有该选项的命令,控制台将显示"将所有文件添加到暂存区"。

5.2。 带参数的选项

如我们所见,对于不带参数的选项,它们的存在或不存在总是被评估为布尔值。

但是,可以注册带有参数的选项。 我们只需声明我们的字段为其他类型即可完成此操作。 让我们向我们的commit命令添加一个消息选项:

1
2
3
4
5
6
7
8
9
10
@Option(names = {"-m","--message"})
private String message;

@Override
public void run() {
    System.out.println("Committing files in the staging area, how wonderful?");
    if (message != null) {
        System.out.println("The commit message is" + message);
    }
}

毫不奇怪,当给定message选项时,该命令将在控制台上显示提交消息。 在本文的后面,我们将介绍库处理的类型以及如何处理其他类型。

5.3。 具有多个参数的选项

但是现在,如果我们希望我们的命令接受多条消息,就像真正的git commit命令那样,该怎么办? 不用担心,让我们将字段设为数组或Collection,我们已经完成很多工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Option(names = {"-m","--message"})
private String[] messages;

@Override
public void run() {
    System.out.println("Committing files in the staging area, how wonderful?");
    if (messages != null) {
        System.out.println("The commit message is");
        for (String message : messages) {
            System.out.println(message);
        }
    }
}

现在,我们可以多次使用message选项:

1
commit -m"My commit is great" -m"My commit is beautiful"

但是,我们可能也只想给该选项一次,并用正则表达式定界符分隔不同的参数。 因此,我们可以使用@Option注释的split参数:

1
2
@Option(names = {"-m","--message"}, split =",")
private String[] messages;

现在,我们可以通过-m"我的承诺很棒","我的承诺很棒"来达到与上述相同的结果。

5.4。 必选选项

有时,我们可能有一个必需的选项。 必需的参数(默认为false)允许我们执行以下操作:

1
2
@Option(names = {"-m","--message"}, required = true)
private String[] messages;

现在,如果不指定message选项,就无法调用commit命令。 如果我们尝试这样做,picocli将输出错误:

1
2
3
Missing required option '--message=<messages>'
Usage: git commit -m=<messages> [-m=<messages>]...
  -m, --message=<messages>

6.管理位置参数

6.1。 捕获位置参数

现在,让我们关注我们的add命令,因为它还不是很强大。 我们只能决定添加所有文件,但是如果我们想添加特定文件,该怎么办?

我们可以使用另一种方法来执行此操作,但此处更好的选择是使用位置参数。 实际上,位置参数旨在捕获占据特定位置的命令参数,这些参数既不是子命令也不是选项。

在我们的示例中,这将使我们能够执行以下操作:

1
add file1 file2

为了捕获位置参数,我们将使用@Parameters批注:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Parameters
private List<Path> files;

@Override
public void run() {
    if (allFiles) {
        System.out.println("Adding all files to the staging area");
    }

    if (files != null) {
        files.forEach(path -> System.out.println("Adding" + path +" to the staging area"));
    }
}

现在,我们前面的命令将输出:

1
2
Adding file1 to the staging area
Adding file2 to the staging area

6.2。 捕获位置参数的子集

由于注释的index参数,可以更精确地捕获要捕获的位置参数。 索引从零开始。 因此,如果我们定义:

1
@Parameters(index="2..*")

从第三个到最后,这将捕获与选项或子命令不匹配的参数。

索引可以是一个范围或单个数字,代表一个位置。

7.关于类型转换的一句话

正如我们在本教程前面所看到的,picocli本身可以处理一些类型转换。 例如,它将多个值映射到数组或集合,但也可以将参数映射到特定类型,例如当我们将路径类用于add命令时。

实际上,picocli带有许多预处理的类型。 这意味着我们可以直接使用这些类型,而不必考虑自己转换它们。

但是,我们可能需要将命令参数映射到已处理类型以外的其他类型。 对我们来说幸运的是,这要归功于ITypeConverter接口和CommandLine#registerConverter方法,该方法将类型与转换器相关联。

假设我们想将config子命令添加到我们的git命令中,但是我们不希望用户更改不存在的配置元素。 因此,我们决定将这些元素映射到一个枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum ConfigElement {
    USERNAME("user.name"),
    EMAIL("user.email");

    private final String value;

    ConfigElement(String value) {
        this.value = value;
    }

    public String value() {
        return value;
    }

    public static ConfigElement from(String value) {
        return Arrays.stream(values())
          .filter(element -> element.value.equals(value))
          .findFirst()
          .orElseThrow(() -> new IllegalArgumentException("The argument"
          + value +" doesn't match any ConfigElement"));
    }
}

另外,在我们新创建的GitConfigCommand类中,让我们添加两个位置参数:

1
2
3
4
5
6
7
8
9
10
@Parameters(index ="0")
private ConfigElement element;

@Parameters(index ="1")
private String value;

@Override
public void run() {
    System.out.println("Setting" + element.value() +" to" + value);
}

这样,我们确保用户将无法更改不存在的配置元素。

最后,我们必须注册我们的转换器。 美妙的是,如果使用Java 8或更高版本,我们甚至不必创建实现ITypeConverter接口的类。 我们可以将lambda或方法引用传递给registerConverter()方法:

1
2
3
4
CommandLine commandLine = new CommandLine(new GitCommand());
commandLine.registerConverter(ConfigElement.class, ConfigElement::from);

commandLine.parseWithHandler(new RunLast(), args);

这发生在GitCommandmain()方法中。 请注意,我们必须放开convenienceCommandLine.run()方法。

与未处理的配置元素一起使用时,该命令将显示帮助消息以及一条信息,告诉我们无法将参数转换为ConfigElement:

1
2
3
4
5
6
Invalid value for positional parameter at index 0 (<element>):
cannot convert 'user.phone' to ConfigElement
(java.lang.IllegalArgumentException: The argument user.phone doesn't match any ConfigElement)
Usage: git config <element> <value>
      <element>
      <value>

8.与Spring Boot集成

最后,让我们看看如何将所有这些内容进行弹奏!

确实,我们可能正在Spring Boot环境中工作,并希望从我们的命令行程序中受益。 为了做到这一点,我们必须创建一个实现了CommandLineRunner接口的SpringBootApplication:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class Application implements CommandLineRunner {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) {
    }
}

另外,让我们用Spring @Component注释注释所有命令和子命令,并自动装配应用程序中的所有内容:

1
2
3
4
5
6
7
8
9
10
11
12
private GitCommand gitCommand;
private GitAddCommand addCommand;
private GitCommitCommand commitCommand;
private GitConfigCommand configCommand;

public Application(GitCommand gitCommand, GitAddCommand addCommand,
  GitCommitCommand commitCommand, GitConfigCommand configCommand) {
    this.gitCommand = gitCommand;
    this.addCommand = addCommand;
    this.commitCommand = commitCommand;
    this.configCommand = configCommand;
}

注意,我们必须自动装配每个子命令。 不幸的是,这是因为到目前为止,picocli在以声明方式(带有批注)进行声明时仍无法从Spring上下文中检索子命令。 因此,我们必须以编程方式自己进行连接:

1
2
3
4
5
6
7
8
9
@Override
public void run(String... args) {
    CommandLine commandLine = new CommandLine(gitCommand);
    commandLine.addSubcommand("add", addCommand);
    commandLine.addSubcommand("commit", commitCommand);
    commandLine.addSubcommand("config", configCommand);

    commandLine.parseWithHandler(new CommandLine.RunLast(), args);
}

现在,我们的命令行程序对Spring组件来说就像一个魅力。 因此,我们可以创建一些服务类并在命令中使用它们,而letSpring负责依赖注入。

9.结论

在本文中,我们已经了解了picoclilibrary的一些关键功能。 我们已经学习了如何创建新命令并向其中添加一些子命令。 我们已经看到了许多处理选项和位置参数的方法。 另外,我们还学习了如何实现自己的类型转换器以使命令强类型化。 最后,我们已经了解了如何将Spring Boot引入命令。

当然,还有很多事情可以发现。 该库提供了完整的文档。

至于本文的完整代码,可以在我们的GitHub上找到。