Create a Java Command Line Program with Picocli – 用Picocli创建一个Java命令行程序

最后修改: 2019年 5月 6日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In this tutorial, we’ll approach the picocli library, which allows us to easily create command line programs in Java.

在本教程中,我们将接近picocli,它允许我们在Java中轻松创建命令行程序。

We’ll first get started by creating a Hello World command. We’ll then take a deep dive into the key features of the library by reproducing, partially, the git command.

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

2. Hello World Command

2.你好世界 “命令

Let’s begin with something easy: a Hello World command!

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

First things first, we need to add the dependency to the picocli project:

首先,我们需要将依赖关系添加到picocli项目中

<dependency>
    <groupId>info.picocli</groupId>
    <artifactId>picocli</artifactId>
    <version>3.9.6</version>
</dependency>

As we can see, we’ll use the 3.9.6 version of the library, though a 4.0.0 version is under construction (currently available in alpha test).

正如我们所看到的,我们将使用3.9.6版本的库,尽管4.0.0版本正在建设中(目前有alpha测试)。

Now that the dependency is set up, let’s create our Hello World command. In order to do that, we’ll use the @Command annotation from the library:

现在依赖关系已经设置好了,让我们来创建我们的Hello World命令。为了做到这一点,我们将使用库中的@Command注解

@Command(
  name = "hello",
  description = "Says hello"
)
public class HelloWorldCommand {
}

As we can see, the annotation can take parameters. We’re only using two of them here. Their purpose is to provide information about the current command and text for the automatic help message.

正如我们所看到的,注解可以接受参数。我们在这里只使用其中的两个。它们的目的是提供关于当前命令的信息和自动帮助信息的文本。

At the moment, there’s not much we can do with this command. To make it do something, we need to add a main method calling the convenience CommandLine.run(Runnable, String[]) method. This takes two parameters: an instance of our command, which thus has to implement the Runnable interface, and a String array representing the command arguments (options, parameters, and subcommands):

目前,我们对这个命令没有什么可以做的。为了让它有所作为,我们需要添加一个main方法,调用方便的CommandLine.run(Runnable, String[])方法。这个方法需要两个参数:一个是我们的命令的实例,因此必须实现Runnable接口,另一个是代表命令参数(选项、参数和子命令)的String数组。

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

Now, when we run the main method, we’ll see that the console outputs “Hello World!”

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

When packaged to a jar, we can run our Hello World command using the java command:

打包成jar后,我们可以使用java命令运行我们的Hello World命令。

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

With no surprise, that also outputs the “Hello World!” string to the console.

毫不奇怪,这也是向控制台输出“Hello World!”字符串。

3. A Concrete Use Case

3.一个具体的用例

Now that we’ve seen the basics, we’ll deep dive into the picocli library. In order to do that, we’re going to reproduce, partially, a popular command: git.

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

Of course, the purpose won’t be to implement the git command behavior but to reproduce the possibilities of the git command — which subcommands exist and which options are available for a peculiar subcommand.

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

First, we have to create a GitCommand class as we did for our Hello World command:

首先,我们必须创建一个GitCommand类,正如我们为Hello World命令所做的那样。

@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. Adding Subcommands

4.添加子命令

The git command offers a lot of subcommandsadd, commit, remote, and many more. We’ll focus here on add and commit.

git命令提供了很多子命令添加、提交、远程,以及更多。我们在这里将重点讨论addcommit

So, our goal here will be to declare those two subcommands to the main command. Picocli offers three ways to achieve this.

所以,我们在这里的目标将是向主命令声明这两个子命令。Picocli提供了三种方法来实现这一目标。

4.1. Using the @Command Annotation on Classes

4.1.在类上使用@Command注解

The @Command annotation offers the possibility to register subcommands through the subcommands parameter:

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

@Command(
  subcommands = {
      GitAddCommand.class,
      GitCommitCommand.class
  }
)

In our case, we add two new classes: GitAddCommand and GitCommitCommand. Both are annotated with @Command and implement Runnable. It’s important to give them a name, as the names will be used by picocli to recognize which subcommand(s) to execute:

在我们的案例中,我们添加了两个新的类。GitAddCommandGitCommitCommand。这两个类都被注解为@Command,并实现了Runnable给它们一个名字很重要,因为这些名字将被picocli用来识别要执行的子命令:

@Command(
  name = "add"
)
public class GitAddCommand implements Runnable {
    @Override
    public void run() {
        System.out.println("Adding some files to the staging area");
    }
}

 

@Command(
  name = "commit"
)
public class GitCommitCommand implements Runnable {
    @Override
    public void run() {
        System.out.println("Committing files in the staging area, how wonderful?");
    }
}

Thus, if we run our main command with add as an argument, the console will output “Adding some files to the staging area”.

因此,如果我们以add为参数运行我们的主命令,控制台将输出“将一些文件添加到暂存区域”

4.2. Using the @Command Annotation on Methods

4.2.在方法上使用@Command注解

Another way to declare subcommands is to create @Command-annotated methods representing those commands in the GitCommand class:

另一种声明子命令的方式是GitCommand类中创建代表这些命令的@Command注解方法

@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?");
}

That way, we can directly implement our business logic into the methods and not create separate classes to handle it.

这样,我们就可以直接在方法中实现我们的业务逻辑,而不是创建单独的类来处理。

4.3. Adding Subcommands Programmatically

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

Finally, picocli offers us the possibility to register our subcommands programmatically. This one’s a bit trickier, as we have to create a CommandLine object wrapping our command and then add the subcommands to it:

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

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

After that, we still have to run our command, but we can’t make use of the CommandLine.run() method anymore. Now, we have to call the parseWithHandler() method on our newly created CommandLine object:

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

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

We should note the use of the RunLast class, which tells picocli to run the most specific subcommand. There are two other command handlers provided by picocli: RunFirst and RunAll. The former runs the topmost command, while the latter runs all of them.

我们应该注意到RunLast类的使用,它告诉picocli要运行最具体的子命令。还有两个由picocli提供的命令处理程序。RunFirstRunAll。前者运行最上面的命令,而后者则运行所有的命令。

When using the convenience method CommandLine.run(), the RunLast handler is used by default.

当使用便利方法CommandLine.run()时,RunLast处理程序被默认使用。

5. Managing Options Using the @Option Annotation

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

5.1. Option with No Argument

5.1.没有参数的选项

Let’s now see how to add some options to our commands. Indeed, we would like to tell our add command that it should add all modified files. To achieve that, we’ll add a field annotated with the @Option annotation to our GitAddCommand class:

现在让我们看看如何给我们的命令添加一些选项。事实上,我们想告诉我们的add命令,它应该添加所有修改过的文件。为了达到这个目的,我们将在我们的GitAddCommand类中添加一个带有@Option注解的字段

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

As we can see, the annotation takes a names parameter, which gives the different names of the option. Therefore, calling the add command with either -A or –all will set the allFiles field to true. So, if we run the command with the option, the console will show “Adding all files to the staging area”.

正如我们所看到的,该注释需要一个names参数,它给出了选项的不同名称。因此,用-A-all调用add命令会将allFiles字段设置为true。因此,如果我们用该选项运行命令,控制台将显示“将所有文件添加到暂存区域”

5.2. Option with an Argument

5.2.带有参数的选项

As we just saw, for options without arguments, their presence or absence is always evaluated to a boolean value.

正如我们刚刚看到的,对于没有参数的选项,其存在与否总是被评估为一个boolean值。

However, it’s possible to register options that take arguments. We can do this simply by declaring our field to be of a different type. Let’s add a message option to our commit command:

然而,我们有可能注册一些带参数的选项。我们可以通过将我们的字段声明为不同的类型来做到这一点。让我们为我们的commit命令添加一个message选项。

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

Unsurprisingly, when given the message option, the command will show the commit message on the console. Later in the article, we’ll cover which types are handled by the library and how to handle other types.

不出所料,当给出message选项时,该命令将在控制台显示提交信息。在文章的后面,我们将介绍哪些类型是由库处理的,以及如何处理其他类型。

5.3. Option with Multiple Arguments

5.3.带有多个参数的选项

But now, what if we want our command to take multiple messages, as is done with the real git commit command? No worries, let’s make our field be an array or a Collection, and we’re pretty much done:

但是现在,如果我们想让我们的命令接受多条信息,就像真正的git commit命令那样,怎么办?不用担心,让我们的字段成为一个array或一个Collection,我们就基本完成了。

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

Now, we can use the message option multiple times:

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

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

However, we might also want to give the option only once and separate the different parameters by a regex delimiter. Hence, we can use the split parameter of the @Option annotation:

然而,我们也可能想只给出一次选项,并通过一个重码分隔符来分隔不同的参数。因此,我们可以使用split参数的@Option注释。

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

Now, we can pass -m “My commit is great”,”My commit is beautiful” to achieve the same result as above.

现在,我们可以通过-m “My commit is great”, “My commit is beautiful”来达到和上面一样的结果。

5.4. Required Option

5.4.必备选项

Sometimes, we might have an option that is required. The required argument, which defaults to false, allows us to do that:

有时,我们可能有一个选项是必须的。required参数,默认为false,允许我们这样做:

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

Now it’s impossible to call the commit command without specifying the message option. If we try to do that, picocli will print an error:

现在,如果不指定message选项,就无法调用commit命令。如果我们试图这样做,picocli将打印一个错误。

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

6. Managing Positional Parameters

6.管理位置参数

6.1. Capture Positional Parameters

6.1.捕获位置参数

Now, let’s focus on our add command because it’s not very powerful yet. We can only decide to add all files, but what if we wanted to add specific files?

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

We could use another option to do that, but a better choice here would be to use positional parameters. Indeed, positional parameters are meant to capture command arguments that occupy specific positions and are neither subcommands nor options.

我们可以使用另一个选项来做到这一点,但这里更好的选择是使用位置参数。事实上,位置参数是为了捕捉占据特定位置的命令参数,这些参数既不是子命令也不是选项。

In our example, this would enable us to do something like:

在我们的例子中,这将使我们能够做这样的事情。

add file1 file2

In order to capture positional parameters, we’ll make use of the @Parameters annotation:

为了捕捉位置参数,我们将利用@Parameters 注解

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

Now, our command from earlier would print:

现在,我们前面的命令将打印出来。

Adding file1 to the staging area
Adding file2 to the staging area

6.2. Capture a Subset of Positional Parameters

6.2.捕捉位置参数的子集

It’s possible to be more fine-grained about which positional parameters to capture, thanks to the index parameter of the annotation. The index is zero-based. Thus, if we define:

由于注解的index参数,我们有可能更细化地捕获哪些位置参数。索引是基于零的。因此,如果我们定义

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

This would capture arguments that don’t match options or subcommands, from the third one to the end.

这将捕获不符合选项或子命令的参数,从第三个到最后一个。

The index can be either a range or a single number, representing a single position.

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

7. A Word About Type Conversion

7.关于类型转换的说明

As we’ve seen earlier in this tutorial, picocli handles some type conversion by itself. For example, it maps multiple values to arrays or Collections, but it can also map arguments to specific types like when we use the Path class for the add command.

正如我们在本教程前面看到的,picocli自己处理一些类型转换。例如,它将多个值映射到arraysCollections,但它也可以将参数映射到特定的类型,比如我们在add命令中使用Path类。

As a matter of fact, picocli comes with a bunch of pre-handled types. This means we can use those types directly without having to think about converting them ourselves.

事实上,picocli自带一堆预先处理的类型。这意味着我们可以直接使用这些类型,而不需要考虑自己转换它们。

However, we might need to map our command arguments to types other than those that are already handled. Fortunately for us, this is possible thanks to the ITypeConverter interface and the CommandLine#registerConverter method, which associates a type to a converter.

然而,我们可能需要将我们的命令参数映射到那些已经被处理的类型之外。幸运的是,这是可能的,这要感谢ITypeConverter接口和CommandLine#registerConverter方法,它将一个类型与一个转换器联系起来

Let’s imagine we want to add the config subcommand to our git command, but we don’t want users to change a configuration element that doesn’t exist. So, we decide to map those elements to an enum:

假设我们想把config子命令添加到我们的git命令中,但我们不想让用户改变一个不存在的配置元素。所以,我们决定将这些元素映射到一个枚举中。

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

Plus, in our newly created GitConfigCommand class, let’s add two positional parameters:

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

@Parameters(index = "0")
private ConfigElement element;

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

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

This way, we make sure that users won’t be able to change non-existent configuration elements.

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

Finally, we have to register our converter. What’s beautiful is that, if using Java 8 or higher, we don’t even have to create a class implementing the ITypeConverter interface. We can just pass a lambda or method reference to the registerConverter() method:

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

CommandLine commandLine = new CommandLine(new GitCommand());
commandLine.registerConverter(ConfigElement.class, ConfigElement::from);

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

This happens in the GitCommand main() method. Note that we had to let go of the convenience CommandLine.run() method.

这发生在GitCommand main()方法中。请注意,我们必须放开方便的CommandLine.run()方法。

When used with an unhandled configuration element, the command would show the help message plus a piece of information telling us that it wasn’t possible to convert the parameter to a ConfigElement:

当与未处理的配置元素一起使用时,该命令将显示帮助信息和一条信息,告诉我们不可能将该参数转换为ConfigElement

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. Integrating with Spring Boot

8.与Spring Boot集成

Finally, let’s see how to Springify all that!

最后,让我们看看如何将所有这些东西春风化雨!

Indeed, we might be working within a Spring Boot environment and want to benefit from it in our command-line program. In order to do that, we must create a SpringBootApplication implementing the CommandLineRunner interface:

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

@SpringBootApplication
public class Application implements CommandLineRunner {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

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

Plus, let’s annotate all our commands and subcommands with the Spring @Component annotation and autowire all that in our Application:

另外,让我们用Spring的@Component注解来注解我们所有的命令和子命令,并在我们的Application中自动连接所有这些。

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

Note that we had to autowire every subcommand. Unfortunately, this is because, for now, picocli is not yet able to retrieve subcommands from the Spring context when declared declaratively (with annotations). Thus, we’ll have to do that wiring ourselves, in a programmatic way:

请注意,我们必须对每个子命令进行自动布线。不幸的是,这是因为目前picocli还不能从Spring上下文中检索到声明性声明的子命令(有注释)。因此,我们必须以编程的方式,自己来完成这种连接。

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

And now, our command line program works like a charm with Spring components. Therefore, we could create some service classes and use them in our commands, and let Spring take care of the dependency injection.

而现在,我们的命令行程序在Spring组件的帮助下工作得很好。因此,我们可以创建一些服务类,并在我们的命令中使用它们,让Spring来处理依赖注入的问题。

9. Conclusion

9.结语

In this article, we’ve seen some key features of the picocli library. We’ve learned how to create a new command and add some subcommands to it. We’ve seen many ways to deal with options and positional parameters. Plus, we’ve learned how to implement our own type converters to make our commands strongly typed. Finally, we’ve seen how to bring Spring Boot into our commands.

在这篇文章中,我们已经看到了picocli库的一些关键功能。我们已经学会了如何创建一个新的命令并为其添加一些子命令。我们看到了许多处理选项和位置参数的方法。此外,我们还学习了如何实现我们自己的类型转换器,使我们的命令具有强类型。最后,我们看到了如何将Spring Boot引入我们的命令。

Of course, there are many things more to discover about it. The library provides complete documentation.

当然,关于它还有很多东西需要发现。该库提供完整的文档

As for the full code of this article, it can be found on our GitHub.

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