Intro to the Apache Commons CLI – Apache Commons CLI 简介

最后修改: 2024年 3月 14日

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

1. Overview

1.概述

In this tutorial, we’ll explore the Java Apache Commons CLI library. It’s a framework that empowers developers by helping them build the command line interface (CLI) of existing software tools in an efficient and standard way.

在本教程中,我们将探讨 Java Apache Commons CLI 库。这是一个帮助开发人员以高效、标准的方式构建现有软件工具的命令行界面(CLI)的框架。

The library can speed up the development of CLIs with its support for defining the CLI options and basic validation of them. It helps parse the command line arguments and their values. Finally, the argument values can be passed to the underlying services implementing the tool.

该库支持定义 CLI 选项并对其进行基本验证,从而加快了 CLI 的开发速度。它有助于解析命令行参数及其值。最后,参数值可传递给实现工具的底层服务。

Notably, the Apache Commons CLI library is also used in several of Apache’s products, including Kafka, Maven, Ant, and Tomcat.

值得注意的是,Apache Commons CLI 库还用于 Apache 的多个产品,包括 Kafka、Maven、Ant 和 Tomcat。

We’ll discuss a few important classes from Apache Commons CLI and then use them in sample programs to showcase its capabilities.

我们将讨论 Apache Commons CLI 中的几个重要类,然后在示例程序中使用它们来展示其功能。

2. Key Concerns of a CLI

2.CLI 的主要关注点

CLIs give an edge to tools by helping automate a series of tasks relevant to their domain. Moreover, in today’s world, it’s unimaginable for DevOps engineers to work without CLIs.

CLI 可帮助自动化一系列与其领域相关的任务,从而为工具带来优势。此外,在当今世界,DevOps 工程师的工作离不开 CLI。

Apart from the challenge of the underlying implementation of the tool, all CLIs need to handle some basic requirements:

除了工具底层实现方面的挑战外,所有 CLI 都需要处理一些基本要求:

  • Parse command line arguments, extract argument values, and pass them to the underlying services
  • Display help information with a certain format
  • Display version
  • Handle missing required options
  • Handle unknown options
  • Handle mutually exclusive options

3. Important Classes

3.重要课程

Let’s take a look at the important classes of the Apache Commons CLI library:

让我们来看看 Apache Commons CLI 库的重要 类:

 

The classes Option, OptionGroup, and Options help define a CLI. The definitions of all the CLI options are wrapped into the Options class. The parse() method of the CommandLineParser class uses the Options class to parse the command line. In case of any deviation, appropriate exceptions are thrown by the parse() method. After parsing, the CommandLine class can be probed further to extract the values of the CLI options, if any.

OptionOptionGroupOptions有助于定义 CLI。所有 CLI 选项的定义都封装在 Options 类中。CommandLineParser 类的 parse() 方法使用 Options 类来解析命令行。如果出现任何偏差,parse()方法将抛出适当的异常。解析完成后,CommandLine 类可被进一步探测,以提取 CLI 选项(如果有)的值。

Finally, the extracted values can be passed to the underlying services implementing the CLI tool.

最后,提取的值可以传递给实现 CLI 工具的底层服务。

Similar to the parse() method in the CommandLineParser class, the HelpFormatter also uses the Options Class to display the help text of a CLI tool.

CommandLineParser 类中的 parse() 方法类似,HelpFormatter 也使用 Options 类来显示 CLI 工具的帮助文本。

4. Implementation

4.实施

Let’s explore more about the Apache Commons CLI library classes and understand how they help create a CLI tool consistently and quickly.

让我们进一步了解 Apache Commons CLI 库类,了解它们如何帮助我们一致、快速地创建 CLI 工具。

4.1. Prerequisite Maven Dependency

4.1.Maven 依赖前提

Firstly, let’s add the necessary Maven dependency in the pom.xml file:

首先,让我们在 pom.xml 文件中添加必要的 Maven 依赖关系

<dependency>
    <groupId>commons-cli</groupId>
    <artifactId>commons-cli</artifactId>
    <version>1.6.0</version>
</dependency>

4.2. Define, Parse, and Probe Command Line Arguments

4.2.定义、解析和探测命令行参数

Consider the command to connect to a PostgreSQL database using its psql CLI:

考虑使用 psql CLI 连接 PostgreSQL 数据库的命令:

psql -h PGSERVER -U postgres -d empDB

Alternatively:

或者

psql --host PGSERVER -username postgres -dbName empDB

Both commands require input parameters for the database server host, username, and database name. The first command uses short option names, while the second uses long option names. The username and dbName are required options, whereas the host is optional. If the host is missing, then by default, we consider the localhost as the host value.

这两条命令都需要输入数据库服务器主机、用户名和数据库名称参数。第一个命令使用短选项名,第二个命令使用长选项名。usernamedbName 是必填选项,而 host 是可选选项。如果缺少 host,默认情况下我们会将 localhost 作为主机值。

Now, let’s define, parse, and probe the command line arguments:

现在,让我们定义、解析和探测命令行参数:

@Test
void whenCliOptionProvided_thenParseAndExtractOptionAndArgumentValues() throws ParseException {
    Options options = new Options();

    Option hostOption = createOption("h", "host", "HOST", "Database server host", false);
    Option userNameOption = createOption("U", "username", "USERNAME", "Database user name", true);
    Option dbNameOption = createOption("d", "dbName", "DBNAME", "Database name to connect to", true);

    options.addOption(hostOption)
      .addOption(dbNameOption)
      .addOption(userNameOption);

    String[] commandWithShortNameOptions = new String[] { "-h", "PGSERVER", "-U", "postgres", "-d", "empDB" };
    parseThenProcessCommand(options, commandWithShortNameOptions, "h", "U", "d" );

    String[] commandWithLongNameOptions =  new String[] { "--username", "postgres", "--dbName", "empDB" };
    parseThenProcessCommand(options, commandWithShortNameOptions, "host", "username", "dbName" );
}

To define the options in the command, we created the Option objects corresponding to each of the input options by calling the method createOption():

为了定义命令中的选项,我们通过调用方法 createOption() 创建了与每个输入选项相对应的 Option 对象:

Option createOption(String shortName, String longName, String argName, String description, boolean required) {
    return Option.builder(shortName)
      .longOpt(longName)
      .argName(argName)
      .desc(description)
      .hasArg()
      .required(required)
      .build();
}

We used the Option.Builder class to set the short name, long name, argument name, and description of the input options in the CLI. Additionally, we consider the -U and -d options defined earlier as mandatory with the help of the required() method in the builder class.

我们使用 Option.Builder 类来设置 CLI 中输入选项的短名称、长名称、参数名称和描述。此外,借助构建器类中的 required() 方法,我们将前面定义的 -U-d 选项视为强制选项。

Finally, we pass the arguments with short name options and long name options, respectively, to the method parseThenProcessCommand():

最后,我们分别将带有短名称选项和长名称选项的参数传递给 parseThenProcessCommand() 方法:

void parseThenProcessCommand(Options options, String[] commandArgs, String hostOption,
    String usernameOption, String dbNameOption) throws ParseException {
    CommandLineParser commandLineParser = new DefaultParser();

    CommandLine commandLine = commandLineParser.parse(options, commandArgs);
    String hostname = commandLine.hasOption("h") ? commandLine.getOptionValue(hostOption) : "localhost";
    String username = commandLine.getOptionValue(usernameOption);
    String dbName = commandLine.getOptionValue(dbNameOption);
    if (commandLine.hasOption("h")) {
        assertEquals("PGSERVER", hostname);
    } else {
        assertEquals("localhost", hostname);
    }

    assertEquals("postgres", userName);
    assertEquals("empDB", dbName);
    createConnection(hostname, username, dbName);
}

Interestingly, the method can handle both commands with short names and long names of options. The CommandLineParser class parses the arguments, and then we retrieve their values by calling the getOptionValue() method of the CommandLine object. Since the host is optional, we call the hasOption() method in the class CommandLine to probe if it’s present. If it’s not present, we replace its value with the default localhost.

CommandLineParser 类会解析参数,然后我们会调用 CommandLine 对象的 getOptionValue() 方法来获取参数值。 由于 host 是可选的,因此我们调用类 CommandLine 中的 hasOption() 方法来检测它是否存在。如果不存在,我们就用默认的 localhost. 替换它的值。

Finally, we pass on the values to the underlying services by calling the method createConnection().

最后,我们通过调用方法 createConnection() 将值传递给底层服务。

4.3. Handle Missing Mandatory Options

4.3.处理缺失的强制性选项

In most CLIs, an error should be displayed when a mandatory option is missing. Assume that the mandatory host option is missing in the psql command:

在大多数 CLI 中,如果缺少必选项,就会显示错误。假设在 psql 命令中缺少了必选的 host 选项:

psql -h PGSERVER -U postgres

Let’s see how to handle this:

让我们看看如何处理这个问题:

@Test
void whenMandatoryOptionMissing_thenThrowMissingOptionException() {
    Options options = createOptions();
    String[] commandWithMissingMandatoryOption = new String[]{"-h", "PGSERVER", "-U", "postgres"};
    CommandLineParser commandLineParser = new DefaultParser();
    assertThrows(MissingOptionException.class, () -> {
        try {
            CommandLine commandLine = commandLineParser.parse(options, commandWithMissingMandatoryOption);
        } catch (ParseException e) {
            assertTrue(e instanceof MissingOptionException);
            handleException(new RuntimeException(e));
            throw e;
        }
    });
}

When we invoke the parse() method in the CommandLineParser class, it throws MissingOptionException indicating the absence of the required option d. Following this, we call a method handleException() to manage the exception.

当我们调用 CommandLineParser 类中的 parse() 方法时,它会抛出 MissingOptionException 异常,表示缺少所需的选项 d 。随后,我们调用方法 handleException() 来管理异常。

Suppose the –d option is present, but its argument is missing:

假设存在 –d 选项,但缺少其参数:

psql -h PGSERVER -U postgres -d

Now, let’s see how to handle this:

现在,让我们来看看如何处理这个问题:

@Test
void whenOptionArgumentIsMissing_thenThrowMissingArgumentException() {
    Options options = createOptions();
    String[] commandWithOptionArgumentOption = new String[]{"-h", "PGSERVER", "-U", "postgres", "-d"};
    CommandLineParser commandLineParser = new DefaultParser();
    assertThrows(MissingArgumentException.class, () -> {
        try {
            CommandLine commandLine = commandLineParser.parse(options, commandWithOptionArgumentOption);
        } catch (ParseException e) {
            assertTrue(e instanceof MissingArgumentException);
            handleException(new RuntimeException(e));
            throw e;
        }
    });
}

When the parse() method is invoked on the CommandLineParser, a MissingArgumentException is thrown due to the absence of an argument next to the -d option. Further down, we call handleException() to manage the exception.

在调用 CommandLineParser 上的 parse() 方法时,由于 -d 选项旁边没有参数,因此会抛出 MissingArgumentException 异常。接下来,我们将调用 handleException() 来管理异常。

4.4. Handle Unrecognized Options

4.4.处理无法识别的选项

Sometimes, while running commands, we provide unrecognized options:

有时,在运行命令时,我们会提供一些无法识别的选项:

psql -h PGSERVER -U postgres -d empDB -y

We provided an incorrect non-existent -y option. Let’s see how we handle it in the code:

我们提供了一个不正确、不存在的 -y 选项。让我们看看如何在代码中处理它:

@Test
void whenUnrecognizedOptionProvided_thenThrowUnrecognizedOptionException() {
    Options options = createOptions();
    String[] commandWithIncorrectOption = new String[]{"-h", "PGSERVER", "-U", "postgres", "-d", "empDB", "-y"};
    CommandLineParser commandLineParser = new DefaultParser();
    assertThrows(UnrecognizedOptionException.class, () -> {
        try {
            CommandLine commandLine = commandLineParser.parse(options, commandWithIncorrectOption);
        } catch (ParseException e) {
            assertTrue(e instanceof UnrecognizedOptionException);
            handleException(new RuntimeException(e));
            throw e;
        }
    });
}

The parse() method throws UnrecogniedOptionException when it encounters the unknown -y option. Later, we call handleException() to manage the runtime exception.

parse()方法在遇到未知的-y选项时会抛出UnrecogniedOptionException随后,我们调用handleException()来管理运行时异常。

4.5. Handle Mutually Exclusive Options

4.5.处理互斥选项

Consider the command using cp to copy files in Unix platforms:

考虑使用 cp 命令在 Unix 平台上复制文件:

cp -i -f file1 file2

The -i option prompts before overwriting files; however, the -f option overwrites the files without prompting. Both these options are conflicting and hence should not be used together.

-i选项在覆盖文件前会提示;而-f选项覆盖文件时不会提示。这两个选项相互冲突,因此不应同时使用。

Let’s try implementing this validation:

让我们试着执行这个验证:

@Test
void whenMutuallyExclusiveOptionsProvidedTogether_thenThrowAlreadySelectedException() {
    Option interactiveOption = new Option("i", false, "Prompts the user before overwriting the existing files");
    Option forceOption = new Option("f", false, "Overwrites the existing files without prompting");

    OptionGroup optionGroup = new OptionGroup();
    optionGroup.addOption(interactiveOption)
      .addOption(forceOption);

    Options options = new Options();
    options.addOptionGroup(optionGroup);

    String[] commandWithConflictingOptions = new String[]{"cp", "-i", "-f", "file1", "file2"};
    CommandLineParser commandLineParser = new DefaultParser();
    assertThrows(AlreadySelectedException.class, () -> {
        try {
            CommandLine commandLine = commandLineParser.parse(options, commandWithConflictingOptions);
        } catch (ParseException e) {
            assertTrue(e instanceof AlreadySelectedException);
            handleException(new RuntimeException(e));
            throw e;
        }
    });
}

First, we created the relevant Option objects using its constructor instead of the Option.Builder class. This is also another way of instantiating the Option class.

首先,我们使用其构造函数而不是 Option.Builder 类创建了相关的 Option 对象。这也是实例化 Option 类的另一种方法。

The OptionGroup class helps group mutually exclusive options. Hence, we added the two options to the OptionGroup object. Then, we added the OptionGroup object to the Options object. Finally, when we called the parse() method on CommandLineParser class, it raised AlreadySelectedException, indicating the conflicting options.

OptionGroup类有助于将相互排斥的选项分组。因此,我们将两个选项添加到了OptionGroup对象中。然后,我们将 OptionGroup 对象添加到 Options 对象中。最后,当我们调用 CommandLineParser 类的 parse() 方法时,它引发了 AlreadySelectedException, 异常,这表明存在冲突的选项。

4.6. Display Help Text

4.6.显示帮助文本

Formatting the help text and displaying it on the terminal is a common concern for all the CLI tools. Hence, the Apache Commons CLI addresses this as well with the help of the HelpFormatter class.

格式化帮助文本并将其显示在终端上是所有 CLI 工具共同关心的问题。因此,在 HelpFormatter 类的帮助下,Apache Commons CLI 也解决了这一问题。

Let’s consider the psql CLI as an example:

让我们以 psql CLI 为例进行说明:

@Test
void whenNeedHelp_thenPrintHelp() {
    HelpFormatter helpFormatter = new HelpFormatter();
    Options options = createOptions();
    options.addOption("?", "help", false, "Display help information");
    helpFormatter.printHelp("psql -U username -h host -d empDB", options);
}

The printHelp() method in the class HelpFormatter uses the Options object that holds the definition of the CLI to display the help text. The first argument of the method generates the usage text of the CLI shown at the top.

HelpFormatter 中的 printHelp() 方法使用保存 CLI 定义的 Options 对象来显示帮助文本。该方法的第一个参数将生成显示在顶部的 CLI 使用文本。

Let’s look at the output generated by the HelpFormatter class:

让我们看看 HelpFormatter 类生成的输出:

usage: psql -U username -h host -d empDB
 -?,--help                  Display help information
 -d,--dbName <DBNAME>       Database name to connect to
 -h,--host <HOST>           Database server host
 -U,--username <USERNAME>   Database user name

5. Conclusion

5.结论

In this article, we discussed the Apache Commons CLI library’s ability to help create CLIs quickly and efficiently in a standardized way. Moreover, the library is concise and easy to understand.

在本文中,我们讨论了 Apache Commons CLI 库以标准化方式帮助快速高效地创建 CLI 的能力。此外,该库简洁易懂。

Notably, there are other libraries like JCommander, Airline, and Picocli that are equally efficient and worth exploring. Unlike Apache Commons CLI, all of them support annotation as well.

值得注意的是,JCommanderAirlinePicocli等其他库也同样高效,值得探索。与 Apache Commons CLI 不同的是,它们都支持注释。

As usual, the code used is available over on GitHub.

像往常一样,使用的代码可在 GitHub 上获取。