評論有人提到沒有例子,不知道講的是什麼。因此,為了大家能夠更好地理解,特意加了一個樣本。其實本文更多講解的是 flag 的實現原理,加上樣本之後,就更好地知道怎麼使用了。建議閱讀 《Go語言標準庫》一書的對應章節:flag – 命令列參數解析。
在寫命令列程式(工具、server)時,對命令參數進行解析是常見的需求。各種語言一般都會提供解析命令列參數的方法或庫,以方便程式員使用。如果命令列參數純粹自己寫代碼解析,對於比較複雜的,還是挺費勁的。在go標準庫中提供了一個包:flag,方便進行命令列解析。
註:區分幾個概念
1)命令列參數(或參數):是指運行程式提供的參數
2)已定義命令列參數:是指程式中通過flag.Xxx等這種形式定義了的參數
3)非flag(non-flag)命令列參數(或保留的命令列參數):後文解釋
一、使用樣本
我們以 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
仔細理解以上例子,如果有不理解的,看完下文的講解再回過頭來看。
二、flag包概述
flag包實現了命令列參數的解析。
定義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")
另外,還可以建立自訂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 這種非基本類型的支援,使用的就是類似這樣的方式。
在所有的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能解析的類型
三、類型和函數
在看類型和函數之前,先看一下變數
ErrHelp:該錯誤類型用於當命令列指定了-help參數但沒有定義時。
Usage:這是一個函數,用於輸出所有定義了的命令列參數和協助資訊(usage message)。一般,當命令列參數解析出錯時,該函數會被調用。我們可以指定自己的Usage函數,即:flag.Usage = func(){}
1、函數
go標準庫中,經常這麼做:
定義了一個類型,提供了很多方法;為了方便使用,會執行個體化一個該類型的執行個體(通用),這樣便可以直接使用該執行個體調用方法。比如:encoding/base64中提供了StdEncoding和URLEncoding執行個體,使用時:base64.StdEncoding.Encode()
在flag包中,進行了進一步封裝:將FlagSet的方法都重新定義了一遍,也就是提供了一序列函數,而函數中只是簡單的調用已經執行個體化好了的FlagSet執行個體:commandLine 的方法,這樣commandLine執行個體便不需要export。這樣,使用者是這麼調用:flag.Parse()而不是flag.commandLine.Parse()。(Go 1.2 版本起改為了 CommandLine)
這裡不詳細介紹各個函數,在類型方法中介紹。
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的狀態。
比如:autogo -f abc.txt,代碼 flag.String(“f”, “a.txt”, “usage”),則該Flag執行個體(可以通過flag.Lookup(“f”)獲得)相應的值為: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 // 是否執行過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
四、主要類型的方法(包括類型執行個體化)
flag包中主要是FlagSet類型。
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。
2、定義flag參數方法
這一序列的方法都有兩種形式,在一開始已經說了兩種方式的區別。這些方法用於定義某一類型的flag參數。
3、解析參數(Parse)
func (f *FlagSet) Parse(arguments []string) error
從參數列表中解析定義的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
}
也就是,當遇到單獨的一個”-“或不是”-“開始時,會停止解析。比如:
./autogo – -f或./autogo build -f
這兩種情況,-f都不會被正確解析。像該例子中的”-“或build(以及之後的參數),我們稱之為non-flag參數
2)兩個連續的”–“
if s[1] == '-' {
num_minuses++
if len(s) == 2 { // "--" terminates the flags
f.args = f.args[1:]
return false, nil
}
}
也就是,當遇到連續的兩個”-“時,解析停止。
說明:這裡說的”-“和”–“,位置和”-f”這種的一樣。也就是說,下面這種情況並不是這裡說的:
./autogo -f —
這裡的”–“會被當成是f的值
parseOne方法中接下來是處理-flag=x,然後是-flag(bool類型)(這裡對bool進行了特殊處理),接著是-flag x這種形式,最後,將解析成功的Flag執行個體存入FlagSet的actual map中。
另外,在parseOne中有這麼一句:
1 f.args = f.args[1:]
也就是說,每執行成功一次parseOne,f.args會少一個。所以,FlagSet中的args最後留下來的就是所有non-flag參數。
4、Arg(i int)和Args()、NArg()、NFlag()
Arg(i int)和Args()這兩個方法就是擷取non-flag參數的;NArg()獲得non-flag個數;NFlag()獲得FlagSet中actual長度(即被設定了的參數個數)。
5、Visit/VisitAll
這兩個函數分別用於訪問FlatSet的actual和formal中的Flag,而具體的訪問方式由調用者決定。
6、PrintDefaults()
列印所有已定義參數的預設值(調用VisitAll),預設輸出到標準錯誤,除非指定了FlagSet的output(通過SetOutput()設定)
7、Set(name, value string)
設定某個flag的值(通過Flag的name)
五、總結
使用建議:雖然上面講了那麼多,一般來說,我們只簡單的定義flag,然後parse,就如同開始的例子一樣。
如果項目需要複雜或更進階的命令列解析方式,可以試試goptions包
如果想要像go工具那樣的多命令(子命令)處理方式,可以試試command
更新:
2017-10-14:複雜的命令解析,推薦 https://github.com/urfave/cli 或者 https://github.com/spf13/cobra