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.NamedMapperDecode 等来观察处理流程。

对比之后,对于上述的 --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"`
 }

修改的内容包括:

  1. 添加 ipAddrMapper.Decode(),实现从参数字符串的获取、有效性的检查、解析,并写入结果;
  2. 定义 type:"ipaddr",关联到 ipAddrMapper,并在 kong.Parse() 里注册;
  3. 使用,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

Read More: