golang flag包源码解析

在写命令行程序(工具、server)时,对命令行参数进行解析,是一种常见的需求。各种语言一般都会提供相应的方法或库,以方便开发者使用。在go标准库中提供了一个包:flag,方便进行命令行解析。也就是说,Go的flag包用来解析命令行参数。

flag 用法

命令行flag的语法有如下三种形式:

-flag // 只支持bool类型
-flag=x
-flag x // 只支持非bool类型

第三种形式只能用于非bool类型的原因是:对于这样的命令 cmd -x *,如果有一个文件名字是:0或false等,则命令的原意会改变。因为bool类型支持-flag这种形式,所以go语言在Parse()方法中对bool类型进行了特殊处理。默认的,若提供了-flag,则对应的值为true;否则,则为flag.Bool/BoolVar中指定的默认值;如果希望显示地设置为false,则使用-flag=false。

先看一段输出:

var bval = flag.Bool("bool", false, "bool value for test")
var ival = flag.Int("int", 100, "integer value for test")
var sval = flag.String("string", "null", "string value for test")
var tval = flag.Duration("time", 10*time.Second, "time duration for test")
func main() {
    flag.Parse()
    fmt.Println("bool:\t", *bval)
    fmt.Println("int:\t", *ival)
    fmt.Println("string:\t", *sval)
    fmt.Println("time:\t", *tval)
}

========================================================================

$ go build -o echoflag.bin echoflag.go
$ ./echoflag.bin -bool -int 10 --string "string for test" --time=100s argv
bool:    true
int:     10
string:  string for test
time:    1m40s

1. 布尔类型的参数仅有标记没有取值,指定bool表示标记为True,不指定就是False
2. 整型参数的取值为10
3. 字符串参数的取值为string for test”,注意我们这里使用了双连接线,这与-string效果是一样的
4. 标记与标记值之间可以用空格或等号分隔,如时间参数我们则使用了等号
5. 最后一个参数argv不带连接线,因此被当做普通参数处理,flag包对此不做解析

程序首先定义了4个全局变量(也可以使用局部变量),调用flag的Bool、Int、String和Duration函数给它们赋值,然后在main函数一开始调用flag的Parse函数解析命令行参数,由于全局变量的初始化先于main函数,因此调用Parse时4个标志已经被登记到flag包内部了,Parse在解析时会参考这些信息,并结合实际输入的命令行参数进行解析。注意,Bool、Int等函数返回的是指针类型的变量。程序最后再通过Println回显输出标志的值。

flag 定义

定义flags有两种方式:

1)flag.Xxx(),其中Xxx可以是Int、String等;返回一个相应类型的指针,如:

var ip = flag.Int("flagname", 123, "int flag for flagname")

2)flag.XxxVar(),将flag绑定到一个变量上,如:

var flagvar int
flag.IntVar(&flagvar, "flagname", 123, "int flag for flagname")

自定义flag

另外,还可以创建自定义flag,只要实现flag.Value接口即可(要求receiver是指针),这时候可以通过如下方式定义该flag:

flag.Var(&flagVal, "name", "help message for flagname")

例如,解析以英文逗号分割的字符串直接到 slice 中,我们可以定义如下 Value:

type sliceValue []string

func newSliceValue(vals []string, p *[]string) *sliceValue {
    *p = vals
    return (*sliceValue)(p)
}

func (s *sliceValue) Set(val string) error {
    *s = sliceValue(strings.Split(val, ","))
    return nil
}

func (s *sliceValue) Get() interface{} { return []string(*s) }

func (s *sliceValue) String() string { return strings.Join([]string(*s), ",") }

之后,可以这么使用:

var languages []string
flag.Var(newSliceValue([]string{}, &languages), "slice", "I like programming `languages`")

这样通过 -slice “go,php”这样的形式传递参数,languages得到的就是[go, php]。flag 包中对 Duration 这种非基本类型的支持,使用的就是类似这样的方式。

flag 常用API

直接获取命令行参数

// 获取名字为name的参数值,默认值为value,用法为usage
// 注意返回值是一个指针
// 类似的方法还有 Int(),Bool()等。
func String(name string, value string, usage string) *string

将获取的命令行参数赋给指定值

// 这种方式p作为返回值,可以传入变量的地址
// 类似的方法还有 IntVar(),BoolVar()等。
func StringVar(p *string, name string, value string, usage string)

展示使用方法

// Usage用来打印用法
var Usage = func() {
    // ...
}

获取 non-flag 参数

func (f *FlagSet) Arg(i int) string {
	if i < 0 || i >= len(f.args) {
		return ""
	}
	return f.args[i]
}

func Arg(i int) string {
	return CommandLine.Arg(i)
}

// NArg is the number of arguments remaining after flags have been processed.
func (f *FlagSet) NArg() int { return len(f.args) }

// NArg is the number of arguments remaining after flags have been processed.
func NArg() int { return len(CommandLine.args) }

Arg(i int) 和 Args() 这两个方法就是获取 non-flag 参数的。

NArg()获得 non-flag 的个数。

NFlag() 获得 FlagSet 中 actual 长度(即被设置了的参数个数)。

遍历参数

//Visit方法会遍历有输入的参数,flag.Flag可以将参数的名称、值、默认值、描述等内容取到
func (f *FlagSet) Visit(fn func(*Flag)) {
	for _, flag := range sortFlags(f.actual) {
		fn(flag)
	}
}
// 比如
flag.Visit(func(f *flag.Flag){
	fmt.Printf("参数名[%s], 参数值[%s], 默认值[%s], 描述信息[%s]\n", f.Name, f.Value, f.DefValue, f.Usage)
})
//VisitAll方法会遍历所有定义的参数(包括没有在命令行输入的),flag.Flag可以将参数的名称、值、默认值、描述等内容取到
func (f *FlagSet) VisitAll(fn func(*Flag)) {
   for _, flag := range sortFlags(f.formal) {
   	fn(flag)
   }
}

// 比如
flag.VisitAll(func(f *flag.Flag){
 fmt.Printf("参数名[%s], 参数值[%s], 默认值[%s], 描述信息[%s]\n", f.Name, f.Value, f.DefValue, f.Usage)
})

这两个函数分别用于访问 FlatSet 的 actual 和 formal 中的 Flag,而具体的访问方式由调用者决定

Visit方法,用于遍历每个有传入值的参数,Visit方法的入参是个自定义方法,用于接收和出入命令行的传入值

VisitAll方法,用于遍历所有在代码中声明过的命令行参数,VisitAll方法的入参是个自定义方法,用于接收和出入命令行的传入值

打印默认值

打印所有已定义参数的默认值(调用 VisitAll 实现),默认输出到标准错误,除非指定了 FlagSet 的 output(通过SetOutput() 设置)

func (f *FlagSet) PrintDefaults() {
	f.VisitAll(func(flag *Flag) {
		s := fmt.Sprintf("  -%s", flag.Name) // Two spaces before -; see next two comments.
		name, usage := UnquoteUsage(flag)
		if len(name) > 0 {
			s += " " + name
		}
		// Boolean flags of one ASCII letter are so common we
		// treat them specially, putting their usage on the same line.
		if len(s) <= 4 { // space, space, '-', 'x'.
			s += "\t"
		} else {
			// Four spaces before the tab triggers good alignment
			// for both 4- and 8-space tab stops.
			s += "\n    \t"
		}
		s += strings.ReplaceAll(usage, "\n", "\n    \t")

		if !isZeroValue(flag, flag.DefValue) {
			if _, ok := flag.Value.(*stringValue); ok {
				// put quotes on the value
				s += fmt.Sprintf(" (default %q)", flag.DefValue)
			} else {
				s += fmt.Sprintf(" (default %v)", flag.DefValue)
			}
		}
		fmt.Fprint(f.Output(), s, "\n")
	})
}

设置值

设置某个 flag 的值(通过 name 查找到对应的 Flag)

func (f *FlagSet) Set(name, value string) error {
	flag, ok := f.formal[name]
	if !ok {
		return fmt.Errorf("no such flag -%v", name)
	}
	err := flag.Value.Set(value)
	if err != nil {
		return err
	}
	if f.actual == nil {
		f.actual = make(map[string]*Flag)
	}
	f.actual[name] = flag
	return nil
}

解析参数

// 解析参数,应在设置完参数变量后调用
func Parse()

flag 源码

我们下面来看一下flag包的实现,核心文件是$GOROOT/src/flag/flag.go文件

参数载入

flag包的核心数据结构是FlagSet结构体

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. What happens after Usage is called depends
	// on the ErrorHandling setting; for the command line, this defaults
	// to ExitOnError, which exits the program after calling Usage.
	Usage func()

	name          string // FlagSet的名字。CommandLine 给的是 os.Args[0]
	parsed        bool // 是否执行过Parse()
	actual        map[string]*Flag // 存放实际传递了的参数(即命令行参数)
	formal        map[string]*Flag // 存放所有已定义命令行参数 
	args          []string // arguments after flags // 开始存放所有参数,最后保留 非flag(non-flag)参数
	errorHandling ErrorHandling // 当解析出错时,处理错误的方式
	output        io.Writer // nil means stderr; use Output() accessor
}

// 预定义的 FlagSet 实例 CommandLine 的定义方式,可见,默认的 FlagSet 实例在解析出错时会退出程序。
// 由于FlagSet中的字段是非导出的,其他方式获得FlagSet实例后,比如:FlagSet{} 或 new(FlagSet),
// 应该调用Init()方法初始化name和errorHandling,否则name为空,errorHandling为ContinueOnError
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

func init() {
	// Override generic FlagSet default Usage with call to global Usage.
	// Note: This is not CommandLine.Usage = Usage,
	// because we want any eventual call to use any updated value of Usage,
	// not the value it has when this line is run.
	CommandLine.Usage = commandLineUsage
}

// NewFlagSet() 用于实例化 FlagSet
func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet {
    f := &FlagSet{
        name:          name,
        errorHandling: errorHandling,
    }
    f.Usage = f.defaultUsage
    return f
}

CommandLine是FlagSet类型的全局变量,其中的关键字段描述如下:

  • Usage是一个帮助函数,在命令行标志输入不符合预期时被调用,并提示用户正确的输入方式;
  • name是程序的名称,在CommandLine被初始化时赋值为os.Args[0],也就是应用程序的名称;
  • actual和formal是两个重要的map,将命令行标志的名称映射到Flag类型的结构,该结构体定义如下:
type Flag struct {
    Name     string // name as it appears on command line
    Usage    string // help message
    Value    Value  // value as set
    DefValue string // default value (as text); for usage message
}

Flag 类型代表一个 flag 的状态,比如,对于命令:./nginx -c /etc/nginx.conf,相应代码是::

flag.StringVar(&c, "c", "conf/nginx.conf", "set configuration `file`")

则该 Flag 实例(可以通过 flag.Lookup(“c”) 获得)相应各个字段的值为:

&Flag{
    Name: c,
    Usage: set configuration file,
    Value: /etc/nginx.conf,
    DefValue: conf/nginx.conf,
}

其中Name就是标记名称,也就是命令行输入的类似-int 10中的int,Usage是在调用Int/IntVar时传入的帮助字符串,Value则是flag支持的标记值类型,其定义如下:

type Value interface {
    String() string
    Set(string) error
}

其中的String方法用于显示命令行标志的名字,Set则用于记录标志的值。记住一句话,任何实现了Value接口的类型都可以作为命令行标志的类型,下面我们以字符串类型的Value为例说明,flag包内置的字符串类型定义为:

type stringValue string
func (s *stringValue) String() string { return string(*s) }
func (s *stringValue) Set(val string) error {
    *s = stringValue(val)
    return nil
}

可以看到stringValue其实就是go内置的string类型,flag给这个类型定义了String和Set方法以实现Value接口。

那么我们在调用String/StringVar函数时,发生了什么呢?

func String(name string, value string, usage string) *string {
    return CommandLine.String(name, value, usage)
}

func (f *FlagSet) String(name string, value string, usage string) *string {
    p := new(string)
    f.StringVar(p, name, value, usage)
    return p
}

func (f *FlagSet) StringVar(p *string, name string, value string, usage string) {
    f.Var(newStringValue(value, p), name, usage)
}

// 主要是完成参数的初始化设置,即提供的默认值设置,并进行相应的类型转换
func newStringValue(val string, p *string) *stringValue {
    *p = val
    return (*stringValue)(p)
}

// Var中主要完成参数的查重,flag的封装,然后存入format中
func (f *FlagSet) Var(value Value, name string, usage string) {
    // Remember the default value as a string; it won't change.
    flag := &Flag{name, usage, value, value.String()}
    _, alreadythere := f.formal[name]
    if alreadythere { // 若已存在则panic报错
        var msg string
        if f.name == "" {
            msg = fmt.Sprintf("flag redefined: %s", name)
        } else {
            msg = fmt.Sprintf("%s flag redefined: %s", f.name, name)
        }
        fmt.Fprintln(f.out(), msg)
        panic(msg) // Happens only if flags are declared with identical names
    }
    if f.formal == nil {
        f.formal = make(map[string]*Flag)
    }
    f.formal[name] = flag
}

最终在调用到FlagSet的Var方法时,字符串类型的标志被记录到了CommandLine的formal里面了。

参数解析

好了,通过调用String、Int函数登记标志到CommandLine后,Parse函数会最终实现命令行参数的解析:

// 该方法应该在 flag 参数定义后而具体参数值被访问前调用
func Parse() {
    // Ignore errors; CommandLine is set for ExitOnError.
    CommandLine.Parse(os.Args[1:])
}

// 参数列表中解析定义的flag。方法参数arguments不包括命令名,即是os.Args[1:]
func (f *FlagSet) Parse(arguments []string) error {
    f.parsed = true
    f.args = arguments
    for {
      	// 解析是一个接一个参数完成的
        seen, err := f.parseOne()
        if seen { // 解析到对应的值,解析下一个
            continue
        }
        if err == nil { // 最终无错直接退出
            break
        }
        // 发生错误处理
        switch f.errorHandling {
        case ContinueOnError:
            return err
        case ExitOnError:
            os.Exit(2)
        case PanicOnError:
            panic(err)
        }
    }
    return nil
}

如果提供了 -help 参数(命令中给了)但没有定义(代码中没有),该方法返回 ErrHelp 错误。默认的 CommandLine,在 Parse 出错时会退出程序(ExitOnError)

Parse函数读取所有的命令行参数,即os.Args[1:],并传入FlagSet的Parse方法,后者通过parseOne方法逐个读取标志进行解析:

func (f *FlagSet) parseOne() (bool, error) {
    if len(f.args) == 0 { // 不能为空
        return false, nil
    }
    s := f.args[0]
    if len(s) == 0 || s[0] != '-' || len(s) == 1 { // key必须包含’-',且长度必须大于1
        return false, nil
    }
    numMinuses := 1
    if s[1] == '-' {
        numMinuses++
        if len(s) == 2 { // "--" terminates the flags
            f.args = f.args[1:]
            return false, nil
        }
    }
    name := s[numMinuses:]
    if len(name) == 0 || name[0] == '-' || name[0] == '=' {
        return false, f.failf("bad flag syntax: %s", s)
    }

    // it's a flag. does it have an argument?
    // 每执行成功一次 parseOne,f.args 会少一个。所以,FlagSet 中的 args 最后留下来的就是所有 non-flag 参数
    f.args = f.args[1:]
    hasValue := false
    value := ""
    for i := 1; i < len(name); i++ { // equals cannot be first
        if name[i] == '=' { // 处理中包含'='的问题,以'='为界限拆分为key、value两部分
            value = name[i+1:]
            hasValue = true
            name = name[0:i]
            break
        }
    }
    m := f.formal
    flag, alreadythere := m[name] // BUG
    if !alreadythere { // 解析到不存在的参数
        if name == "help" || name == "h" { // special case for nice help message.
            f.usage()
            return false, ErrHelp
        }
        return false, f.failf("flag provided but not defined: -%s", name)
    }

  	// 接下来是处理-flag=x这种形式,然后是-flag这种形式(bool类型)(这里对bool进行了特殊处理),
  	// 接着是-flag x这种形式,最后将解析成功的Flag实例存入FlagSet的actual map中。
  
    // bool相关问题,提供value,则设置value,不提供value,则设置为true
    if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { // special case: doesn't need an arg
        if hasValue {
            if err := fv.Set(value); err != nil {
                return false, f.failf("invalid boolean value %q for -%s: %v", value, name, err)
            }
        } else {
            if err := fv.Set("true"); err != nil {
                return false, f.failf("invalid boolean flag %s: %v", name, err)
            }
        }
    } else {
        // It must have a value, which might be the next argument.
        if !hasValue && len(f.args) > 0 {
            // value is the next arg
            // 直接取下一个值为key对应的值
            hasValue = true
            value, f.args = f.args[0], f.args[1:]
        }
        if !hasValue {
            return false, f.failf("flag needs an argument: -%s", name)
        }
        // 设置值
        if err := flag.Value.Set(value); err != nil {
            return false, f.failf("invalid value %q for flag -%s: %v", value, name, err)
        }
    }
    if f.actual == nil {
        f.actual = make(map[string]*Flag)
    }
    // 存储
    f.actual[name] = flag
    return true, nil
}

这里会调用到具体Value类型的Set方法,还记得前面String类型的Set方法吗?它将标志值写入了对应的Value内,因此String返回的指针就可以取到最终的标志值了,这里需要对Golang通过接口实现的多态机制有所了解。

更多发现

从以上源码中我们发现了:

  1. 参数以”=“传值的形式
./test -para1=value1 -para2=1 -para3=true
  1. value中包含”=“是允许的
-para1 value1=2//①
-para1=value1=2//②

①是因为代码不会检查value的

②因为代码只要处理了第一个”=“,就不再处理了,代码如下:

for i := 1; i < len(name); i++ { // equals cannot be first
        if name[i] == '=' {   //处理中包含'='的问题,以'='为界限拆分为key、value两部分
            value = name[i+1:]
            hasValue = true
            name = name[0:i]
            break
        }
}
  1. ”=“传值,则”=“右侧不能为空,但是左侧可以为空,虽然没什么实际使用价值

  2. bool类型可以不传value,对于bool类型的参数,可以不传value,不过会默认设置为true。

参考:

Go解析命令行参数(flag包)

Golang命令行参数解析:flag包的用法及源码解析

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值