1. 概述

本文将深入探讨 Apache Commons CLI 这个 Java 库。它是一个强大的框架,能帮助开发者以高效、标准化的方式为现有软件工具构建命令行接口(CLI)。

该库通过支持定义 CLI 选项和基本验证功能,显著加速了 CLI 的开发流程。 它能解析命令行参数及其值,并将这些参数值传递给实现工具的底层服务。

值得注意的是,Apache Commons CLI 库被广泛应用于 Apache 生态的多个产品中,包括 Kafka、Maven、Ant 和 Tomcat。

我们将介绍 Apache Commons CLI 中的几个核心类,并通过示例程序展示其功能。

2. CLI 的核心关注点

CLI 为工具提供了自动化领域相关任务的能力。在当今 DevOps 工程师的工作中,CLI 已成为不可或缺的工具。

除了工具底层实现的挑战,所有 CLI 都需要处理以下基本需求:

  • 解析命令行参数,提取参数值并传递给底层服务
  • 按特定格式显示帮助信息
  • 显示版本信息
  • 处理缺失的必需选项
  • 处理未知选项
  • 处理互斥选项

3. 核心类解析

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

Apache Commons CLI 核心类图

OptionOptionGroupOptions 类用于定义 CLI 结构。 所有 CLI 选项的定义都封装在 Options 类中。CommandLineParser 类的 parse() 方法使用 Options 类解析命令行。当遇到不符合规范的情况时,parse() 方法会抛出相应的异常。解析完成后,可以通过 CommandLine 类进一步提取 CLI 选项的值。

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

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

4. 实战演练

让我们深入探索 Apache Commons CLI 库的核心类,了解它们如何帮助快速、一致地创建 CLI 工具。

4.1. Maven 依赖配置

首先,在 pom.xml 文件中添加必要的 Maven 依赖

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

4.2. 定义、解析和提取命令行参数

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

psql -h PGSERVER -U postgres -d empDB

或使用长选项格式:

psql --host PGSERVER -username postgres -dbName empDB

两条命令都需要输入数据库服务器主机、用户名和数据库名。第一条使用短选项名,第二条使用长选项名。usernamedbName 是必需选项,而 host 是可选的。如果未提供主机,默认使用 localhost。

现在我们来定义、解析和提取命令行参数:

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

通过调用 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();
}

我们使用 Option.Builder 类设置 CLI 输入选项的短名称、长名称、参数名称和描述。 此外,通过构建器的 required() 方法将之前定义的 -U-d 选项设为必需。

最后,分别将短选项和长选项参数传递给 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);
}

有趣的是,该方法能同时处理短选项和长选项命令。CommandLineParser 类解析参数后,我们通过调用 CommandLine 对象的 getOptionValue() 方法获取参数值。 由于 host 是可选的,我们调用 CommandLine 类的 hasOption() 方法检查其是否存在。若不存在,则使用默认值 localhost

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

4.3. 处理缺失的必需选项

大多数 CLI 在缺少必需选项时应显示错误。假设 psql 命令中缺少必需的 host 选项:

psql -h PGSERVER -U postgres

处理方式如下:

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

*当调用 CommandLineParser 类的 parse() 方法时,会抛出 MissingOptionException,指示缺少必需选项 d*。随后调用 handleException() 方法处理异常。

假设提供了 -d 选项但缺少其参数:

psql -h PGSERVER -U postgres -d

处理方式如下:

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

当在 CommandLineParser 上调用 parse() 方法时,由于 -d 选项后缺少参数,会抛出 MissingArgumentException。之后调用 handleException() 处理异常。

4.4. 处理未知选项

运行命令时有时会提供未知选项:

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

这里提供了不存在的 -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;
        }
    });
}

parse() 方法遇到未知的 -y 选项时,会抛出 UnrecognizedOptionException。之后调用 handleException() 处理运行时异常。

4.5. 处理互斥选项

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

cp -i -f file1 file2

-i 选项在覆盖文件前提示用户,而 -f 选项直接覆盖文件不提示。这两个选项冲突,不应同时使用。

实现验证逻辑:

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

首先,我们使用构造函数(而非 Option.Builder 类)创建相关 Option 对象。这是实例化 Option 类的另一种方式。

*OptionGroup* 类用于分组互斥选项。 因此,我们将两个选项添加到 OptionGroup 对象中,然后将 OptionGroup 对象添加到 Options 对象中。最后,当在 CommandLineParser 类上调用 parse() 方法时,会抛出 AlreadySelectedException,指示选项冲突。

4.6. 显示帮助文本

格式化帮助文本并在终端显示是所有 CLI 工具的常见需求。Apache Commons CLI 通过 HelpFormatter 类解决了这个问题。

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

*HelpFormatter* 类的 printHelp() 方法使用包含 CLI 定义的 Options 对象显示帮助文本。 方法的第一个参数生成顶部显示的 CLI 用法文本。

查看 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. 总结

本文探讨了 Apache Commons CLI 库如何帮助以标准化方式快速高效地创建 CLI。该库简洁易用,值得掌握。

其他库如 JCommanderAirlinePicocli 同样高效且值得探索。与 Apache Commons CLI 不同,它们都支持注解功能。

本文使用的代码可在 GitHub 获取。


原始标题:Intro to the Apache Commons CLI | Baeldung