Go应用构建工具–pflag

1. 概述

Go标准库提供了一个解析命令行的flag库,小型项目使用flag库其实已经足够用了,但在大型项目中应用较多的是另一个开源库:pflag,也是今天学习的对象。
pflag是一个用来替代Go标准库flag包的,兼容flag库,几乎不用更改就可以替换,pflag在大型项目中应用的比较广泛,比较知名的有:K8S,Docker,ETCD等。

pflag包github地址为:https://github.com/spf13/pflag
作者spf13大牛,还有其他好几个强大的开源库,包括配置神器Viper,命令行框架cobra等(这两个接下来都会学习一波)

2. 快速使用

package main

import (
	"fmt"

	"github.com/spf13/pflag"
)

var stringFlag = pflag.String("stringflag", "stringflag", "string flag usage")
var stringpFlag = pflag.StringP("stringpflag", "s", "stringpflag", "stringp flag usage")
var intFlag int
var boolFlag bool

func init() {
	pflag.IntVarP(&intFlag, "intflag", "i", 0, "int flag usage")
	pflag.BoolVar(&boolFlag, "boolflag", false, "bool flag usage")
}

func main() {
	pflag.Parse()

	// flag保存在指针的
	fmt.Println("stringflag = ", *stringFlag)

	fmt.Println("stringpflag = ", *stringpFlag)

	// flag绑定在变量
	fmt.Println("intflag = ", intFlag)

	fmt.Println("boolflag = ", boolFlag)

    // 也可以使用Getxxx方法获取
	str, _ := pflag.CommandLine.GetString("stringflag")
	fmt.Println("str = ", str)
}

可以先编译程序,然后再运行,这里直接用go run

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go
stringflag =  stringflag
stringpflag =  stringpflag
intflag =  0
boolflag =  false
str =  stringflag

在没有传递flag时,输出的都是默认值。

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --stringflag hello -s world -i 1234 --boolflag true
stringflag =  hello
stringpflag =  world
intflag =  1234
boolflag =  true
str =  hello

指定flag的值后运行,相应的值都被设置了。
还可以使用-h打印帮助信息

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go -h
Usage of /tmp/go-build2029445686/b001/exe/pflag_fast:
      --boolflag             bool flag usage
  -i, --intflag int          int flag usage
      --stringflag string    string flag usage (default "stringflag")
  -s, --stringpflag string   stringp flag usage (default "stringpflag")
pflag: help requested
exit status 2

小结

  1. 使用pflag定义命令行参数有好几种方式,长选项/短选项/保存在指针/绑定到变量…
  2. pflag.Parse()方法必须放在所有flag都定义后调用,否则flag就无法解析了。

3. PFlag包结构

从上面的快速使用例子里,使用Getxxx方法来获取flag的值:str, _ := pflag.CommandLine.GetString("stringflag")
这里调用了pflag包的CommandLine,查看源码CommandLine的定义如下:

// CommandLine is the default set of command-line flags, parsed from os.Args.
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

实质上CommandLine是由NewFlagSet()创建的一个全局的FlagSet,再看看FlagSet的定义:

// A FlagSet represents a set of defined flags.
type FlagSet struct {
	// Usage is the function called when an error occurs while parsing flags.
	// The field is a function (not a method) that may be changed to point to
	// a custom error handler.
	Usage func()

	// SortFlags is used to indicate, if user wants to have sorted flags in
	// help/usage messages.
	SortFlags bool

	// ParseErrorsWhitelist is used to configure a whitelist of errors
	ParseErrorsWhitelist ParseErrorsWhitelist

	name              string
	parsed            bool
	actual            map[NormalizedName]*Flag
	orderedActual     []*Flag
	sortedActual      []*Flag
	formal            map[NormalizedName]*Flag
	orderedFormal     []*Flag
	sortedFormal      []*Flag
	shorthands        map[byte]*Flag
	args              []string // arguments after flags
	argsLenAtDash     int      // len(args) when a '--' was located when parsing, or -1 if no --
	errorHandling     ErrorHandling
	output            io.Writer // nil means stderr; use out() accessor
	interspersed      bool      // allow interspersed option/non-option args
	normalizeNameFunc func(f *FlagSet, name string) NormalizedName

	addedGoFlagSets []*goflag.FlagSet
}

FlagSet是一个Flag的集合,几乎所有的pflag操作都是借助FlagSet的方法来实现的,默认定义了一个全局的FlagSet:CommandLine,我们拿pflag.IntVarP()方法看看定义:

// IntVarP is like IntVar, but accepts a shorthand letter that can be used after a single dash.
func IntVarP(p *int, name, shorthand string, value int, usage string) {
	CommandLine.VarP(newIntValue(value, p), name, shorthand, usage)
}

// VarPF is like VarP, but returns the flag created
func (f *FlagSet) VarPF(value Value, name, shorthand, usage string) *Flag {
	// Remember the default value as a string; it won't change.
	flag := &Flag{
		Name:      name,
		Shorthand: shorthand,
		Usage:     usage,
		Value:     value,
		DefValue:  value.String(),
	}
	f.AddFlag(flag)
	return flag
}

// VarP is like Var, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) VarP(value Value, name, shorthand, usage string) {
	f.VarPF(value, name, shorthand, usage)
}

其内部调用的就是CommandLine.一般使用全局的FlagSet会更加方便。

我们也可以定义自己的FlagSet:调用NewFlagSet()方法:

var output string
flagset := pflag.NewFlagSet("myflag", pflag.ContinueOnError)

flagset.StringVar(&output, "output", "txt", "Output File Format")

flagset.Parse(os.Args[1:]) // 调用的是自己创建的FlagSet的parse方法!

fmt.Println("output = ", output)

接下来看看源码里的Flag相关定义:

在pflag包里,一个命令行参数会解析为一个Flag类型的变量,Flag定义如下:

// A Flag represents the state of a flag.
type Flag struct {
    Name                string              // name as it appears on command line (flag长选项的名称)
    Shorthand           string              // one-letter abbreviated flag (flag短选项的名称,一个字母)
    Usage               string              // help message (flag的使用帮助信息)
    Value               Value               // value as set (flag的值)
    DefValue            string              // default value (as text); for usage message (flag的默认值)
    Changed             bool                // If the user set the value (or if left to default) (记录flag的值是否被设置过)
    NoOptDefVal         string              // default value (as text); if the flag is on the command line without any options (当flag出现在命令行,但是没有指定选项值时的默认值)
    Deprecated          string              // If this flag is deprecated, this string is the new or now thing to use (如果该flag被废弃,显示的使用帮助信息)
    Hidden              bool                // used by cobra.Command to allow flags to be hidden from help/usage text (为true,则在help输出信息中隐藏这个flag)
    ShorthandDeprecated string              // If the shorthand of this flag is deprecated, this string is the new or now thing to use(如果该flag的短选项被废弃,显示的使用帮助信息)
    Annotations         map[string][]string // used by cobra.Command bash autocomple code (注解,用于cobra的bash自动补全)
}

Flag的值是一个Value类型的接口,定义如下:

// Value is the interface to the dynamic value stored in a flag.
// (The default value is represented as a string.)
type Value interface {
	String() string
	Set(string) error
	Type() string
}

也就是说只要实现了Value接口的结构,就是一个新的类型值,我们拿源码里的String类型看看:

func (s *stringValue) Set(val string) error {
	*s = stringValue(val)
	return nil
}
func (s *stringValue) Type() string {
	return "string"
}

func (s *stringValue) String() string { return string(*s) }	

这里简单的自定义一个新类型:

type Person struct {
	Name string
	Age  int
}

func newPersonValue(val Person, p *Person) *Person {
	*p = val
	return (*Person)(p)
}

func (p *Person) Set(val string) error {
	strs := strings.Split(val, "@")

	p.Name = strs[0]
	p.Age, _ = strconv.Atoi(strs[1])

	return nil
}

func (p *Person) Type() string {
	return "Person"
}

func (p *Person) String() string {
	return fmt.Sprintf("%s@%d", p.Name, p.Age)
}

func PersonVarP(p *Person, name, shorthand string, value Person, usage string) {
	pflag.CommandLine.VarP(newPersonValue(value, p), name, shorthand, usage)
}

var personFlag Person

func init() {
	PersonVarP(&personFlag, "person", "p", Person{Name: "abc", Age: 12}, "input person msg")
}

func main() {
	pflag.Parse()

	fmt.Printf("person = %+v\n", personFlag)
}

测试如下:

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go
person = {Name:abc Age:12}
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go -p zhangsan@18
person = {Name:zhangsan Age:18}
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --person lisi@27
person = {Name:lisi Age:27}
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go -h
Usage of /tmp/go-build1927169625/b001/exe/pflag_fast:
  -p, --person Person   input person msg (default abc@12)
pflag: help requested
exit status 2

4. 使用方法

  1. 多种命令行参数定义方式
    定义方式按flag值存储位置分为两种:保存在指针或绑定到变量
  • flag值保存在指针
    • 支持长选项,默认值和使用说明文本
      比如:
    var name = flag.String("name", "lucas", "Input your name")
    
    func main() {
        flag.Parse()
        fmt.Println("name = ", *name)
    }
    
    
    • 支持长选项,短选项,默认值和使用说明文本
      比如:
    var name = flag.StringP("name", "n", "lucas", "Input your name")
    func main() {
        flag.Parse()
        fmt.Println("name = ", *name)
    }
    
  • flag值绑定到变量
    • 支持长选项,默认值和使用说明文本
      比如:
    var name string
    func main() {
        flag.StringVar(&name, "name", "lucas", "Input your name")
    
        flag.Parse()
        fmt.Println("name = ", name)
    }
    
    • 支持长选项,短选项,默认值和使用说明文本
      比如:
    var name string
    func main() {
        flag.StringVarP(&name, "name", "n", "lucas", "Input your name")
    
        flag.Parse()
        fmt.Println("name = ", name)
    }
    
    从上面的函数命名中可以总结出以下规律:
    • 函数名带P的说明是支持短选项的
    • 函数名带Var的说明是将flag绑定到变量,否则则是存储在指针中
  1. 定义flag使用flag.String(),Bool(),Int()等,后缀可以带Var,P等
  2. 所有flag定义完成后,需要调用flag.Parse()去解析命令行参数
  3. 使用Get<Type>获取参数的值
    可以使用Get<Type>的方法来获取flag的值,比如GetString(),GetInt(),其中<Type>表示pflag支持的所有类型。
    需要注意的是,要获取的flag必须存在且类型必须和<Type>一致,比如获取上述的name,就可以使用GetString("name")来获取

注意:
这里调用这些Get<Type>方法都是FlagSet这个结构的方法集里的,以GetString为例,它的方法定义是这样的:
func (f *FlagSet) GetString(name string) (string, error)
而我们使用全局变量CommandLine调用时候需要这样使用:name, err := flag.CommandLine.GetString("name")

  1. 获取参数
    参数在标签flag之后,在调用pflag.Parse()之后,可以调用pflag提供的几个方法用来获取非flag的参数:
// Arg returns the i'th command-line argument.  Arg(0) is the first remaining argument
// after flags have been processed.
func Arg(i int) string 
// Args returns the non-flag command-line arguments.
func Args() []string
// NArg is the number of arguments remaining after flags have been processed.
func NArg() int

Arg(i int): 返回第i个非flag参数
Args(): 返回所有非flag的参数
NArg(): 返回非flag的参数个数
使用示例如下:

var stringFlag = pflag.String("stringflag", "stringflag", "string flag usage")
var stringpFlag = pflag.StringP("stringpflag", "s", "stringpflag", "stringp flag usage")
var intFlag int
var boolFlag bool

func init() {
	pflag.IntVarP(&intFlag, "intflag", "i", 0, "int flag usage")
	pflag.BoolVar(&boolFlag, "boolflag", false, "bool flag usage")
}

func main() {

	pflag.Parse()

	fmt.Printf("arg个数是: %v\n", pflag.NArg())
	fmt.Printf("arg: %v\n", pflag.Args())
	fmt.Printf("第1个arg是:%v\n", pflag.Arg(0))
}

执行如下:

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --stringflag Hello arg0 arg1 arg2
arg个数是: 3
arg: [arg0 arg1 arg2]
第1个arg是:arg0
  1. 没有给flag设置选项默认值(Setting no option default values for flags)
    当你创建了一个flag后,可以给这个flag设置pflag.NoOptDefVal.
    如果一个flag具有NoOptDefVal,并且该flag在命令行上没有传递这个flag的值,那么这个flag的值就被设置为NoOptDefVal指定的值。
    示例:
var port = pflag.IntP("port", "p", 6379, "redis port")

func main() {

	pflag.Lookup("port").NoOptDefVal = "8080"

	pflag.Parse()

	fmt.Printf("redis port = %v\n", *port)
}

执行如下:

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go
redis port = 6379
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --port 8888
redis port = 8080
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --port
redis port = 8080
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --port=8888
redis port = 8888

总结结果如下表:

命令行flag解析结果
–port=8888port=8888
–portport=8080
[nothing]port=6379

注意:
这里需要注意一点,就是指定了NoOptDefVal后,在命令行传递flag时,不能再用诸如--flag xxx,要用--flag=xxx,因为--flag xxx会被认为是没有设置值,从而使用了NoOptDefVal的值,xxx则会被认为是arg

  1. 命令行flag格式
    pflag包的---是不同的,-表示短选项,--表示长选项(标准库flag包---作用是一样的)。
    pflag包支持以下几种格式:
--flag    // 支持布尔类型flag,或者设置了NoOptDefVal的flag
--flag x  // 只支持没有设置NoOptDefVal的flag(这个上面的"注意"也有提到原因)
--flag=x

注意:pflag遇到--后会停止解析,这一点不同于flag包

  1. 改变或标准化flag名
    pflag允许在构建代码或者使用命令行时标准化flag名,比如我们创建了flag为my-flag,但是用户传递时候写错成my.flag或者my_flag,也可以正常识别:
func wordSepNormailzeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
	from := []string{"-", "_"}
	to := "."

	for _, sep := range from {
		name = strings.Replace(name, sep, to, -1)
	}
	return pflag.NormalizedName(name)
}

var myFlag = pflag.String("my-flag", "myflag", "myflag test")

func main() {

	pflag.CommandLine.SetNormalizeFunc(wordSepNormailzeFunc)

	pflag.Parse()

	fmt.Println("myFlag = ", *myFlag)
}

执行如下:

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --my-flag=hello
myFlag =  hello
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --my_flag hello
myFlag =  hello
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --my.flag hello
myFlag =  hello
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --my+flag hello
unknown flag: --my+flag
Usage of /tmp/go-build951017333/b001/exe/pflag_fast:
      --my.flag string      myflag test (default "myflag")
  -p, --port int            redis port (default 6379)
      --stringflag string   string flag usage (default "stringflag")
unknown flag: --my+flag
exit status 2
  1. 弃用flag或flag缩写
    pflag支持设置flag或其简写为弃用的,弃用的flag在帮助文本中会被隐藏,并且在使用弃用的flag时会打印提示信息。
    示例如下:
var stringFlag = pflag.String("stringflag", "stringflag", "string flag usage")
var port = pflag.IntP("port", "p", 6379, "redis port")
var intFlag = pflag.IntP("intflag", "i", 0, "int flag usage")
func main() {

	pflag.CommandLine.MarkDeprecated("port", "please use --new-port instead")
    // 只弃用缩写,保留长选项
    pflag.CommandLine.MarkShorthandDeprecated("intflag", "please use --intflag only")
	pflag.Parse()
}

执行如下:

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --port 1234
Flag --port has been deprecated, please use --new-port instead
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --help
Usage of /tmp/go-build2446988640/b001/exe/pflag_fast:
      --stringflag string   string flag usage (default "stringflag")
pflag: help requested
exit status 2
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go -i 12
Flag shorthand -i has been deprecated, please use --intflag only
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --intflag 12

注意:usage文本是必需的,并且不应该为空

  1. 隐藏flag
    pflag包支持将flag标记为隐藏的,这样子在程序内部这个flag仍是正常运行的,但是不会显示在帮助文本中。
var stringFlag = pflag.String("stringflag", "stringflag", "string flag usage")
var port = pflag.IntP("port", "p", 6379, "redis port")
var intFlag = pflag.IntP("intflag", "i", 0, "int flag usage")

func main() {
	pflag.CommandLine.MarkHidden("port")
	pflag.Parse()
}

执行help结果:

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run pflag_fast.go --help
Usage of /tmp/go-build739458557/b001/exe/pflag_fast:
  -i, --intflag int         int flag usage
      --stringflag string   string flag usage (default "stringflag")
pflag: help requested
exit status 2
  1. 禁用flag排序
    pflag支持在显示帮助文本时禁用flag排序,比如:
pflag.BoolP("verbose", "v", false, "verbose output")
pflag.String("coolflag", "yeaah", "it's really cool flag")
pflag.Int("usefulflag", 777, "sometimes it's very useful")
pflag.CommandLine.SortFlags = false
pflag.PrintDefaults()

输出如下(按定义的顺序输出):

-v, --verbose           verbose output
    --coolflag string   it's really cool flag (default "yeaah")
    --usefulflag int    sometimes it's very useful (default 777)

如果设置pflag.CommandLine.SortFlags = true,输出如下:

    --coolflag string   it's really cool flag (default "yeaah")
    --usefulflag int    sometimes it's very useful (default 777)
-v, --verbose           verbose output
  1. 支持Go的flag包
import (
	goflag "flag"
	flag "github.com/spf13/pflag"
)

var ip *int = flag.Int("flagname", 1234, "help message for flagname")

func main() {
	flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
	flag.Parse()
}
Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐