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 库中的重要类:
Option、OptionGroup 和 Options 类用于定义 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
两条命令都需要输入数据库服务器主机、用户名和数据库名。第一条使用短选项名,第二条使用长选项名。username 和 dbName 是必需选项,而 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。该库简洁易用,值得掌握。
其他库如 JCommander、Airline 和 Picocli 同样高效且值得探索。与 Apache Commons CLI 不同,它们都支持注解功能。
本文使用的代码可在 GitHub 获取。