在写命令行程序(工具、server)时,对命令行参数进行解析,是一种常见的需求。各种语言一般都会提供相应的方法或库,以方便开发者使用。在go标准库中提供了一个包:flag,方便进行命令行解析。也就是说,Go的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回显输出标志的值。
定义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.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
这种非基本类型的支持,使用的就是类似这样的方式。
// 获取名字为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() {
// ...
}
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包的实现,核心文件是$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类型的全局变量,其中的关键字段描述如下:
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通过接口实现的多态机制有所了解。
从以上源码中我们发现了:
./test -para1=value1 -para2=1 -para3=true
-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
}
}
”=“传值,则”=“右侧不能为空,但是左侧可以为空,虽然没什么实际使用价值
bool类型可以不传value,对于bool类型的参数,可以不传value,不过会默认设置为true。
参考:
Go解析命令行参数(flag包)
Golang命令行参数解析:flag包的用法及源码解析