一、前言
flag主要用以对命令行工具的参数的解析获取,其源码位于/flag/flag.go中。
二、flag使用
flag一般使用方法:
(1)声明变量并绑定
有两种方式:
①Var形式
var (
_para1 string
_para2 int
_para3 bool
...
)
func init{
flag.StringVar(&_para1, "para1", "", "para1")
flag.IntVar(&_para2, "para2", 0, "para2")
flag.IntVar(&_para3, "para3", false, "para3")
...
}
②不带Var格式
var (
_para1 *string
_para2 *int
_para3 *bool
…
)
func init(
_para1=flag.String( "para1", "", "para1")
_para2=flag.Int("para2", 0, "para2")
_para3=flag.Bool("para3",false,"para3")
...
)
两者的功能是一致的,具体原因后面说。
(2)main中Parse
func main(){
flag.Parse()
...
}
调用flag.Parse()后参数的值会解析到绑定的变量中。
(3)使用
./test -para1 value1 -para2 1 -para3 true
三、flag源码
1.绑定
绑定有两种形式,我们以string的处理做说明,其他格式的,可参考具体源码。
// StringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a string variable in which to store the value of the flag.
func (f *FlagSet) StringVar(p *string, name string, value string, usage string) {
f.Var(newStringValue(value, p), name, usage)
}
// StringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a string variable in which to store the value of the flag.
func StringVar(p *string, name string, value string, usage string) {
CommandLine.Var(newStringValue(value, p), name, usage)
}
// String defines a string flag with specified name, default value, and usage string.
// The return value is the address of a string variable that stores the value of the flag.
func (f *FlagSet) String(name string, value string, usage string) *string {
p := new(string)
f.StringVar(p, name, value, usage)
return p
}
// String defines a string flag with specified name, default value, and usage string.
// The return value is the address of a string variable that stores the value of the flag.
func String(name string, value string, usage string) *string {
return CommandLine.String(name, value, usage)
}
我们可以看到,两者调用的均为Var
func,其他类型的参数最终也是调用Var来处理。不同之处是,String会new一个string的指针,然后用声明的变量指针指向这个指针的值,而StringVar是直接使用我们传入的指针。
newStringValue主要是完成参数的初始化设置,即提供的默认值设置,并进行相应的类型转换。
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.Output(), 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
}
Var
中完成flag的封装,以name为key存入format(map)中。若name已存在,说明重复定义。
2.解析
// Parse parses flag definitions from the argument list, which should not
// include the command name. Must be called after all flags in the FlagSet
// are defined and before flags are accessed by the program.
// The return value will be ErrHelp if -help or -h were set but not defined.
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
}
解析是一个接一个参数完成的,具体由parseOne
实现。解析完一个flag才继续下一个,中间发生任何错误,不再继续进行,报错退出。
parseOne
// parseOne parses one flag. It reports whether a flag was seen.
func (f *FlagSet) parseOne() (bool, error) {
if len(f.args) == 0 {//不能为空
return false, nil
}
s := f.args[0]
if len(s) < 2 || s[0] != '-' {//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?
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)
}
if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { // special case: doesn't need an arg
//bool相关问题,提供value,则设置value,不提供value,则设置为true
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
}
解析就是找到key及对应的value。key与value总是成对出现的,要想找到value,首要就是先找到key,flag要求key的传入方式是-key
,代码就是从找-
符号开始。
大致过程如下:
1.寻找key
- key、value是成对出现的,所以要求len(s)>2
- key必须以
-
开头,但不能以--
开头 - 获取
-
后的部分为name,注意name中可能会包含value
2.处理name中包含value的情况(-key=value
)
- 以第一个
=
为分割符,分成两部分,前一部分为key,后一部分为value
3.确认解析的key对应的flag是否存在,没有则报错
4.对于bool类型的flag,可以不提供value,有value的话用value,没有的话取默认值
5.其他类型的flag必须提供value,当前name中没有value,则取参数中的下一个值
6.设置flag获取到value
7.将flag存入map中
3.更多发现
从以上源码中我们发现了:
(1)参数以"="
传值的形式
./test -para1=value1 -para2=1 -para3=true
(2)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
}
}
(3)"="
传值,则"="
右侧不能为空
但是左侧可以为空,虽然没什么实际使用价值。
(4)bool类型可以不传value
对于bool类型的参数,可以不传value,不过会默认设置为true。
(5)传值方式可以混用
实际使用时,两种形式是可以混合使用的,为避免理解错误,非常不建议混用。
./test -para1 value1 -para2=value2 -para3
公众号
鄙人刚刚开通了公众号,专注于分享Go开发相关内容,望大家感兴趣的支持一下,在此特别感谢。