flag - 命令行參數解析
在寫命令行程序(工具、server)時,對命令參數進行解析是常見的需求。各種語言一般都會提供解析命令行參數的方法或庫,以方便程序員使用。如果命令行參數純粹自己寫代碼解析,對於比較復雜的,還是挺費勁的。在 go 標准庫中提供了一個包:flag
,方便進行命令行解析。
注:區分幾個概念
- 命令行參數(或參數):是指運行程序提供的參數
- 已定義命令行參數:是指程序中通過flag.Xxx等這種形式定義了的參數
- 非flag(non-flag)命令行參數(或保留的命令行參數):后文解釋
1.1. 使用示例
我們以 nginx 為例,執行 nginx -h,輸出如下:
nginx version: nginx/1.10.0 Usage: nginx [-?hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives] Options: -?,-h : this help -v : show version and exit -V : show version and configure options then exit -t : test configuration and exit -T : test configuration, dump it and exit -q : suppress non-error messages during configuration testing -s signal : send signal to a master process: stop, quit, reopen, reload -p prefix : set prefix path (default: /usr/local/nginx/) -c filename : set configuration file (default: conf/nginx.conf) -g directives : set global directives out of configuration file
我們通過 flag
實現類似 nginx 的這個輸出,創建文件 nginx.go,內容如下:
package main import ( "flag" "fmt" "os" ) // 實際中應該用更好的變量名 var ( h bool v, V bool t, T bool q *bool s string p string c string g string ) func init() { flag.BoolVar(&h, "h", false, "this help") flag.BoolVar(&v, "v", false, "show version and exit") flag.BoolVar(&V, "V", false, "show version and configure options then exit") flag.BoolVar(&t, "t", false, "test configuration and exit") flag.BoolVar(&T, "T", false, "test configuration, dump it and exit") // 另一種綁定方式 q = flag.Bool("q", false, "suppress non-error messages during configuration testing") // 注意 `signal`。默認是 -s string,有了 `signal` 之后,變為 -s signal flag.StringVar(&s, "s", "", "send `signal` to a master process: stop, quit, reopen, reload") flag.StringVar(&p, "p", "/usr/local/nginx/", "set `prefix` path") flag.StringVar(&c, "c", "conf/nginx.conf", "set configuration `file`") flag.StringVar(&g, "g", "conf/nginx.conf", "set global `directives` out of configuration file") // 改變默認的 Usage flag.Usage = usage } func main() { flag.Parse() if h { flag.Usage() } } func usage() { fmt.Fprintf(os.Stderr, `nginx version: nginx/1.10.0 Usage: nginx [-hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives] Options: `) flag.PrintDefaults() }
執行:go run nginx.go -h,(或 go build -o nginx && ./nginx -h)輸出如下:
nginx version: nginx/1.10.0 Usage: nginx [-hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives] Options: -T test configuration, dump it and exit -V show version and configure options then exit -c file set configuration file (default "conf/nginx.conf") -g directives set global directives out of configuration file (default "conf/nginx.conf") -h this help -p prefix set prefix path (default "/usr/local/nginx/") -q suppress non-error messages during configuration testing -s signal send signal to a master process: stop, quit, reopen, reload -t test configuration and exit -v show version and exit
仔細理解以上例子,如果有不理解的,看完下文的講解再回過頭來看。
1.2. flag 包概述
flag
包實現了命令行參數的解析。
1.2.1. 定義 flags 有兩種方式
1)flag.Xxx(),其中 Xxx
可以是 Int、String 等;返回一個相應類型的指針,如:
var ip = flag.Int("flagname", 1234, "help message for flagname")
2)flag.XxxVar(),將 flag 綁定到一個變量上,如:
var flagvar int flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
1.2.2. 自定義 Value
另外,還可以創建自定義 flag,只要實現 flag.Value 接口即可(要求 receiver
是指針),這時候可以通過如下方式定義該 flag:
flag.Var(&flagVal, "name", "help message for flagname")
例如,解析我喜歡的編程語言,我們希望直接解析到 slice 中,我們可以定義如下 Value:
type sliceValue []string func newSliceValue(vals []string, p *[]string) *sliceValue { *p = vals return (*sliceValue)(p) } func (s *sliceValue) Set(val string) error { *s = sliceValue(strings.Split(val, ",")) return nil } func (s *sliceValue) Get() interface{} { return []string(*s) } func (s *sliceValue) String() string { return strings.Join([]string(*s), ",") }
之后可以這么使用:
var languages []string flag.Var(newSliceValue([]string{}, &languages), "slice", "I like programming `languages`")
這樣通過 -slice "go,php"
這樣的形式傳遞參數,languages
得到的就是 [go, php]
。
flag 中對 Duration 這種非基本類型的支持,使用的就是類似這樣的方式。
1.2.3. 解析 flag
在所有的 flag 定義完成之后,可以通過調用 flag.Parse()
進行解析。
命令行 flag 的語法有如下三種形式:
-flag // 只支持bool類型 -flag=x -flag x // 只支持非bool類型
其中第三種形式只能用於非 bool 類型的 flag,原因是:如果支持,那么對於這樣的命令 cmd -x *,如果有一個文件名字是:0或false等,則命令的原意會改變(之所以這樣,是因為 bool 類型支持 -flag
這種形式,如果 bool 類型不支持 -flag
這種形式,則 bool 類型可以和其他類型一樣處理。也正因為這樣,Parse()中,對 bool 類型進行了特殊處理)。默認的,提供了 -flag
,則對應的值為 true,否則為 flag.Bool/BoolVar
中指定的默認值;如果希望顯示設置為 false 則使用 -flag=false
。
int 類型可以是十進制、十六進制、八進制甚至是負數;bool 類型可以是1, 0, t, f, true, false, TRUE, FALSE, True, False。Duration 可以接受任何 time.ParseDuration 能解析的類型。
1.3. 類型和函數
在看類型和函數之前,先看一下變量。
ErrHelp:該錯誤類型用於當命令行指定了 ·-help` 參數但沒有定義時。
Usage:這是一個函數,用於輸出所有定義了的命令行參數和幫助信息(usage message)。一般,當命令行參數解析出錯時,該函數會被調用。我們可以指定自己的 Usage 函數,即:flag.Usage = func(){}
1.3.1. 函數
go標准庫中,經常這么做:
定義了一個類型,提供了很多方法;為了方便使用,會實例化一個該類型的實例(通用),這樣便可以直接使用該實例調用方法。比如:encoding/base64 中提供了 StdEncoding 和 URLEncoding 實例,使用時:base64.StdEncoding.Encode()
在 flag 包使用了有類似的方法,比如 CommandLine 實例,只不過 flag 進行了進一步封裝:將 FlagSet 的方法都重新定義了一遍,也就是提供了一序列函數,而函數中只是簡單的調用已經實例化好了的 FlagSet 實例:CommandLine 的方法。這樣,使用者是這么調用:flag.Parse() 而不是 flag. CommandLine.Parse()。(Go 1.2 起,將 CommandLine 導出,之前是非導出的)
這里不詳細介紹各個函數,在類型方法中介紹。
1.3.2. 類型(數據結構)
1)ErrorHandling
type ErrorHandling int
該類型定義了在參數解析出錯時錯誤處理方式。定義了三個該類型的常量:
const ( ContinueOnError ErrorHandling = iota ExitOnError PanicOnError )
三個常量在源碼的 FlagSet 的方法 parseOne() 中使用了。
2)Flag
// A Flag represents the state of a 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 }
Flag 類型代表一個 flag 的狀態。
比如,對於命令:./nginx -c /etc/nginx.conf
,相應代碼是:
flag.StringVar(&c, "c", "conf/nginx.conf", "set configuration `file`")
則該 Flag 實例(可以通過 flag.Lookup("c")
獲得)相應各個字段的值為:
&Flag{ Name: c, Usage: set configuration file, Value: /etc/nginx.conf, DefValue: conf/nginx.conf, }
3)FlagSet
// A FlagSet represents a set of defined flags. type FlagSet struct { // Usage is the function called when an error occurs while parsing flags. // The field is a function (not a method) that may be changed to point to // a custom error handler. Usage func() name string // FlagSet的名字。CommandLine 給的是 os.Args[0] parsed bool // 是否執行過Parse() actual map[string]*Flag // 存放實際傳遞了的參數(即命令行參數) formal map[string]*Flag // 存放所有已定義命令行參數 args []string // arguments after flags // 開始存放所有參數,最后保留 非flag(non-flag)參數 exitOnError bool // does the program exit if there's an error? errorHandling ErrorHandling // 當解析出錯時,處理錯誤的方式 output io.Writer // nil means stderr; use out() accessor }
4)Value 接口
// Value is the interface to the dynamic value stored in a flag. // (The default value is represented as a string.) type Value interface { String() string Set(string) error }
所有參數類型需要實現 Value 接口,flag 包中,為int、float、bool等實現了該接口。借助該接口,我們可以自定義flag。(上文已經給了具體的例子)
1.4. 主要類型的方法(包括類型實例化)
flag 包中主要是 FlagSet 類型。
1.4.1. 實例化方式
NewFlagSet()
用於實例化 FlagSet。預定義的 FlagSet 實例 CommandLine
的定義方式:
// The default set of command-line flags, parsed from os.Args. var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
可見,默認的 FlagSet 實例在解析出錯時會退出程序。
由於 FlagSet 中的字段沒有 export,其他方式獲得 FlagSet實例后,比如:FlagSet{} 或 new(FlagSet),應該調用Init() 方法,以初始化 name 和 errorHandling,否則 name 為空,errorHandling 為 ContinueOnError。
1.4.2. 定義 flag 參數的方法
這一序列的方法都有兩種形式,在一開始已經說了兩種方式的區別。這些方法用於定義某一類型的 flag 參數。
1.4.3. 解析參數(Parse)
func (f *FlagSet) Parse(arguments []string) error 從參數列表中解析定義的 flag。方法參數 arguments 不包括命令名,即應該是os.Args[1:]。事實
從參數列表中解析定義的 flag。方法參數 arguments 不包括命令名,即應該是os.Args[1:]。事實上,flag.Parse()
函數就是這么做的:
// Parse parses the command-line flags from os.Args[1:]. Must be called // after all flags are defined and before flags are accessed by the program. func Parse() { // Ignore errors; CommandLine is set for ExitOnError. CommandLine.Parse(os.Args[1:]) }
該方法應該在 flag 參數定義后而具體參數值被訪問前調用。
如果提供了 -help
參數(命令中給了)但沒有定義(代碼中沒有),該方法返回 ErrHelp
錯誤。默認的 CommandLine,在 Parse 出錯時會退出程序(ExitOnError)。
為了更深入的理解,我們看一下 Parse(arguments []string)
的源碼:
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 }
真正解析參數的方法是非導出方法 parseOne
。
結合 parseOne
方法,我們來解釋 non-flag
以及包文檔中的這句話:
Flag parsing stops just before the first non-flag argument ("-" is a non-flag argument) or after the terminator "--".
我們需要了解解析什么時候停止。
根據 Parse() 中 for 循環終止的條件(不考慮解析出錯),我們知道,當 parseOne 返回 false, nil
時,Parse 解析終止。正常解析完成我們不考慮。看一下 parseOne 的源碼發現,有兩處會返回 false, nil
。
1)第一個 non-flag 參數
s := f.args[0] if len(s) == 0 || s[0] != '-' || len(s) == 1 { return false, nil }
也就是,當遇到單獨的一個"-"或不是"-"開始時,會停止解析。比如:
./nginx - -c 或 ./nginx build -c
這兩種情況,-c
都不會被正確解析。像該例子中的"-"或build(以及之后的參數),我們稱之為 non-flag
參數。
2)兩個連續的"--"
if s[1] == '-' { num_minuses++ if len(s) == 2 { // "--" terminates the flags f.args = f.args[1:] return false, nil } }
也就是,當遇到連續的兩個"-"時,解析停止。
說明:這里說的"-"和"--",位置和"-c"這種的一樣。也就是說,下面這種情況並不是這里說的:
./nginx -c --
這里的"--"會被當成是 c
的值
parseOne 方法中接下來是處理 -flag=x
這種形式,然后是 -flag
這種形式(bool類型)(這里對bool進行了特殊處理),接着是 -flag x
這種形式,最后,將解析成功的 Flag 實例存入 FlagSet 的 actual map 中。
另外,在 parseOne 中有這么一句:
f.args = f.args[1:]
也就是說,每執行成功一次 parseOne,f.args 會少一個。所以,FlagSet 中的 args 最后留下來的就是所有 non-flag
參數。
1.4.4. Arg(i int) 和 Args()、NArg()、NFlag()
Arg(i int) 和 Args() 這兩個方法就是獲取 non-flag
參數的;NArg()獲得 non-flag
的個數;NFlag() 獲得 FlagSet 中 actual 長度(即被設置了的參數個數)。
1.4.5. Visit/VisitAll
這兩個函數分別用於訪問 FlatSet 的 actual 和 formal 中的 Flag,而具體的訪問方式由調用者決定。
1.4.6. PrintDefaults()
打印所有已定義參數的默認值(調用 VisitAll 實現),默認輸出到標准錯誤,除非指定了 FlagSet 的 output(通過SetOutput() 設置)
1.4.7. Set(name, value string)
設置某個 flag 的值(通過 name 查找到對應的 Flag)
1.5. 總結
使用建議:雖然上面講了那么多,一般來說,我們只簡單的定義flag,然后 parse,就如同開始的例子一樣。
如果項目需要復雜或更高級的命令行解析方式,可以使用 https://github.com/urfave/cli 或者 https://github.com/spf13/cobra 這兩個強大的庫。