Apache Groovy 2.5 CliBuilder 更新
发布时间:2018-05-30 上午 11:28
用于快速简洁地构建命令行应用程序的 CliBuilder
类已在 Apache Groovy 2.5 中更新。本文重点介绍了新功能。

groovy.util.CliBuilder
类已弃用
CliBuilder 的早期版本使用 Apache Commons CLI 作为底层解析器库。从 Groovy 2.5 开始,CliBuilder 有一个基于 picocli 解析器的替代版本。
建议应用程序显式导入 groovy.cli.picocli.CliBuilder
或 groovy.cli.commons.CliBuilder
。groovy.util.CliBuilder
类已弃用,并为了向后兼容而委托给 Commons CLI 版本。
新功能可能只会添加到 picocli 版本中,而 groovy.util.CliBuilder
可能会在 Groovy 的未来版本中删除。Commons CLI 版本适用于依赖 CliBuilder 的 Commons CLI 实现内部机制且无法轻松迁移到 picocli 版本的应用程序。
接下来,让我们看看 Groovy 2.5 CliBuilder 中的一些新功能。
类型化选项

选项可以是布尔标志,也可以接受一个或多个选项参数。在 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<T>))。基于 picocli 的 CliBuilder 支持这些类型以及更多。
添加更多类型
如果内置类型不满足您的需求,可以轻松注册自定义转换器。指定一个 convert
Closure 来将 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 | 将一个 String 转换为另一个 String |
2 | 选项值转换为 java.nio.file.Path |
3 | 选项值转换为 java.time.LocalTime |
注解

从这个版本开始,Groovy 提供了用于处理命令行参数的注解 API。
应用程序可以使用 @groovy.cli.Option
注解字段或方法以表示命名选项,或使用 @groovy.cli.Unparsed
注解以表示位置参数。当解析器将命令行参数与选项名称或位置参数匹配时,值将被转换为正确的类型并注入到字段或方法中。
注解接口的方法
使用注解的一种方式是将其应用于接口的“类似 getter”方法(返回值的方法)。例如
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”方法(带单个参数的 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 | 验证 String 选项值是否已分配给属性。 |
4 | 验证剩余参数属性。 |
当调用 parseFromInstance
时,CliBuilder
再次读取注解,解析命令行参数,最后返回实例。注解的字段和 setter 方法将使用与相关选项匹配的值进行初始化。
脚本注解

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) 完成,而 CliBuilder 的 picocli 版本支持这些类型的一个超集。
此功能仅适用于注解 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 特性

有时您可能希望使用底层解析库的高级功能。例如,您可能有一个具有互斥选项的命令行应用程序。下面的代码演示了如何使用 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 特性

强类型列表

具有多个值的选项通常使用数组或列表来捕获值。数组可以是强类型的,即,包含除 String 以外的元素。CliBuilder 的 picocli 版本允许您对列表执行相同的操作。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> 。 |
强类型映射

CliBuilder 的 picocli 版本提供了对 Map 选项的原生支持。这就像将 Map 指定为选项类型一样简单。默认情况下,键和值都以字符串形式存储在 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 通过指定每个选项必须有两个参数,并带有一些分隔符,来实现类似 Map 的选项。 |
2 | CliBuilder 的 picocli 版本对 Map 选项提供原生支持。 |
3 | 可以为强类型映射指定键类型和值类型。 |
4 | 一个命令行示例。 |
5 | Commons CLI 风格的选项给出了 [key, value, key, value, …] 对象的列表。 |
6 | picocli 风格的选项将结果作为 Map<String, String> 返回。 |
7 | 指定 auxiliaryTypes 时,Map 的键和值将转换为指定类型,从而得到 Map<TimeUnit, Integer> 。 |
带详细概要的使用帮助

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
使用任何选项名称

图片来源:(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')
CliBuilder 的 picocli 版本支持一个 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')
细粒度使用帮助消息

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()
上述代码给出以下输出

(ASCII 艺术归功于 http://patorjk.com/software/taag/。)
新增 errorWriter
属性

当用户提供无效输入时,CliBuilder 的 picocli 版本将错误消息和使用帮助消息写入新的 errorWriter
属性(默认设置为 System.err
)。当用户请求帮助,并且应用程序调用 CliBuilder.usage()
时,使用帮助消息将打印到 writer
属性(默认设置为 System.out
)。
CliBuilder 的早期版本将 writer
属性用于无效输入和用户请求帮助。
为什么会有这个变化?这有助于命令行应用程序的作者遵循标准做法,将诊断输出与程序输出分开:如果 Groovy 程序的输出通过管道传输到另一个程序,将错误消息发送到 STDERR 可以防止下游程序无意中尝试解析错误输出。另一方面,当用户请求 --help
或 --version
帮助时,输出应该发送到 STDOUT,因为用户可能希望将输出通过管道传输到 less
或 grep
等实用程序。
为了向后兼容,将 writer
属性设置为另一个值也会将 errorWriter
设置为相同的值。(如果需要,您仍然可以在之后将 errorWriter
设置为另一个值。)
注意事项/不兼容性

新版本的 CliBuilder
在某些方面与以前的版本或彼此不兼容。
Picocli 版本中 options
和 formatter
属性不可用
CliBuilder 的 Commons CLI 版本以及 CliBuilder 的早期版本,公开了一个类型为 org.apache.commons.cli.Options
的 options
属性,该属性可用于在不通过 CliBuilder API 的情况下配置底层 Commons CLI 解析器。此属性在 CliBuilder 的 picocli 版本中不可用。读取或写入此属性的应用程序必须导入 groovy.cli.commons.CliBuilder
或修改应用程序。
此外,类型为 org.apache.commons.cli.HelpFormatter
的 formatter
属性在 CliBuilder 的 picocli 版本中不可用。如果您的应用程序使用此属性,请考虑改用 usageMessage
属性,或导入 groovy.cli.commons.CliBuilder
。
Picocli 和 Commons CLI 版本中 parser
属性不同
CliBuilder 的 picocli 版本有一个 parser
属性,它公开了一个 picocli.CommandLine.Model.ParserSpec
对象,可用于配置解析器行为。
CliBuilder 的 Commons CLI 版本和 CliBuilder 的早期版本公开了一个类型为 org.apache.commons.cli.CommandLineParser
的 parser
属性。此功能在 CliBuilder 的 picocli 版本中不可用。
如果您的应用程序使用 parser
属性来设置不同的 Commons CLI 解析器,请考虑改用 posix
属性,或导入 groovy.cli.commons.CliBuilder
。
longOption
的不同解析器行为
Commons CLI DefaultParser
识别带有单个连字符前缀(例如,-option
)和带有双连字符前缀(例如,--option
)的 longOption
选项名称。这并不总是很明显,因为使用帮助消息仅显示 longOption
选项名称的双连字符前缀。
为了向后兼容,CliBuilder 的 picocli 版本有一个 acceptLongOptionsWithSingleHyphen
属性:如果解析器应该识别带有单个连字符和双连字符前缀的长选项名称,请将此属性设置为 true
。默认值为 false
,因此只识别带有双连字符前缀(--option
)的长选项名称。
结论
Groovy 2.5 CliBuilder 提供了许多令人兴奋的新功能。尝试一下,让我们知道您的想法!
参考:Groovy 网站 和 GitHub 镜像,picocli 网站 和 picocli GitHub 项目。如果您喜欢您所看到的,请给项目加星!
本文副本之前已在 picocli 网站上发布。
请参阅此处原文。