Apache Groovy 2.5 CliBuilder 更新

作者:Remko Popma
发布时间:2018 年 5 月 30 日上午 11:28


用于快速简洁地构建命令行应用程序的 CliBuilder 类已在 Apache Groovy 2.5 中更新。本文重点介绍了新功能。

CliBuilder2.5 cygwin

groovy.util.CliBuilder 类已弃用

以前版本的 CliBuilder 使用 Apache Commons CLI 作为底层解析器库。从 Groovy 2.5 开始,有一个基于 picocli 解析器的 CliBuilder 替代版本。

建议应用程序显式导入 groovy.cli.picocli.CliBuildergroovy.cli.commons.CliBuildergroovy.util.CliBuilder 类已弃用,并委托给 Commons CLI 版本以实现向后兼容性。

新功能可能只添加到 picocli 版本中,groovy.util.CliBuilder 可能会在 Groovy 的未来版本中删除。Commons CLI 版本适用于依赖于 CliBuilder 的 Commons CLI 实现内部结构且无法轻松迁移到 picocli 版本的应用程序。

接下来,让我们来看看 Groovy 2.5 CliBuilder 中的一些新功能。

类型化选项

Type

选项可以是布尔标志,也可以采用一个或多个选项参数。在以前版本的 CliBuilder 中,您必须为需要参数的选项指定 args: 1,或者为接受多个参数的选项指定 args: '+'

此版本的 CliBuilder 添加了对类型化选项的支持。这在处理解析结果时很方便,但此外,参数数量是从类型推断出来的,因此如果指定了 type,则可以省略 args

例如

def cli = new CliBuilder()
cli.a(type: String, 'a-arg')
cli.b(type: boolean, 'b-arg')
cli.c(type: Boolean, 'c-arg')
cli.d(type: int, 'd-arg')
cli.e(type: Long, 'e-arg')
cli.f(type: Float, 'f-arg')
cli.g(type: BigDecimal, 'g-arg')
cli.h(type: File, 'h-arg')
cli.i(type: RoundingMode, 'i-arg')

def argz = '''-a John -b -d 21 -e 1980 -f 3.5 -g 3.14159
    -h cv.txt -i DOWN and some more'''.split()

def options = cli.parse(argz)
assert options.a == 'John'
assert options.b
assert !options.c
assert options.d == 21
assert options.e == 1980L
assert options.f == 3.5f
assert options.g == 3.14159
assert options.h == new File('cv.txt')
assert options.i == RoundingMode.DOWN
assert options.arguments() == ['and', 'some', 'more']

支持的类型

基于 Commons CLI 的 CliBuilder 支持基元、数字类型、文件、枚举及其数组(使用 StringGroovyMethods#asType(String, Class))。基于 picocli 的 CliBuilder 支持这些类型 以及更多类型

添加更多类型

如果内置类型不能满足您的需求,则可以轻松注册自定义转换器。指定一个 convert 闭包,将 String 参数转换为任何其他类型。例如

import java.nio.file.Paths
import java.time.LocalTime

def cli = new CliBuilder()
cli.a(convert: { it.toUpperCase() }, 'a-arg')    // (1)
cli.p(convert: { Paths.get(it) }, 'p-arg')       // (2)
cli.t(convert: { LocalTime.parse(it) }, 't-arg') // (3)

def options = cli.parse('-a abc -p /usr/home -t 15:31:59'.split())
assert options.a == 'ABC'
assert options.p.absolute && options.p.parent == Paths.get('/usr')
assert options.t.hour == 15 && options.t.minute == 31
1 将一个字符串转换为另一个字符串
2 选项值转换为 java.nio.file.Path
3 选项值转换为 java.time.LocalTime

注解

Annotations

从这个版本开始,Groovy 提供了一个用于处理命令行参数的注解 API。

应用程序可以使用 @groovy.cli.Option 为命名选项或使用 @groovy.cli.Unparsed 为位置参数添加注解字段或方法。当解析器将命令行参数与选项名称或位置参数匹配时,该值将转换为正确的类型并注入到字段或方法中。

为接口的方法添加注解

使用注解的一种方法是在接口的“getter-like”方法(返回值的方法)上。例如

import groovy.cli.*

interface IHello {
    @Option(shortName='h', description='display usage') Boolean help()   // (1)
    @Option(shortName='u', description='user name')     String user()    // (2)
    @Unparsed(description = 'positional parameters')    List remaining() // (3)
}
1 如果在命令行上指定了 -h--help,则方法返回 true
2 方法返回为 -u--user 选项指定的参数值。
3 任何剩余的参数将作为列表从此方法返回。

如何使用此接口(使用 picocli 版本演示其用法帮助)

import groovy.cli.picocli.CliBuilder

def cli = new CliBuilder(name: 'groovy Greeter')
def argz = '--user abc'.split()
IHello hello = cli.parseFromSpec(IHello, argz)
assert hello.user() == 'abc'

hello = cli.parseFromSpec(GreeterI, ['--help', 'Some', 'Other', 'Args'] as String[])
assert hello.help()
cli.usage()
assert hello.remaining() == ['Some', 'Other', 'Args']

这将打印以下用法帮助消息

Usage: groovy Greeter [-h] [-u=<user>] [<remaining>...]
      [<remaining>...]   positional parameters
  -u, --user=<user>      user name
  -h, --help             display usage

当调用 parseFromSpec 时,CliBuilder 会读取注解,解析命令行参数并返回接口的实例。接口方法返回在命令行上匹配的选项值。

为类的属性或 Setter 方法添加注解

使用注解的另一种方法是在类的属性或“setter-like”方法(具有单个参数的 void 方法)上。例如

class Hello {
    @Option(shortName='h', description='display usage') // (1)
    Boolean help

    private String user
    @Option(shortName='u', description='user name')     // (2)
    void setUser(String user) {
        this.user = user
    }
    String getUser() { user }

    @Unparsed(description = 'positional parameters')    // (3)
    List remaining
}
1 如果在命令行上指定了 -h--help,则 help 布尔属性设置为 true
2 使用 -u--user 选项参数值调用 setUser 属性 setter 方法。
3 remaining 属性设置为包含剩余参数(如果有)的新 List

带注解的类可以使用如下

String[] argz = ['--user', 'abc', 'foo']

def cli = new CliBuilder(usage: 'groovy Greeter [option]') // (1)
Hello greeter = cli.parseFromInstance(new Hello(), argz)   // (2)
assert greeter.user == 'abc'                               // (3)
assert greeter.remaining == ['foo']                        // (4)
1 创建一个 CliBuilder 实例。
2 从带注解的实例中提取选项,解析参数,填充并返回提供的实例。
3 验证字符串选项值是否已分配给属性。
4 验证剩余参数属性。

当调用 parseFromInstance 时,CliBuilder 再次读取注解,解析命令行参数并最终返回实例。带注解的字段和 setter 方法使用为关联选项匹配的值进行初始化。

脚本注解

Script

Groovy 2.5 还为 Groovy 脚本提供了新的注解。

@OptionField 等效于组合 @groovy.transform.Field@Option,而 @UnparsedField 等效于组合 @Field@Unparsed

使用这些注解将脚本变量转换为字段,以便 CliBuilder 可以填充这些变量。例如

import groovy.cli.OptionField
import groovy.cli.UnparsedField

@OptionField String user
@OptionField Boolean help
@UnparsedField List remaining

String[] argz = ['--user', 'abc', 'foo']

new CliBuilder().parseFromInstance(this, argz)
assert user == 'abc'
assert remaining == ['foo']

类型化位置参数

此版本的 CliBuilder 为强类型位置参数提供了一些有限的支持。

如果所有位置参数都具有相同的类型,则 @Unparsed 注解可以与 String[] 以外的数组类型一起使用。同样,类型转换在 Commons CLI 版本中使用 StringGroovyMethods#asType(String, Class) 完成,而 picocli 版本的 CliBuilder 支持这些类型 的超集

此功能仅适用于注解 API,不适用于动态 API。下面是一个可以捕获强类型位置参数的接口示例

interface TypedPositionals {
    @Unparsed Integer[] nums()
}

下面的代码演示了类型转换

def argz = '12 34 56'.split()
def cli = new CliBuilder()
def options = cli.parseFromSpec(TypedPositionals, argz)
assert options.nums() == [12, 34, 56]

Apache Commons CLI 功能

FeatureIconAdvancedOptions

有时您可能希望使用底层解析库的高级功能。例如,您可能有一个带有互斥选项的命令行应用程序。下面的代码展示了如何使用 Apache Commons CLI OptionGroup API 来实现这一点

import groovy.cli.commons.CliBuilder
import org.apache.commons.cli.*

def cli = new CliBuilder()
def optionGroup = new OptionGroup()
optionGroup.with {
  addOption cli.option('s', [longOpt: 'silent'], 's option')
  addOption cli.option('v', [longOpt: 'verbose'], 'v option')
}
cli.options.addOptionGroup optionGroup

assert !cli.parse('--silent --verbose'.split()) (1)
1 解析此输入将失败,因为指定了两个互斥选项。

Picocli CliBuilder 功能

FeatureIconAdvancedOptions

强类型列表

list

具有多个值的选项通常使用数组或列表来捕获值。数组可以是强类型的,也就是说,包含 String 以外的元素。picocli 版本的 CliBuilder 允许您对列表执行相同的操作。auxiliaryType 指定元素应转换为的类型。例如

import groovy.cli.picocli.CliBuilder

def cli = new CliBuilder()
cli.T(type: List, auxiliaryTypes: Long, 'typed list')  // (1)

def options = cli.parse('-T 1 -T 2 -T 3'.split())      // (2)
assert options.Ts == [ 1L, 2L, 3L ]                    // (3)
1 定义一个可以有多个整数值的选项。
2 示例命令行。
3 选项值作为 List<Integer>

强类型映射

map

picocli 版本的 CliBuilder 本机支持映射选项。这就像将 Map 指定为选项类型一样简单。默认情况下,键和值都作为字符串存储在映射中,但可以使用 auxiliaryType 指定键和值应转换成的类型。

import groovy.cli.picocli.CliBuilder

def cli = new CliBuilder()
cli.D(args: 2,   valueSeparator: '=', 'Commons CLI style map')                 // (1)
cli.X(type: Map, 'picocli style map support')                                  // (2)
cli.Z(type: Map, auxiliaryTypes: [TimeUnit, Integer].toArray(), 'typed map')   // (3)

def options = cli.parse('-Da=b -Dc=d -Xx=y -Xi=j -ZDAYS=2 -ZHOURS=23'.split()) // (4)
assert options.Ds == ['a', 'b', 'c', 'd']                                      // (5)
assert options.Xs == [ 'x':'y', 'i':'j' ]                                      // (6)
assert options.Zs == [ (DAYS as TimeUnit):2, (HOURS as TimeUnit):23 ]          // (7)
1 Commons CLI 通过指定每个选项必须有两个参数(带有一些分隔符)来提供类似映射的选项。
2 picocli 版本的 CliBuilder 本机支持映射选项。
3 可以为强类型映射指定键类型和值类型。
4 示例命令行。
5 Commons CLI 样式选项提供了一个 [键,值,键,值,…​] 对象列表。
6 picocli 样式选项将结果作为 Map<String, String> 提供。
7 当指定了 auxiliaryTypes 时,映射的键和值将转换为指定的类型,为您提供 Map<TimeUnit, Integer>

带详细概要的用法帮助

iceberg

CliBuilder 一直支持 usage 属性来显示命令的用法帮助概要

// the old way
new CliBuilder(usage: 'myapp [options]').usage()

上面的程序打印

Usage: myapp [options]

这仍然有效,但 picocli 版本有一个更好的替代方法,即使用 name 属性。如果指定 name 而不是 usage,则 picocli 将在一个简洁的概要中显示所有选项,并使用方括号 [] 表示可选元素,使用省略号 …​ 表示可以重复一次或多次的元素。例如

// the new way
def cli = new CliBuilder(name: 'myapp') // detailed synopsis
cli.a('option a description')
cli.b('option b description')
cli.c(type: List, 'option c description')
cli.usage()

上面的程序打印

Usage: myapp [-ab] [-c=PARAM]...
  -a           option a description
  -b           option b description
  -c= PARAM    option c description

使用任意选项名称

freedom c PsychoShadow www.bigstockphoto.com

图片来源:(c) PsychoShadow - www.bigstockphoto.com

以前,如果一个选项有多个名称,并且名称之间只有一个连字符,则必须多次声明该选项

// before: split -cp, -classpath into two options
def cli = new CliBuilder(usage: 'groovyConsole [options] [filename]')
cli.classpath('Where to find the class files')
cli.cp(longOpt: 'classpath', 'Aliases for '-classpath')

Picocli 版本的 CliBuilder 支持 names 属性,该属性可以包含任意数量的选项名称,并且可以使用任何前缀。例如

// after: an option can have many names with any prefix
def cli = new CliBuilder(usage: 'groovyConsole [options] [filename]')
cli._(names: ['-cp', '-classpath', '--classpath'], 'Where to find the class files')

精细的用法帮助消息

sift

Picocli 提供了对用法帮助消息格式的精细控制,此功能通过 usageMessage CliBuilder 属性公开。

用法消息包含多个部分:标题、概要、描述、参数、选项和页脚。每个部分都有一个标题,位于该部分第一行的前面。例如

import groovy.cli.picocli.CliBuilder

def cli = new CliBuilder()
cli.name = "groovy clidemo"
cli.usageMessage.with {                // (1)
    headerHeading("Header heading:%n") // (2)
    header("header 1", "header 2")     // (3)
    synopsisHeading("%nUSAGE: ")
    descriptionHeading("%nDescription heading:%n")
    description("description 1", "description 2")
    optionListHeading("%nOPTIONS:%n")
    footerHeading("%nFooter heading:%n")
    footer("footer 1", "footer 2")
}
cli.a(longOpt: 'aaa', 'a-arg')         // (4)
cli.b(longOpt: 'bbb', 'b-arg')
cli.usage()
1 使用 usageMessage CliBuilder 属性来自定义用法帮助消息。
2 标题可以包含字符串格式说明符,例如 %n 换行符。
3 各部分是多行的:每个字符串将呈现在单独的行上。
4 定义一些选项。

这将打印以下输出

Header heading:
header 1
header 2

USAGE: groovy clidemo [-ab]

Description heading:
description 1
description 2

OPTIONS:
  -a, --aaa    a-arg
  -b, --bbb    b-arg

Footer heading:
footer 1
footer 2

使用 ANSI 颜色的用法帮助

默认情况下,用法帮助消息中的命令名称、选项名称和参数标签使用 ANSI 样式和颜色 呈现。可以使用系统属性配置 这些元素的颜色方案。

除此之外,您还可以在描述和用法帮助消息的其他部分中使用颜色和样式,方法是使用简单的标记符号。下面的示例演示了

def cli = new groovy.cli.picocli.CliBuilder(name: 'myapp')
cli.usageMessage.with {
    headerHeading("@|bold,red,underline Header heading|@:%n")
    header($/@|bold,green \
  ___ _ _ ___      _ _    _
 / __| (_) _ )_  _(_) |__| |___ _ _
| (__| | | _ \ || | | / _` / -_) '_|
 \___|_|_|___/\_,_|_|_\__,_\___|_|
|@/$)
    synopsisHeading("@|bold,underline Usage|@: ")
    descriptionHeading("%n@|bold,underline Description heading|@:%n")
    description("Description 1", "Description 2")      // after the synopsis
    optionListHeading("%n@|bold,underline Options heading|@:%n")
    footerHeading("%n@|bold,underline Footer heading|@:%n")
    footer($/@|bold,blue \
   ___                         ___   ___
  / __|_ _ ___  _____ ___  _  |_  ) | __|
 | (_ | '_/ _ \/ _ \ V / || |  / / _|__ \
  \___|_| \___/\___/\_/ \_, | /___(_)___/
                        |__/             |@/$)
}
cli.a('option a description')
cli.b('option b description')
cli.c(type: List, 'option c description')
cli.usage()

上面的代码给出以下输出

CliBuilder2.5 cygwin

(ASCII 艺术作品的版权归http://patorjk.com/software/taag/所有。)

新的 errorWriter 属性

error

当用户提供无效输入时,Picocli 版本的 CliBuilder 会将错误消息和用法帮助消息写入新的 errorWriter 属性(默认设置为 System.err)。当用户请求帮助并且应用程序调用 CliBuilder.usage() 时,用法帮助消息将打印到 writer 属性(默认设置为 System.out)。

以前版本的 CliBuilder 对无效输入和用户请求的帮助都使用 writer 属性。

为什么要进行此更改?这有助于命令行应用程序作者遵循标准做法,并将诊断输出与程序输出分开:如果 Groovy 程序的输出通过管道传输到另一个程序,则将错误消息发送到 STDERR 可以防止下游程序无意中尝试解析错误输出。另一方面,当用户使用 --help--version 请求帮助时,输出应该发送到 STDOUT,因为用户可能希望将输出通过管道传输到 lessgrep 等实用程序。

为了向后兼容,将 writer 属性设置为另一个值也会将 errorWriter 设置为相同的值。(如果需要,您以后仍然可以将 errorWriter 设置为另一个值。)

陷阱/不兼容性

incompatible

在新版本的 CliBuilder 中,有几个方面与以前的版本或彼此不兼容。

Picocli 版本中不提供属性 optionsformatter

Commons CLI 版本的 CliBuilder 以及以前版本的 CliBuilder 公开了一个类型为 org.apache.commons.cli.Optionsoptions 属性,该属性可用于配置底层的 Commons CLI 解析器,而无需通过 CliBuilder API。Picocli 版本的 CliBuilder 中不提供此属性。读取或写入此属性的应用程序必须导入 groovy.cli.commons.CliBuilder 或修改应用程序。

此外,Picocli 版本的 CliBuilder 中不提供类型为 org.apache.commons.cli.HelpFormatterformatter 属性。如果您的应用程序使用此属性,请考虑改用 usageMessage 属性,或导入 groovy.cli.commons.CliBuilder

属性 parser 在 Picocli 和 Commons CLI 版本中有所不同

Picocli 版本的 CliBuilder 有一个 parser 属性,该属性公开了一个 picocli.CommandLine.Model.ParserSpec 对象,可用于配置解析器行为。

Commons CLI 版本的 CliBuilder 以及以前版本的 CliBuilder 公开了一个类型为 org.apache.commons.cli.CommandLineParserparser 属性。Picocli 版本的 CliBuilder 中不提供此功能。

如果您的应用程序使用 parser 属性来设置不同的 Commons CLI 解析器,请考虑改用 posix 属性,或导入 groovy.cli.commons.CliBuilder

longOption 的不同解析器行为

Commons CLI DefaultParser 识别以单个连字符(例如 -option)为前缀的 longOption 选项名称以及以双连字符(例如 --option)为前缀的选项。这并不总是很明显,因为用法帮助消息仅显示 longOption 选项名称的双连字符前缀。

为了向后兼容,Picocli 版本的 CliBuilder 有一个 acceptLongOptionsWithSingleHyphen 属性:如果解析器应该识别以单个连字符和双连字符为前缀的长选项名称,则将此属性设置为 true。默认值为 false,因此仅识别以双连字符为前缀(--option)的长选项名称。

结论

Groovy 2.5 CliBuilder 提供了许多令人兴奋的新功能。快来试用一下,让我们知道您的想法!

供参考:Groovy 网站 和 GitHub 镜像,picocli 网站picocli GitHub 项目。如果您喜欢您所看到的,请为这些项目点赞!

本文的副本先前已在 picocli 网站上发布。
请在此处查看原文。