1 命令行参数的定义

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

git log --stat -n 10
--stat-n 10--stat-n 10
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
-string

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

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包源码解析
$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
}
-int 10
type Value interface {
    String() string
    Set(string) error
}
任何实现了Value接口的类型都可以作为命令行标志的类型
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通过接口实现的多态机制有所了解,如果不熟悉,可以看这里。