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

1 命令行参数的定义

命令行参数用于向应用程序传递一些定制参数,使得程序的功能更加丰富和多样化。命令行标志是一类特殊的命令行参数,通常以减号(-)或双减号(–)连接标志名称,非bool类型的标志后面还会有取值。以git log命令为例,例如我们要观察最近的10条commit记录,且要显示每条记录修改的文件信息:

git log --stat -n 10

其中的--stat-n 10就是两个标志,前者是bool类型,后者是int类型。--stat告诉git log输出每条commit记录的统计信息,它的取值只有True或False,因此标志后不需要其它参数值。而-n 10则需要通过后面的数值10告诉git log命令我们需要显示的commit记录条数。非bool类型的标志值还可以通过等号的形式提供,比如下面这条命令也用于显示最近的10条commit记录:

git log --stat --max-count=10

2 Golang命令行参数解析

Golang的命令行参数解析使用的是flag包,支持布尔、整型、字符串,以及时间格式的标识解析。下面我们以一个echoflag程序为例,演示flag包的用法。这个程序接收来自命令行的输入,并回显命令行标识的值,程序的执行效果如下:

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

我们来看一下程序的实现:

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)
}

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

可以看到,Golang的命令行参数解析非常简单,标志的解析使用flag包,其它非标记类的参数可以通过os.Args获取,并进行解析。

3 Flag包源码解析

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

3.1 数据结构

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

type FlagSet struct {
    Usage func()

    name          string
    parsed        bool
    actual        map[string]*Flag
    formal        map[string]*Flag
    args          []string // arguments after flags
    errorHandling ErrorHandling
    output        io.Writer // nil means stderr; use out() accessor
}

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

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
}

其中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)
}
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 {
        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里面了。

3.2 参数解析实现

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

func Parse() {
    // Ignore errors; CommandLine is set for ExitOnError.
    CommandLine.Parse(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
}

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 {
        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] == '=' {
            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
        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
            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通过接口实现的多态机制有所了解,如果不熟悉,可以看这里。

你可能感兴趣的:(let-us-go)