一、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 建议语法兼容;
以上截图是直接通过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。