使用JCommander解析命令行参数

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
    class ColonParameterSplitter implements IParameterSplitter {

        @Override
        public List split(String value) {
            return asList(value.split(":"));
        }
    }

    然后将实现映射到@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
    $ java App --C customer001
    String parameter customer001 is not a valid UUID.

    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
    String parsedCmdStr = jc.getParsedCommand();

    除了标识命令之外,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上获得。