Apache Groovy 2.5 CliBuilder 更新
作者:Remko Popma
发布时间:2018 年 5 月 30 日上午 11:28
用于快速简洁地构建命令行应用程序的 CliBuilder
类已在 Apache Groovy 2.5 中更新。本文重点介绍了新功能。
groovy.util.CliBuilder
类已弃用
以前版本的 CliBuilder 使用 Apache Commons CLI 作为底层解析器库。从 Groovy 2.5 开始,有一个基于 picocli 解析器的 CliBuilder 替代版本。
建议应用程序显式导入 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))。基于 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 |
注解
从这个版本开始,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 方法使用为关联选项匹配的值进行初始化。
脚本注解
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 功能
有时您可能希望使用底层解析库的高级功能。例如,您可能有一个带有互斥选项的命令行应用程序。下面的代码展示了如何使用 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 以外的元素。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> 。 |
强类型映射
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> 。 |
带详细概要的用法帮助
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')
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')
精细的用法帮助消息
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
属性
当用户提供无效输入时,Picocli 版本的 CliBuilder 会将错误消息和用法帮助消息写入新的 errorWriter
属性(默认设置为 System.err
)。当用户请求帮助并且应用程序调用 CliBuilder.usage()
时,用法帮助消息将打印到 writer
属性(默认设置为 System.out
)。
以前版本的 CliBuilder 对无效输入和用户请求的帮助都使用 writer
属性。
为什么要进行此更改?这有助于命令行应用程序作者遵循标准做法,并将诊断输出与程序输出分开:如果 Groovy 程序的输出通过管道传输到另一个程序,则将错误消息发送到 STDERR 可以防止下游程序无意中尝试解析错误输出。另一方面,当用户使用 --help
或 --version
请求帮助时,输出应该发送到 STDOUT,因为用户可能希望将输出通过管道传输到 less
或 grep
等实用程序。
为了向后兼容,将 writer
属性设置为另一个值也会将 errorWriter
设置为相同的值。(如果需要,您以后仍然可以将 errorWriter
设置为另一个值。)
陷阱/不兼容性
在新版本的 CliBuilder
中,有几个方面与以前的版本或彼此不兼容。
Picocli 版本中不提供属性 options
和 formatter
Commons CLI 版本的 CliBuilder 以及以前版本的 CliBuilder 公开了一个类型为 org.apache.commons.cli.Options
的 options
属性,该属性可用于配置底层的 Commons CLI 解析器,而无需通过 CliBuilder API。Picocli 版本的 CliBuilder 中不提供此属性。读取或写入此属性的应用程序必须导入 groovy.cli.commons.CliBuilder
或修改应用程序。
此外,Picocli 版本的 CliBuilder 中不提供类型为 org.apache.commons.cli.HelpFormatter
的 formatter
属性。如果您的应用程序使用此属性,请考虑改用 usageMessage
属性,或导入 groovy.cli.commons.CliBuilder
。
属性 parser
在 Picocli 和 Commons CLI 版本中有所不同
Picocli 版本的 CliBuilder 有一个 parser
属性,该属性公开了一个 picocli.CommandLine.Model.ParserSpec
对象,可用于配置解析器行为。
Commons CLI 版本的 CliBuilder 以及以前版本的 CliBuilder 公开了一个类型为 org.apache.commons.cli.CommandLineParser
的 parser
属性。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 网站上发布。
请在此处查看原文。