天天看點

Go 指令行解析 flag 包之擴充新類型擴充目标實作思路邏輯梳理實作新類型使用 StringEnumValue

上篇文章 說到,除布爾類型

Flag

,flag 支援的還有整型(int、int64、uint、uint64)、浮點型(float64)、字元串(string)和時長(duration)。

flag 内置支援能滿足大部分的需求,但某些場景,需要自定義解析規則。一個優秀的庫肯定要支援擴充的。本文将介紹如何為 flag 擴充一個新的類型支援?

擴充目标

gvg

這個小工具中,

list

子指令支援擷取 Go 的版本清單。但版本的資訊來源有多處,比如

installed

(已安裝)、

local

(本地倉庫)和

remote

(遠端倉庫)。

檢視下

list

的幫助資訊,如下:

NAME:
   gvg list - list go versions

USAGE:
   gvg list [command options] [arguments...]

OPTIONS:
   --origin value  the origin of version information , such as installed, local, remote (default: "installed")
複制代碼           

複制

可以看出,

list

子指令支援一個

Flag

選項,

--origin

。它用于指定版本資訊的來源,允許值的範圍是

installed

local

remote

如果要求不嚴格,用

StringVar

也可以實作。但問題是,使用

String

,即使輸入不在指定範圍也能成功解析,不夠嚴謹。雖說在擷取後也可以檢查,但還是不夠靈活、可配置型也差。

接下來,我們要實作一個新的類型的

Flag

,使選項的值必需在指定範圍,否則要給出一定的錯誤提示資訊。

實作思路

如何展一個新類型呢?

可以參考 flag 包内置類型的實作思路,比如

flag.DurationVar

Duration

不是基礎類型,解析結果是存放到了

time.Duration

類型中,可能更有參考價值。

進入到

flag.DurationVar

檢視源碼,如下:

func DurationVar(p *time.Duration, name string, value time.Duration, usage string) {
	CommandLine.Var(newDurationValue(value, p), name, usage)
}
複制代碼           

複制

通過

newDurationValue

建立了一個類型為

durationValue

的變量,并傳入到了

CommandLine.Var

方法中。

如果繼續往下追,會根據 Value 建立一個

Flag

變量。 如下:

func (f *FlagSet) Var(value Value, name string, usage string) {
	flag := &Flag{name, usage, value, value.String()}
	...
}
複制代碼           

複制

Var

的定義可以看出,它的第一個參數類型是

Value

接口類型,也就說,durationValue 是實作了

Value

接口的類型。

注意,源碼中出現的

FlagSet

可以先忽略,它是下篇介紹子指令時重點關注的對象。

看下

Value

的定義,如下:

type Value interface {
	String() string
	Set(string) error
}
複制代碼           

複制

那麼,

durationValue

的實作代碼如何?

// 傳入參數分别是預設值和擷取 Flag 值的變量位址
func newDurationValue(val time.Duration, p *time.Duration) *durationValue {
	// 将預設值設定到 p 上
	*p = val
	// 使用 p 建立新的類型,保證可以擷取到解析的結果
	return (*durationValue)(p)
}

// Set 方法負責解析傳入的值
func (d *durationValue) Set(s string) error {
	v, err := time.ParseDuration(s)
	if err != nil {
		err = errParse
	}
	*d = durationValue(v)
	return err
}

// 擷取真正的值
func (d *durationValue) String() string { return (*time.Duration)(d).String() }
複制代碼           

複制

核心在兩個地方。

一個是建立新類型變量時,要使用傳入的變量位址建立新類型變量,以實作将解析結果放到其中,讓前端能擷取到,二是

Set

方法中實作指令行傳入字元串的解析。

邏輯梳理

看完上個小節,基本已經了解如何擴充一個新類型了。本質是是實作

Value

接口。

再看下之前提到的幾個變量,分别是存放解析結果的指針、解析指令行輸入的

Value

和表示一個選項的

Flag

。對應于

flag.DurationVar

,這個變量的類型分别是

*time.Duration

durationValue

Flag

比如有

duration=1h

,大緻流程是首先從

os.Args

擷取參數,按規則解析出選項名稱

duration

,查找是否存在名稱為

duration

Flag

,如果存在,使用

Flag.Value.Set

解析

1h

,如果不滿足

duration

的要求,将給出錯誤提示。

實作新類型

現在實作文章開頭要求的目标。

新類型定義如下:

type stringEnumValue struct {
	options []string
	p   *string
}
複制代碼           

複制

名為

StringEnumValue

,即字元串枚舉。它有

options

p

兩個成員,

options

指定一定範圍的值,

p

string

指針,儲存解析結果的變量的位址。

下面定義建立

StringEnumValue

變量的函數

newStringEnumValue

,代碼如下:

func newStringEnumValue(val string, p *string, options []string) *StringEnumValue {
	*option = val
	return &stringEnumValue{options: options, p: p}
}
複制代碼           

複制

除了

val

p

兩個必要的輸入外,還有一個

string

切片類型的數,名為

options

,它用于範圍的限定。而函數主體,首先設定預設值,然後使用

options

p

建立變量傳回。

Set

是核心方法,解析指令行傳入字元串。代碼如下:

func (s *StringEnumValue) Set(v string) error {
	for _, option := range s.options {
		if v == option {
			*(s.p) = v
			return nil
		}
	}

	return fmt.Errorf("must be one of %v", s.options)
}
複制代碼           

複制

循環檢查輸入參數

v

是否滿足要求。定義如下:

最後是

String()

方法,

func (s *StringEnumValue) String() string {
	return *(s.p)
}
複制代碼           

複制

傳回

p

指針中的值。前面分析實作思路時,

Flag

在設定預設值時就調用了它。

使用 StringEnumValue

直接看代碼吧。如下:

var origin string
func init() {
	flag.Var(
		newStringEnumValue(
			"installed", 	// 預設值
			&origin,
			[]string{"installed", "local", "remote"},
		),
		"origin",
		`the origin of version information, such as installed, local, remote (default: "installed")`,
	)
}

func main() {
	flag.Parse()
	fmt.Println(option)
}
複制代碼           

複制

重點就是

flag.Var(newStringEnumValue(...),...)

。如果覺得有點啰嗦,希望和其他類型建立過程相同,在這個基礎上可以再包裝。代碼如下:

func StringEnumVar(p *string, name string, options []string, defVal string, usage string) {
	flag.Var(newStringEnumValue(defVal, p, options), name, usage)
}
複制代碼           

複制

編譯測試下,結果如下:

$ gvg --origin=any
invalid value "any" for flag -origin: must be one of [installed local remote]
Usage of gvg:
  -origin value
  	the origin of version information, such as installed, local, remote (default installed)
$ gvg --origin=remote
origin remote
複制代碼           

複制