一、前言
flag主要用以對命令行工具的參數的解析獲取,其源碼位於/flag/flag.go中。
二、flag使用
flag一般使用方法:
(1)聲明變量並綁定
有兩種方式:
①Var形式
var (
_para1 string
_para2 int
_para3 bool
...
)
func init{
flag.StringVar(&_para1, "para1", "", "para1")
flag.IntVar(&_para2, "para2", 0, "para2")
flag.IntVar(&_para3, "para3", false, "para3")
...
}
②不帶Var格式
var (
_para1 *string
_para2 *int
_para3 *bool
…
)
func init(
_para1=flag.String( "para1", "", "para1")
_para2=flag.Int("para2", 0, "para2")
_para3=flag.Bool("para3",false,"para3")
...
)
兩者的功能是一致的,具體原因後面說。
(2)main中Parse
func main(){
flag.Parse()
...
}
調用flag.Parse()後參數的值會解析到綁定的變量中。
(3)使用
./test -para1 value1 -para2 1 -para3 true
三、flag源碼
1.綁定
綁定有兩種形式,我們以string的處理做說明,其他格式的,可參考具體源碼。
// StringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a string variable in which to store the value of the flag.
func (f *FlagSet) StringVar(p *string, name string, value string, usage string) {
f.Var(newStringValue(value, p), name, usage)
}
// StringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a string variable in which to store the value of the flag.
func StringVar(p *string, name string, value string, usage string) {
CommandLine.Var(newStringValue(value, p), name, usage)
}
// String defines a string flag with specified name, default value, and usage string.
// The return value is the address of a string variable that stores the value of the flag.
func (f *FlagSet) String(name string, value string, usage string) *string {
p := new(string)
f.StringVar(p, name, value, usage)
return p
}
// String defines a string flag with specified name, default value, and usage string.
// The return value is the address of a string variable that stores the value of the flag.
func String(name string, value string, usage string) *string {
return CommandLine.String(name, value, usage)
}
我們可以看到,兩者調用的均爲Var
func,其他類型的參數最終也是調用Var來處理。不同之處是,String會new一個string的指針,然後用聲明的變量指針指向這個指針的值,而StringVar是直接使用我們傳入的指針。
newStringValue主要是完成參數的初始化設置,即提供的默認值設置,並進行相應的類型轉換。
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 {//若已存在則panic報錯
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.Output(), 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
}
Var
中完成flag的封裝,以name爲key存入format(map)中。若name已存在,說明重複定義。
2.解析
// Parse parses flag definitions from the argument list, which should not
// include the command name. Must be called after all flags in the FlagSet
// are defined and before flags are accessed by the program.
// The return value will be ErrHelp if -help or -h were set but not defined.
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
實現。解析完一個flag才繼續下一個,中間發生任何錯誤,不再繼續進行,報錯退出。
parseOne
// parseOne parses one flag. It reports whether a flag was seen.
func (f *FlagSet) parseOne() (bool, error) {
if len(f.args) == 0 {//不能爲空
return false, nil
}
s := f.args[0]
if len(s) < 2 || s[0] != '-' {//key必須包含’-',且長度必須大於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] == '=' { //處理中包含'='的問題,以'='爲界限拆分爲key、value兩部分
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
//bool相關問題,提供value,則設置value,不提供value,則設置爲true
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
//直接取下一個值爲key對應的值
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
}
解析就是找到key及對應的value。key與value總是成對出現的,要想找到value,首要就是先找到key,flag要求key的傳入方式是-key
,代碼就是從找-
符號開始。
大致過程如下:
1.尋找key
- key、value是成對出現的,所以要求len(s)>2
- key必須以
-
開頭,但不能以--
開頭 - 獲取
-
後的部分爲name,注意name中可能會包含value
2.處理name中包含value的情況(-key=value
)
- 以第一個
=
爲分割符,分成兩部分,前一部分爲key,後一部分爲value
3.確認解析的key對應的flag是否存在,沒有則報錯
4.對於bool類型的flag,可以不提供value,有value的話用value,沒有的話取默認值
5.其他類型的flag必須提供value,當前name中沒有value,則取參數中的下一個值
6.設置flag獲取到value
7.將flag存入map中
3.更多發現
從以上源碼中我們發現了:
(1)參數以"="
傳值的形式
./test -para1=value1 -para2=1 -para3=true
(2)value中包含"="
是允許的
-para1 value1=2//①
-para1=value1=2//②
①是因爲代碼不會檢查value的
②因爲代碼只要處理了第一個"="
,就不再處理了,代碼如下:。
for i := 1; i < len(name); i++ { // equals cannot be first
if name[i] == '=' { //處理中包含'='的問題,以'='爲界限拆分爲key、value兩部分
value = name[i+1:]
hasValue = true
name = name[0:i]
break
}
}
(3)"="
傳值,則"="
右側不能爲空
但是左側可以爲空,雖然沒什麼實際使用價值。
(4)bool類型可以不傳value
對於bool類型的參數,可以不傳value,不過會默認設置爲true。
(5)傳值方式可以混用
實際使用時,兩種形式是可以混合使用的,爲避免理解錯誤,非常不建議混用。
./test -para1 value1 -para2=value2 -para3
公衆號
鄙人剛剛開通了公衆號,專注於分享Go開發相關內容,望大家感興趣的支持一下,在此特別感謝。