Apache Groovy 2.5 CliBuilder 更新

作者: Remko Popma

发布时间:2018-05-30 上午 11:28


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

CliBuilder2.5 cygwin

groovy.util.CliBuilder 类已弃用

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

建议应用程序显式导入 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<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

注解

Annotations

从这个版本开始,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 方法将使用与相关选项匹配的值进行初始化。

脚本注解

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) 完成,而 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 特性

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 以外的元素。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>

强类型映射

map

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>

带详细概要的使用帮助

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')

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')

细粒度使用帮助消息

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

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

CliBuilder 的早期版本将 writer 属性用于无效输入和用户请求帮助。

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

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

注意事项/不兼容性

incompatible

新版本的 CliBuilder 在某些方面与以前的版本或彼此不兼容。

Picocli 版本中 optionsformatter 属性不可用

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

此外,类型为 org.apache.commons.cli.HelpFormatterformatter 属性在 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.CommandLineParserparser 属性。此功能在 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 网站上发布。
请参阅此处原文。