GO标准库—命令行参数解析FLAG
評(píng)論有人提到?jīng)]有例子,不知道講的是什么。因此,為了大家能夠更好地理解,特意加了一個(gè)示例。其實(shí)本文更多講解的是 flag 的實(shí)現(xiàn)原理,加上示例之后,就更好地知道怎么使用了。建議閱讀 《Go語言標(biāo)準(zhǔn)庫》一書的對(duì)應(yīng)章節(jié):flag – 命令行參數(shù)解析。
在寫命令行程序(工具、server)時(shí),對(duì)命令參數(shù)進(jìn)行解析是常見的需求。各種語言一般都會(huì)提供解析命令行參數(shù)的方法或庫,以方便程序員使用。如果命令行參數(shù)純粹自己寫代碼解析,對(duì)于比較復(fù)雜的,還是挺費(fèi)勁的。在go標(biāo)準(zhǔn)庫中提供了一個(gè)包:flag,方便進(jìn)行命令行解析。
注:區(qū)分幾個(gè)概念
1)命令行參數(shù)(或參數(shù)):是指運(yùn)行程序提供的參數(shù)
2)已定義命令行參數(shù):是指程序中通過flag.Xxx等這種形式定義了的參數(shù)
3)非flag(non-flag)命令行參數(shù)(或保留的命令行參數(shù)):后文解釋
一、使用示例
我們以 nginx 為例,執(zhí)行 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` 實(shí)現(xiàn)類似 nginx 的這個(gè)輸出,創(chuàng)建文件 nginx.go,內(nèi)容如下:
package mainimport ("flag""fmt""os" )// 實(shí)際中應(yīng)該用更好的變量名 var (h boolv, V boolt, T boolq *bools stringp stringc stringg 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`。默認(rèn)是 -s string,有了 `signal` 之后,變?yōu)?-s signalflag.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")// 改變默認(rèn)的 Usageflag.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() }
在flag包中,進(jìn)行了進(jìn)一步封裝:將FlagSet的方法都重新定義了一遍,也就是提供了一序列函數(shù),而函數(shù)中只是簡(jiǎn)單的調(diào)用已經(jīng)實(shí)例化好了的FlagSet實(shí)例:commandLine 的方法,這樣commandLine實(shí)例便不需要export。這樣,使用者是這么調(diào)用:flag.Parse()而不是flag.commandLine.Parse()。(Go 1.2 版本起改為了 CommandLine)執(zhí)行:go run nginx.go -h,(或 go build -o nginx && ./nginx -h)輸出如下:
仔細(xì)理解以上例子,如果有不理解的,看完下文的講解再回過頭來看。
二、flag包概述
flag包實(shí)現(xiàn)了命令行參數(shù)的解析。
定義flags有兩種方式:
1)flag.Xxx(),其中Xxx可以是Int、String等;返回一個(gè)相應(yīng)類型的指針,如:
var ip = flag.Int("flagname", 1234,?"help message for flagname")2)flag.XxxVar(),將flag綁定到一個(gè)變量上,如:
var flagvar int flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")另外,還可以創(chuàng)建自定義flag,只要實(shí)現(xiàn)flag.Value接口即可(要求receiver是指針),這時(shí)候可以通過如下方式定義該flag:
flag.Var(&flagVal,?"name",?"help message for flagname")例如,解析我喜歡的編程語言,我們希望直接解析到 slice 中,我們可以定義如下 Value:
type sliceValue []stringfunc newSliceValue(vals []string, p *[]string) *sliceValue {*p = valsreturn (*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`")
在所有的flag定義完成之后,可以通過調(diào)用flag.Parse()進(jìn)行解析。這樣通過 -slice “go,php” 這樣的形式傳遞參數(shù),languages 得到的就是 [go, php]。
flag 中對(duì) Duration 這種非基本類型的支持,使用的就是類似這樣的方式。
命令行flag的語法有如下三種形式:
-flag // 只支持bool類型
-flag=x
-flag x // 只支持非bool類型
其中第三種形式只能用于非bool類型的flag,原因是:如果支持,那么對(duì)于這樣的命令 cmd -x *,如果有一個(gè)文件名字是:0或false等,則命令的原意會(huì)改變(之所以這樣,是因?yàn)閎ool類型支持-flag這種形式,如果bool類型不支持-flag這種形式,則bool類型可以和其他類型一樣處理。也正因?yàn)檫@樣,Parse()中,對(duì)bool類型進(jìn)行了特殊處理)。默認(rèn)的,提供了-flag,則對(duì)應(yīng)的值為true,否則為flag.Bool/BoolVar中指定的默認(rèn)值;如果希望顯示設(shè)置為false則使用-flag=false。
int類型可以是十進(jìn)制、十六進(jìn)制、八進(jìn)制甚至是負(fù)數(shù);bool類型可以是1, 0, t, f, true, false, TRUE, FALSE, True, False。Duration可以接受任何time.ParseDuration能解析的類型
三、類型和函數(shù)
在看類型和函數(shù)之前,先看一下變量
ErrHelp:該錯(cuò)誤類型用于當(dāng)命令行指定了-help參數(shù)但沒有定義時(shí)。
Usage:這是一個(gè)函數(shù),用于輸出所有定義了的命令行參數(shù)和幫助信息(usage message)。一般,當(dāng)命令行參數(shù)解析出錯(cuò)時(shí),該函數(shù)會(huì)被調(diào)用。我們可以指定自己的Usage函數(shù),即:flag.Usage = func(){}
1、函數(shù)
go標(biāo)準(zhǔn)庫中,經(jīng)常這么做:
定義了一個(gè)類型,提供了很多方法;為了方便使用,會(huì)實(shí)例化一個(gè)該類型的實(shí)例(通用),這樣便可以直接使用該實(shí)例調(diào)用方法。比如:encoding/base64中提供了StdEncoding和URLEncoding實(shí)例,使用時(shí):base64.StdEncoding.Encode()
這里不詳細(xì)介紹各個(gè)函數(shù),在類型方法中介紹。
2、類型(數(shù)據(jù)結(jié)構(gòu))
1)ErrorHandling
type ErrorHandling?int該類型定義了在參數(shù)解析出錯(cuò)時(shí)錯(cuò)誤處理方式定義了。三個(gè)該類型的常量:
const (ContinueOnError ErrorHandling = iotaExitOnErrorPanicOnError )三個(gè)常量在源碼的 FlagSet 的方法 parseOne() 中使用了。
2)Flag
// A Flag represents the state of a flag. type Flag struct {Name string // name as it appears on command lineUsage string // help messageValue Value // value as setDefValue string // default value (as text); for usage message }Flag類型代表一個(gè)flag的狀態(tài)。
比如:autogo -f abc.txt,代碼 flag.String(“f”, “a.txt”, “usage”),則該Flag實(shí)例(可以通過flag.Lookup(“f”)獲得)相應(yīng)的值為:f, usage, abc.txt, a.txt。
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 // 是否執(zhí)行過Parse()actual map[string]*Flag // 存放實(shí)際傳遞了的參數(shù)(即命令行參數(shù))formal map[string]*Flag // 存放所有已定義命令行參數(shù)args []string // arguments after flags // 開始存放所有參數(shù),最后保留 非flag(non-flag)參數(shù)exitOnError bool // does the program exit if there's an error?errorHandling ErrorHandling // 當(dāng)解析出錯(cuò)時(shí),處理錯(cuò)誤的方式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() stringSet(string) error }
四、主要類型的方法(包括類型實(shí)例化)所有參數(shù)類型需要實(shí)現(xiàn)Value接口,flag包中,為int、float、bool等實(shí)現(xiàn)了該接口。借助該接口,我們可以自定義flag
flag包中主要是FlagSet類型。
1、實(shí)例化方式
NewFlagSet()用于實(shí)例化FlagSet。預(yù)定義的FlagSet實(shí)例 CommandLine 的定義方式:
// The default set of command-line flags, parsed from os.Args. var CommandLine = NewFlagSet(os.Args[0], ExitOnError)可見,默認(rèn)的FlagSet實(shí)例在解析出錯(cuò)時(shí)會(huì)退出程序。
由于FlagSet中的字段沒有export,其他方式獲得FlagSet實(shí)例后,比如:FlagSet{}或new(FlagSet),應(yīng)該調(diào)用Init()方法,初始化name和errorHandling。
2、定義flag參數(shù)方法
這一序列的方法都有兩種形式,在一開始已經(jīng)說了兩種方式的區(qū)別。這些方法用于定義某一類型的flag參數(shù)。
3、解析參數(shù)(Parse)
func (f *FlagSet) Parse(arguments []string) error從參數(shù)列表中解析定義的flag。參數(shù)arguments不包括命令名,即應(yīng)該是os.Args[1:]。事實(shí)上,flag.Parse()函數(shù)就是這么做的:
// 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:]) }?
該方法應(yīng)該在flag參數(shù)定義后而具體參數(shù)值被訪問前調(diào)用。
如果提供了-help參數(shù)(命令中給了)但沒有定義(代碼中),該方法返回ErrHelp錯(cuò)誤。默認(rèn)的CommandLine,在Parse出錯(cuò)時(shí)會(huì)退出(ExitOnError)
為了更深入的理解,我們看一下Parse(arguments []string)的源碼:
func (f *FlagSet) Parse(arguments []string) error {f.parsed = truef.args = argumentsfor {seen, err := f.parseOne()if seen {continue}if err == nil {break}switch f.errorHandling {case ContinueOnError:return errcase ExitOnError:os.Exit(2)case PanicOnError:panic(err)}}return nil }
真正解析參數(shù)的方法是非導(dǎo)出方法 parseOne。
結(jié)合parseOne方法,我們來解釋non-flag以及包文檔中的這句話:
Flag parsing stops just before the first non-flag argument (“-” is a non-flag argument) or after the terminator “–“.
我們需要了解解析什么時(shí)候停止
根據(jù)Parse()中for循環(huán)終止的條件(不考慮解析出錯(cuò)),我們知道,當(dāng)parseOne返回false, nil時(shí),Parse解析終止。正常解析完成我們不考慮。看一下parseOne的源碼發(fā)現(xiàn),有兩處會(huì)返回false, nil。
1)第一個(gè)non-flag參數(shù)
s := f.args[0] if len(s) == 0 || s[0] != '-' || len(s) == 1 {return false, nil }?
也就是,當(dāng)遇到單獨(dú)的一個(gè)”-“或不是”-“開始時(shí),會(huì)停止解析。比如:
./autogo – -f或./autogo build -f
這兩種情況,-f都不會(huì)被正確解析。像該例子中的”-“或build(以及之后的參數(shù)),我們稱之為non-flag參數(shù)
2)兩個(gè)連續(xù)的”–“
if s[1] == '-' {num_minuses++if len(s) == 2 { // "--" terminates the flagsf.args = f.args[1:]return false, nil} }也就是,當(dāng)遇到連續(xù)的兩個(gè)”-“時(shí),解析停止。
說明:這里說的”-“和”–“,位置和”-f”這種的一樣。也就是說,下面這種情況并不是這里說的:
./autogo -f —
這里的”–“會(huì)被當(dāng)成是f的值
parseOne方法中接下來是處理-flag=x,然后是-flag(bool類型)(這里對(duì)bool進(jìn)行了特殊處理),接著是-flag x這種形式,最后,將解析成功的Flag實(shí)例存入FlagSet的actual map中。
另外,在parseOne中有這么一句:
f.args = f.args[1:]也就是說,每執(zhí)行成功一次parseOne,f.args會(huì)少一個(gè)。所以,FlagSet中的args最后留下來的就是所有non-flag參數(shù)。
4、Arg(i int)和Args()、NArg()、NFlag()
Arg(i int)和Args()這兩個(gè)方法就是獲取non-flag參數(shù)的;NArg()獲得non-flag個(gè)數(shù);NFlag()獲得FlagSet中actual長(zhǎng)度(即被設(shè)置了的參數(shù)個(gè)數(shù))。
5、Visit/VisitAll
這兩個(gè)函數(shù)分別用于訪問FlatSet的actual和formal中的Flag,而具體的訪問方式由調(diào)用者決定。
6、PrintDefaults()
打印所有已定義參數(shù)的默認(rèn)值(調(diào)用VisitAll),默認(rèn)輸出到標(biāo)準(zhǔn)錯(cuò)誤,除非指定了FlagSet的output(通過SetOutput()設(shè)置)
7、Set(name, value string)
設(shè)置某個(gè)flag的值(通過Flag的name)
五、總結(jié)
使用建議:雖然上面講了那么多,一般來說,我們只簡(jiǎn)單的定義flag,然后parse,就如同開始的例子一樣。
如果項(xiàng)目需要復(fù)雜或更高級(jí)的命令行解析方式,可以試試goptions包
如果想要像go工具那樣的多命令(子命令)處理方式,可以試試command
?
更新:
2017-10-14:復(fù)雜的命令解析,推薦?https://github.com/urfave/cli?或者?https://github.com/spf13/cobra
總結(jié)
以上是生活随笔為你收集整理的GO标准库—命令行参数解析FLAG的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 再说 Spring AOP
- 下一篇: maven国内镜像(maven下载慢的解