命令行参数用于向应用程序传递一些定制参数,使得程序的功能更加丰富和多样化。命令行标志是一类特殊的命令行参数,通常以减号(-)或双减号(–)连接标志名称,非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
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获取,并进行解析。
我们下面来看一下flag包的实现,核心文件是$GOROOT/src/flag/flag.go
文件。
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里面了。
好了,通过调用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通过接口实现的多态机制有所了解,如果不熟悉,可以看这里。