Parsing Command-Line Parameters with JCommander
1.概述
在本教程中,我们将学习如何使用JCommander解析命令行参数。 当我们构建一个简单的命令行应用程序时,我们将探索其几个功能。
2.为什么选择JCommander?
"因为生命太短,无法解析命令行参数" –CédricBeust
由CédricBeust创建的JCommander是基于注释的库,用于解析命令行参数。 它可以减少构建命令行应用程序的工作量,并帮助我们为他们提供良好的用户体验。
使用JCommander,我们可以卸载棘手的任务,例如解析,验证和类型转换,以使我们能够专注于应用程序逻辑。
3.设置JCommander
3.1。 Maven配置
让我们从在pom.xml中添加jcommander依赖关系开始:
1 2 3 4 5 | <dependency> <groupId>com.beust</groupId> <artifactId>jcommander</artifactId> <version>1.78</version> </dependency> |
3.2。 你好,世界
<
让我们创建一个简单的HelloWorldApp,它接受一个名为name的输入并输出问候语" Hello
由于JCommander将命令行参数绑定到Java类中的字段,因此我们首先定义一个HelloWorldArgs类,其字段名称用@Parameter注释:
1 2 3 4 5 6 7 8 9 | class HelloWorldArgs { @Parameter( names ="--name", description ="User name", required = true ) private String name; } |
现在,让我们使用JCommander类来解析命令行参数并在HelloWorldArgs对象中分配字段:
1 2 3 4 5 6 | HelloWorldArgs jArgs = new HelloWorldArgs(); JCommander helloCmd = JCommander.newBuilder() .addObject(jArgs) .build(); helloCmd.parse(args); System.out.println("Hello" + jArgs.getName()); |
最后,让我们从控制台使用相同的参数调用主类:
1 2 | $ java HelloWorldApp --name JavaWorld Hello JavaWorld |
4.在JCommander中构建真实的应用程序
现在我们已经启动并运行,让我们考虑一个更复杂的用例-与诸如Stripe之类的计费应用程序交互的命令行API客户端,特别是按计量(或基于使用情况)的计费方案。 该第三方计费服务管理我们的订阅和发票。
假设我们正在运营一个SaaS业务,其中我们的客户购买我们的服务的订阅,并按每月对我们服务的API调用次数收费。 我们将在客户端中执行两项操作:
提交:根据给定的订阅提交客户的使用数量和单价
提取:根据客户在当月的部分或全部订阅的消费来获取费用-我们可以将这些费用汇总到所有订阅中或按每个订阅逐项列出
遍历库的功能时,我们将构建API客户端。
让我们开始!
5.定义参数
让我们开始定义应用程序可以使用的参数。
5.1。 @Parameter批注
用@Parameter注释字段会告诉JCommander将匹配的命令行参数绑定到该字段。 @Parameter具有描述主要参数的属性,例如:
名称–选项的一个或多个名称,例如" -name"或" -n"
说明-选项背后的含义,以帮助最终用户
必需–该选项是否为必需,默认为false
arity –选项使用的其他参数数量
让我们在计费计费方案中配置一个参数customerId:
1 2 3 4 5 6 7 | @Parameter( names = {"--customer","-C" }, description ="Id of the Customer who's using the services", arity = 1, required = true ) String customerId; |
现在,让我们使用新的" –customer"参数执行命令:
1 2 | $ java App --customer cust0000001A Read CustomerId: cust0000001A. |
同样,我们可以使用较短的" -C"参数来达到相同的效果:
1 2 | $ java App -C cust0000001A Read CustomerId: cust0000001A. |
5.2。 必要参数
在必须使用参数的情况下,如果用户未指定参数,则应用程序将退出并抛出ParameterException:
1 2 3 | $ java App Exception in thread"main" com.beust.jcommander.ParameterException: The following option is required: [--customer | -C] |
我们应该注意,通常,解析参数时的任何错误都会导致JCommander中的ParameterException。
6.内置类型
6.1。 IStringConverter接口
JCommander将类型从命令行字符串输入转换为参数类中的Java类型。 IStringConverter接口处理从String到任意类型的参数类型转换。 因此,所有JCommander的内置转换器都实现了此接口。
开箱即用,JCommander支持常见的数据类型,例如String,Integer,Boolean,BigDecimal和Enum。
6.2。 单项类型
Arity与一个选项消耗的其他参数的数量有关。 JCommander的内置参数类型的默认参数为1,布尔值和列表除外。 因此,诸如String,Integer,BigDecimal,Long和Enum之类的常见类型是单别名类型。
6.3。 布尔型
布尔类型或布尔类型的字段不需要任何其他参数-这些选项的偶数为零。
让我们来看一个例子。 也许我们想获取按订购项分类的客户费用。 我们可以添加一个逐项列出的布尔字段,默认情况下为false:
1 2 3 4 | @Parameter( names = {"--itemized" } ) private boolean itemized; |
我们的应用程序将返回汇总费用,其中逐项设置为false。 当我们使用theitemized参数调用命令行时,将字段设置为true:
1 2 | $ java App --itemized Read flag itemized: true. |
除非我们有一个总是需要逐项收费的用例,否则这将很好地起作用,除非另有说明。 我们可以将参数更改为notItemized,但是更清楚的是能够提供false作为itemized的值。
让我们通过对该字段使用默认值true并将其相似性设置为1来介绍这种行为:
1 2 3 4 5 | @Parameter( names = {"--itemized" }, arity = 1 ) private boolean itemized = true; |
现在,当我们指定选项时,该值将设置为false:
1 2 | $ java App --itemized false Read flag itemized: false. |
7.清单类型
JCommander提供了几种将参数绑定到Listfields的方法。
7.1。 多次指定参数
假设我们只想获取客户订阅的子集的费用:
1 2 3 4 | @Parameter( names = {"--subscription","-S" } ) private List<String> subscriptionIds; |
该字段不是必填字段,如果未提供该参数,则应用程序将在所有订阅中获取费用。 但是,我们可以通过多次使用参数名称来指定多个订阅:
1 2 | $ java App -S subscriptionA001 -S subscriptionA002 -S subscriptionA003 Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003]. |
7.2。 使用拆分器的绑定列表
让我们尝试通过传递逗号分隔的字符串来绑定列表,而不是多次指定该选项:
1 2 | $ java App -S subscriptionA001,subscriptionA002,subscriptionA003 Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003]. |
这使用单个参数值(arity = 1)表示列表。 JCommander将使用CommaParameterSplitter类将以逗号分隔的String绑定到我们的List。
7.3。 使用自定义拆分器的绑定列表
我们可以通过实现IParameterSplitter接口来覆盖默认拆分器:
1 2 3 4 5 6 7 |
然后将实现映射到@Parameter中的splitter属性:
1 2 3 4 5 | @Parameter( names = {"--subscription","-S" }, splitter = ColonParameterSplitter.class ) private List<String> subscriptionIds; |
让我们尝试一下:
1 2 | $ java App -S"subscriptionA001:subscriptionA002:subscriptionA003" Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003]. |
7.4。 可变Arity清单
可变Arity允许我们声明可以使用不确定参数的列表,直到下一个选项为止。 我们可以将属性variableArity设置为true来指定此行为。
让我们尝试使用以下方法解析订阅:
1 2 3 4 5 | @Parameter( names = {"--subscription","-S" }, variableArity = true ) private List<String> subscriptionIds; |
当我们运行命令时:
1 2 | $ java App -S subscriptionA001 subscriptionA002 subscriptionA003 --itemized Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003]. |
JCommander将选项" -S"之后的所有输入参数绑定到列表字段,直到下一个选项或命令结束。
7.5。 固定Arity清单
到目前为止,我们已经看到了无边界列表,可以在其中传递任意数量的列表项。 有时,我们可能希望限制传递到"列表"字段的项目数。 为此,我们可以为List字段指定一个整数arity值以使其有界:
1 2 3 4 5 | @Parameter( names = {"--subscription","-S" }, arity = 2 ) private List<String> subscriptionIds; |
固定的Arity会强制检查传递给List选项的参数数量,并在出现违反情况时引发ParameterException:
1 2 | $ java App -S subscriptionA001 subscriptionA002 subscriptionA003 Was passed main parameter 'subscriptionA003' but no main parameter was defined in your arg class |
该错误消息表明,由于JCommander仅需要两个参数,因此尝试解析额外的输入参数" subscriptionA003"作为下一个选项。
8.自定义类型
我们还可以通过编写自定义转换器来绑定参数。 像内置转换器一样,自定义转换器必须实现IStringConverter接口。
让我们编写一个用于解析ISO8601时间戳的转换器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class ISO8601TimestampConverter implements IStringConverter<Instant> { private static final DateTimeFormatter TS_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss"); @Override public Instant convert(String value) { try { return LocalDateTime .parse(value, TS_FORMATTER) .atOffset(ZoneOffset.UTC) .toInstant(); } catch (DateTimeParseException e) { throw new ParameterException("Invalid timestamp"); } } } |
这段代码将解析输入的String并返回一个Instant,如果存在转换错误,则抛出ParameterException。 我们可以通过使用@Parameter中的converter属性将其绑定到Instant类型的字段来使用此转换器:
1 2 3 4 5 | @Parameter( names = {"--timestamp" }, converter = ISO8601TimestampConverter.class ) private Instant timest |
让我们看看它的作用:
1 2 | $ java App --timestamp 2019-10-03T10:58:00 Read timestamp: 2019-10-03T10:58:00Z. |
9.验证参数
JCommander提供了一些默认验证:
是否提供了所需的参数
如果指定的参数数量与字段的匹配性匹配
每个String参数是否可以转换为相应字段的类型
此外,我们不妨添加自定义验证。 例如,假设客户ID必须是UUID。
我们可以为实现字段IParameterValidator的customer字段编写一个验证器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class UUIDValidator implements IParameterValidator { private static final String UUID_REGEX = "[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}"; @Override public void validate(String name, String value) throws ParameterException { if (!isValidUUID(value)) { throw new ParameterException( "String parameter" + value +" is not a valid UUID."); } } private boolean isValidUUID(String value) { return Pattern.compile(UUID_REGEX) .matcher(value) .matches(); } } |
然后,我们可以使用参数的validateWith属性将其连接起来:
1 2 3 4 5 | @Parameter( names = {"--customer","-C" }, validateWith = UUIDValidator.class ) private String customerId; |
如果我们使用非UUID客户ID调用命令,则应用程序退出并显示验证失败消息:
1 2 |
10.子命令
现在我们已经了解了参数绑定,现在让我们将所有内容组合在一起以构建命令。
在JCommander中,我们可以支持多个命令,称为子命令,每个命令都有一组不同的选项。
10.1。 @Parameters批注
我们可以使用@Parameters定义子命令。@ Parameters包含属性commandName来标识命令。
让我们对Submit和fetchas子命令进行建模:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Parameters( commandNames = {"submit" }, commandDescription ="Submit usage for a given customer and subscription," + "accepts one usage item" ) class SubmitUsageCommand { //... } @Parameters( commandNames = {"fetch" }, commandDescription ="Fetch charges for a customer in the current month," + "can be itemized or aggregated" ) class FetchCurrentChargesCommand { //... } |
JCommander使用@Parameters中的属性来配置子命令,例如:
commandNames –子命令的名称; 将命令行参数绑定到以@Parameters注释的类
commandDescription –记录子命令的用途
10.2。 将子命令添加到JCommander
我们使用addCommand方法将子命令添加到JCommander:
1 2 3 4 5 6 7 | SubmitUsageCommand submitUsageCmd = new SubmitUsageCommand(); FetchCurrentChargesCommand fetchChargesCmd = new FetchCurrentChargesCommand(); JCommander jc = JCommander.newBuilder() .addCommand(submitUsageCmd) .addCommand(fetchChargesCmd) .build(); |
addCommand方法使用@Parametersannotation的commandNamesattribute中指定的名称分别注册子命令。
10.3。 解析子命令
要访问用户的命令选择,我们必须首先解析参数:
1 | jc.parse(args); |
接下来,我们可以使用getParsedCommand提取子命令:
1 |
除了标识命令之外,JCommander还将其余命令行参数绑定到子命令中的它们的字段。 现在,我们只需要调用我们要使用的命令:
1 2 3 4 5 6 7 8 9 10 11 12 | switch (parsedCmdStr) { case"submit": submitUsageCmd.submit(); break; case"fetch": fetchChargesCmd.fetch(); break; default: System.err.println("Invalid command:" + parsedCmdStr); } |
11. JCommander使用帮助
我们可以调用用法来呈现用法指南。 这是我们的应用程序使用的所有选项的摘要。 在我们的应用程序中,我们可以在main命令上调用用法,或者在两个命令" submit"和" fetch"中的每一个上分别调用用法。
使用情况显示可以通过两种方式帮助我们:显示帮助选项和错误处理期间。
11.1。 显示帮助选项
我们可以使用boolean参数以及将attributehelp设置为true来在命令中绑定帮助选项:
1 2 | @Parameter(names ="--help", help = true) private boolean help; |
然后,我们可以检测参数中是否传递了" –help",并调用用法:
1 2 3 | if (cmd.help) { jc.usage(); } |
让我们看一下" submit"子命令的帮助输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ java App submit --help Usage: submit [options] Options: * --customer, -C Id of the Customer who's using the services * --subscription, -S Id of the Subscription that was purchased * --quantity Used quantity; reported quantity is added over the billing period * --pricing-type, -P Pricing type of the usage reported (values: [PRE_RATED, UNRATED]) * --timestamp Timestamp of the usage event, must lie in the current billing period --price If PRE_RATED, unit price to be applied per unit of usage quantity reported |
使用方法使用@Parameter属性(例如描述)显示有用的摘要。 标有星号(*)的参数为必填项。
11.2。 错误处理
我们可以捕获ParameterException和调用用法,以帮助用户理解为什么他们的输入不正确。 ParameterException包含JCommander实例以显示帮助:
1 2 3 4 5 6 7 | try { jc.parse(args); } catch (ParameterException e) { System.err.println(e.getLocalizedMessage()); jc.usage(); } |
12,结论
在本教程中,我们使用JCommander来构建命令行应用程序。 尽管我们涵盖了许多主要功能,但官方文档中还有更多功能。
像往常一样,所有示例的源代码都可以在GitHub上获得。