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通過接口實現的多態機制有所瞭解,如果不熟悉,可以看這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章