Go 语言命令行解析增强工具包-pflag

一、Pflag的基本介绍与特点

1、pflag的基本介绍:

在使用 Go 进行开发的过程中,命令行参数解析是我们经常遇到的需求。尽管 Go 标准库提供了 flag 包用于实现命令行参数解析,但只能满足基本需要,不支持高级特性。于是 Go 社区中出现了一个叫 pflag 的第三方包,功能更加全面且足够强大。

Github仓库:https://github.com/spf13/pflag

在线文档地址:https://pkg.go.dev/github.com/spf13/pflag

2、pflag的特点:

pflag 作为 Go 内置 flag 包的替代品,具有如下特点:

  • 兼容 Go 标准库中的 flag 包;

    • 如果直接使用 flag 包定义的全局 FlagSet 对象 CommandLine,则完全兼容;

    • 否则当你手动实例化了 FlagSet 对象,这时就需要为每个标志设置一个简短标志(Shorthand);

  • 实现了 POSIX/GNU 风格的 –flags;

  • pflag 与《The GNU C Library》 中「25.1.1 程序参数语法约定」章节中 POSIX 建议语法兼容;

1695197396693763.png

以上截图是直接通过Google翻译进行的汉化,可能会不太准确,大概了解下即可!


二、pflag的安装与简单使用

1、pflag的快速安装:

安装:

go get github.com/spf13/pflag

在代码中引入:

import flag "github.com/spf13/pflag"

2、像使用 Go 标准库中的 flag 包一样使用 pflag:

package main

import (
    "fmt"

    "github.com/spf13/pflag"
)

/** 自定义一个host类型,实现了 pflag.Value() 接口:
    String() string
    Set(string) error
    Type() string
 */
type host struct {
    value string
}

func (h *host) String() string {
    return h.value
}

func (h *host) Set(v string) error {
    h.value = v
    return nil
}

func (h *host) Type() string {
    return "host"
}

func main() {
    var ip *string = pflag.String("ip", "1234", "help message for ip")

    var port int
    pflag.IntVar(&port, "port", 8080, "help message for port")

    // 由于host实现了 pflag.Value 接口,所以可以通过 pflag.Var() 直接赋值
    var h host
    pflag.Var(&h, "host", "help message for host")

    // 解析命令行参数
    pflag.Parse()

    fmt.Printf("ip: %s\n", *ip)
    fmt.Printf("port: %d\n", port)
    fmt.Printf("host: %+v\n", h)

    fmt.Printf("NFlag: %v\n", pflag.NFlag()) // 返回已设置的命令行标志个数
    fmt.Printf("NArg: %v\n", pflag.NArg())   // 返回处理完标志后剩余的参数个数
    fmt.Printf("Args: %v\n", pflag.Args())   // 返回处理完标志后剩余的参数列表
    fmt.Printf("Arg(1): %v\n", pflag.Arg(1)) // 返回处理完标志后剩余的参数列表中第 i 项
}

通过 –help/-h 参数查看命令行程序使用帮助:

# ~> go run main.go --help
Usage of C:\Users\20112153\AppData\Local\Temp\go-build63604208\b001\exe\main.exe:
      --host host   help message for host
      --ip int      help message for ip (default 1234)
      --port int    help message for port (default 8080)
pflag: help requested
exit status 2

注意:在有些终端下执行程序退出后,还会多打印一行 exit status 2,这并不意味着程序没有正常退出,而是因为 –help 意图就是用来查看使用帮助,所以程序在打印使用帮助信息后,主动调用 os.Exit(2) 退出了。

3、简单运行下上面的程序:

# ~> go run main.go --ip 127.0.0.1 a b --host localhost c d
ip: 127.0.0.1
port: 8080
host: {value:localhost}
NFlag: 2
NArg: 4
Args: [a b c d]
Arg(1): b

还有 4 个非选项参数数 a、b、c、d 也都被 pflag 识别并记录了下来。

这点比 flag 要强大,在 flag 包中,非选项参数数只能写在所有命令行参数最后,a、b 出现在这里程序是会报错的。

4、FlagSet的概念:

FlagSet 是一些预先定义好的 Flag 的集合,几乎所有的 Pflag 操作,都需要借助 FlagSet 提供的方法来完成。在实际开发中,我们可以使用两种方法来获取并使用 FlagSet:

  • 方法一,调用 NewFlagSet 创建一个 FlagSet。

flags := pflag.NewFlagSet("test", pflag.ExitOnError)
  • 方法二,使用 Pflag 包定义的全局 FlagSet:CommandLine。实际上 CommandLine 也是由 NewFlagSet 函数创建的。

var testFlag = pflag.StringP("test-flag", "t", "test", "help message for test-flag")


## 其实pflag.StringP()方法会直接调用CommandLine.StringP()方法:
func StringP(name, shorthand string, value string, usage string) *string {
   return CommandLine.StringP(name, shorthand, value, usage)
}

而CommandLine也是一个FlagSet(全局的)
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)


三、pflag的进阶使用(k8s源码中大量使用)

1、命令行标识flag的基本语法:

语法 说明
–flag 适用于 bool 类型标志,或具有 NoOptDefVal 属性的标志
–flag x 适用于非 bool 类型标志,或没有 NoOptDefVal 属性的标志
–flag=x 适用于 bool 类型标志
-n 1234/-n=1234/-n1234 简短标志,非 bool 类型且没有 NoOptDefVal 属性,三者等价

其中:NoOptDefVal 是 no option default values 的简写,即“没设值时将使用默认值”;

  • 整数标志:接受 1234、0664、0x1234,并且可能为负数;

  • 布尔标志:接受 1, 0, t, f, true, false, TRUE, FALSE, True, False;

  • Duration 标志:接受任何对 time.ParseDuration 有效的输入。

2、标识名 Normalize(正常化):

借助 pflag.NormalizedName 我们能够给标识起一个或多个别名、规范化标识名等!

要使用 pflag.NormalizedName,我们需要创建一个函数 normalizeFunc,然后将其通过 flagset.SetNormalizeFunc(normalizeFunc) 注入到 flagset 使其生效!

package main

import (
    "fmt"
    "os"
    "strings"

    "github.com/spf13/pflag"
)

func normalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
    // alias
    switch name {
    case "old-flag-name":
        name = "new-flag-name"
        break
    }

    // 将-、_号的连接符转变为.号
    // --my-flag == --my_flag == --my.flag
    from := []string{"-", "_"}
    to := "."
    for _, sep := range from {
        name = strings.Replace(name, sep, to, -1)
    }
    return pflag.NormalizedName(name)
}

func main() {
    flagset := pflag.NewFlagSet("test", pflag.ExitOnError)

    //var ip = flagset.IntP("new-flag-name", "i", 1234, "help message for new-flag-name")
    var ipNew = flagset.IntP("new.flag.name", "i", 1234, "help message for new-flag-name")
    var myFlag = flagset.IntP("my.flag", "m", 1234, "help message for my-flag")

    flagset.SetNormalizeFunc(normalizeFunc)  // 这步就可以将-、_号的连接符转变为.号
    flagset.Parse(os.Args[1:])  // os.Arg[0]其实就是我们的main.exe

    //fmt.Printf("ip: %d\n", *ip)
    fmt.Printf("ipNew: %d\n", *ipNew)
    fmt.Printf("myFlag: %d\n", *myFlag)
}

运行测试:

# ~> go run main.go --old-flag-name 2 --my-flag 200
ipNew: 2
myFlag: 200
  • 我们传入的old-flag-name,首先会被转变为new-flag-name,然后又被转化为new.flag.name,所以我们获取new.flag.name标识的值可以可以获取到的;

  • my-flag同理;

  • 所有生成的新flag都属于别名,也就是说,原来的Flag标识依然可以使用!

3、NoOptDefVal:

上面也说过,NoOptDefVal = no option default values

创建标志后,可以为标志设置 NoOptDefVal 属性,如果标志具有 NoOptDefVal 属性,当在命令行上设置了标志而没有参数选项,则标志将设置为 NoOptDefVal 指定的值

注意区分 NoOptDefVal 和 默认值的区别!

package main

import (
    "fmt"
    "github.com/spf13/pflag"
)

func main() {
    var myFlag = pflag.IntP("my-flag", "m", 1234, "help message for my-flag")

    pflag.Lookup("my-flag").NoOptDefVal = "4321"  // 一定要在pflag.Parse()之前

    pflag.Parse()

    fmt.Printf("myFlag: %d\n", *myFlag)
}

进行一个简单的测试:

# 使用显式指定的值
# ~> go run main.go --my-flag=1000
myFlag: 1000

# 使用 NoOptDefVal 设置的值
# ~> go run main.go --my-flag
myFlag: 4321

# 使用 pflag.intP() 设置的默认值
# ~> go run main.go
myFlag: 1234

4、弃用/隐藏标识:

使用 flags.MarkDeprecated 可以弃用一个标志,使用 flags.MarkShorthandDeprecated 可以弃用一个简短标志,使用 flags.MarkHidden 可以隐藏一个标志;

但是 弃用/隐藏 的标识,都还是可以继续使用的,只是不建议使用了。

package main

import (
    "fmt"
    "os"

    "github.com/spf13/pflag"
)

func main() {
    flags := pflag.NewFlagSet("test", pflag.ExitOnError)

    var ip = flags.IntP("ip", "i", 1234, "help message for ip")

    var boolVar bool
    flags.BoolVarP(&boolVar, "boolVar", "b", true, "help message for boolVar")

    var h string
    flags.StringVarP(&h, "host", "H", "127.0.0.1", "help message for host")

    // 弃用标志
    flags.MarkDeprecated("ip", "deprecated")
    flags.MarkShorthandDeprecated("boolVar", "please use --boolVar only")

    // 隐藏标志
    flags.MarkHidden("host")

    flags.Parse(os.Args[1:])

    fmt.Printf("ip: %d\n", *ip)
    fmt.Printf("boolVar: %t\n", boolVar)
    fmt.Printf("host: %+v\n", h)
}

运行做以下测试:

# help命令只能看到正常的标识,弃用/隐藏的标识,是看不到的
# ~> go run main.go --help
Usage of test:
      --boolVar   help message for boolVar (default true)
pflag: help requested
exit status 2

## 虽然ip标识已经被弃用,但是依然可以使用,只是会给出提示
# ~> go run main.go --ip 123
Flag --ip has been deprecated, deprecated
ip: 123
boolVar: true
host: 127.0.0.1

### 隐藏的标识也是可以正常使用的
# ~> go run main.go --ip 123 -b --host 10.206.123.155
Flag --ip has been deprecated, deprecated
Flag shorthand -b has been deprecated, please use --boolVar only
ip: 123
boolVar: true
host: 10.206.123.155

5、对原生Flag的兼容性:

由于 pflag 对 flag 包完全兼容,所以可以在一个程序中混用二者:

package main

import (
    "flag"
    "fmt"

    "github.com/spf13/pflag"
)

func main() {
    // 通过pflag包定义的ip标识
    var ip *int = pflag.Int("ip", 1234, "help message for ip")

    // 通过原生flag包定义的port标识
    var port *int = flag.Int("port", 80, "help message for port")

    pflag.CommandLine.AddGoFlagSet(flag.CommandLine)  //将 flag.CommandLine 注册到 pflag 中
    pflag.Parse()

    fmt.Printf("ip: %d\n", *ip)
    fmt.Printf("port: %d\n", *port)
}

运行测试结果如下:

# ~> go run main.go --ip 123 --port 8080
ip: 123
port: 8080

通过 pflag.AddGoFlagSet() 方法将 flag.CommandLine 注册到 pflag 中,那么 pflag 就可以使用 flag 中声明的标志集合了:

根据方法名就知道,意思就是:将 go 原生的 FlagSet 添加到 pflag 中,简单跟以下源码就能明白!

这个点,在k8s源码中也经常使用,就是为了兼容原生Flag。

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐