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