Go 语言里 Kong 命令行解析工具的参数验证问题
Kong 是一个用于 go 语言项目的命令行参数解析工具。(其实,可以找到不少用于 go 语言的命令行参数解析工具的开源实现,主要还是 标准库 里的 flag 的原因。。)
这里记录一个在使用 Kong 过程中的关于参数验证的问题和解决。
1. 问题
在进行命令行参数解析时,需要对用户输入内容的有效性进行全面的检查,以符合程序的要求,规范程序的运行。比如有这样的一个例子,程序需要用户指定 IP 地址:
app --ipaddr <..>
IP 地址有约定的格式,比如 12.34.56.78
。在处理 --ipaddr
参数时,需要解析这个字符串(string),还要转化成整数(uint)或者程序所需要的其他格式。基于 Kong,实现过程大概是这样的:
cli struct {
IpAddr string `name:"ipaddr" help:"ip address"`
}
func main() {
kong.Parse(&cli)
var ipaddr net.IP
var err error
if ipaddr, err = net.ParseIP(cli.IpAddr); err != nil {
// ...
}
fmt.Printf("%v, type:%s\n", cli.IpAddr, reflect.TypeOf(cli.IpAddr))
}
这里的问题是,kong.Parse()
出来的结果里,cli.IpAddr
只是一个字符串,至于是否包含了有效的 IP 地址,是在下一步操作里进行的判断。而我们的理解是,在输入步骤应该过滤掉大部分的无效输入(比如格式问题),至于业务程序逻辑相关的不合理输入,才可能需要在其他地方处理。
2. 调整
Kong 本身提供了多种 type decoder
,比如指定一个选项的参数为 type:"existingfile"
时,Kong 把后续的参数字符串参数解析成文件名、并检查文件的有效性,如果文件无效(比如不存在),Kong 会进行错误提示的流程,并结束程序的运行;再比如指定一个选项的参数为 type:"existingdir"
,Kong 在把字符串解析(包括 home ~ 的展开)成目录名后、同时检查目录是否存在,等等。对程序开发来说,这些步骤是自动完成的,不需要额外的编码。
Kong includes a number of builtin custom type mappers. These can be used by specifying the tag type:"
". They are registered with the option function NamedMapper(name, mapper).
从上述文档描述来看,除了预定义的之外,Kong 也允许程序自定义 type 以及相应的操作。
不过文档描述过于简略,无法着手。好在开源项目,很多细节可以从源码开始了解,在文件 mapper_test.go
里,搜索关键字 kong.NamedMapper
、Decode
等来观察处理流程。
对比之后,对于上述的 --ipaddr <..>
可以这样实现:
cli struct {
IpAddr net.IP `name:"ipaddr" help:"ip address" type:"ipaddr"`
}
type ipAddrMapper struct{}
func (ipAddrMapper) Decode(ctx *kong.DecodeContext, v reflect.Value) error {
var err error
var text string
var ipaddr net.IP
// 获取参数字符串
if err = ctx.Scan.PopValueInto("string", &text); err != nil {
// ...
}
// 检查和解析
if ipaddr, err = net.ParseIP(cli.IpAddr); err != nil {
// ...
}
// 写入结果
v.SetBytes(ipaddr)
return nil
}
func main() {
kong.Parse(&cli, kong.NamedMapper("ipaddr", ipAddrMapper{}))
fmt.Printf("%v, type:%s\n", cli.IpAddr, reflect.TypeOf(cli.IpAddr))
}
3. 总结
关键的修改先高亮显示一下:
cli struct {
- IpAddr string `name:"ipaddr" help:"ip address"`
+ IpAddr net.IP `name:"ipaddr" help:"ip address" type:"ipaddr"`
}
修改的内容包括:
- 添加
ipAddrMapper.Decode()
,实现从参数字符串的获取、有效性的检查、解析,并写入结果; - 定义
type:"ipaddr"
,关联到ipAddrMapper
,并在kong.Parse()
里注册; - 使用,
cli.IpAddr
的定义里添加type:"ipaddr"
;并且从string
修改成net.IP
,这样做的好处是,完成解析后可以直接使用cli.IpAddr
;
这就把整个的过程放在了解析工具里面,而不会暴露到代码的其他地方。
运行时是这样的:
$ app --ipaddr 12.34.56.78
12.34.56.78, type:net.IP
$ app --ipaddr 12.34.56.789
app: error: --ipaddr: 12.34.56.789