Go Lib flag

2020-10-19

flag 是官方提供的命令行参数解析的工具包.

基本用法

package main

import (
	"flag"
	"fmt"
)

var name = flag.String("name", "Default Name", "Name setting")

func main() {
	flag.Parse()
	fmt.Printf("Hello %s\n", *name)
}
// go run main.go -name=John
// go run main.go -name="John smith"

自定义解析器

package main

import (
	"flag"
	"fmt"
	"github.com/spf13/cast"
	"strings"
)

type User struct {
	Name string
	Age  int
}

func (u *User) String() string {
	return fmt.Sprintf("Name:%s, Age:%d\n", u.Name, u.Age)
}

// Set by Bob:12
func (u *User) Set(value string) error {
	splits := strings.Split(value, ":")
	if len(splits) == 2 {
		u.Name = splits[0]
		u.Age = cast.ToInt(splits[1])
		return nil
	}
	return fmt.Errorf("format error")
}

func FlagUser(name string, value string, usage string) *User {
	u := User{}
	_ = u.Set(value) // init default value
	flag.Var(&u, name, usage)
	return &u
}

var u = FlagUser("user", "Default Name:100", "parse User")

func main() {
	flag.Parse()
	fmt.Printf("%T %s\n", u, u)
}
// go run main.go -user=Bob:12

核心方法是 func (f *FlagSet) Var(value Value, name string, usage string), Value 的接口如下所示:

type Value interface {
	String() string
	Set(string) error
}

对于解析参数来说, 我们用不到 String(), 但是在语法上要实现这个接口就要实现 String() 方法.

还需要注意实现 String() 的时候不要直接return fmt.Sprintln(u), 否则会导致方法无限递归而 stack overflow.

FlagUser 参照了 flag 自带方法的风格, 参数依次为 flag值, 默认值, 帮助提示, 最后返回一个自定义类型的指针.

源码解析

在开始读源码之前, 我们先看一下 os.Args 的解析规则, 它会得到一个字符串 slice.

package main

import (
	"fmt"
	"os"
)

func main() {
	for _, s := range os.Args {
		fmt.Println(s)
	}
}
$ go run main.go  --s -s hello -s=hi --s="hello world"

output:

/var/folders/jd/tgbfgqzn3z1c_qmcd0qcmcx80000gn/T/go-build687856139/b001/exe/main
--s
-s
hello
-s=hi
--s=hello world

规则要点如下:

  • 每个元素都是字符串, 大小写敏感;
  • 第一个元素为该程序的绝对路径;
  • 后面的元素依次为空格分隔的参数, 连续空格视作一个分隔;
  • 引号内的空格不做分隔, 自动脱去引号.

flag 在以上规则的基础上, 衍生出以下规则:

  • 参数由 ---开始, 用等号或空格分隔键值, --s=1, -s=1, --s 1, -s 1 等效;
  • -s -S 为不同的标签;
  • 对于布尔型的标签, 可以略去值来表示标签为 true: -b, --b;
  • -- 后的参数将被忽略;
  • 不允许重复注册标签.

核心结构体

像 flag 这样先注册再使用的模式(类似路由匹配), 一般会在注册时解析规则, 然后把规则保存到一个集合里, 在使用的时候去集合里匹配规则.

FlagSet 就是这样的一个集合, Flag 是其中的每个元素.

type Flag struct {
	Name     string // 标签名: `-s` 或 `--s` 中 的 `s`
	Usage    string // 用途, 帮助信息.
	Value    Value  // Value 接口: `Set(v string) error` && `String() string`
	DefValue string // 标签的默认值, 为打印帮助信息所用.
}
type FlagSet struct {
	Usage func()	// 帮助信息的打印函数, 默认为 `defaultUsage()`

	name          string // 当前程序的绝对路径
	parsed        bool // 标记已经执行过解析: `flag.Parse()`
	actual        map[string]*Flag // 由 `os.Args` 解析得到的标签
	formal        map[string]*Flag // 之前注册的标签
	args          []string // 命令行参数列表 (除去程序的绝对路径)
	errorHandling ErrorHandling // 异常处理的方式: 返回 error / 程序 exit / panic
	output        io.Writer // 打印目的地, 默认为 `os.Stderr`
}

注册标签

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

CommandLine 是 package 级别的, 只会运行一次 NewFlagSet(), 得到的结果指向一个未完全初始化的 FlagSet (map 和 slice 现在还是空).

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.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 是注册的核心方法. 经过几层代理, 默认值已经设置到 value 中, name 是标签名, usage 是帮助信息, 他们被包装到 Flag 结构体内, 保存到 FlagSetformal 里.

formal 是个 map, 标签名作为 map 的键, 不允许重复注册, 否则抛 panic .

解析标签

func (f *FlagSet) parseOne() (bool, error) {
	//...
	flag.Value.Set(value)
	//...
}

Parse() 方法内循环调用 parseOne(), 每成功解析完一对参数, FlagSetargs 就被截断一截, 直到解析结束, args 也被截空了.

一旦解析出错, 就按照 FlagSeterrorHandling 的规则处理, 默认是 exit(2) 退出程序.

成功解析的参数经由 flag.Value.Set(value) 被设置为对应的值.