簡介
在上一篇文章中,我們介紹了flag庫。flag庫是用于解析命令行選項(xiàng)的。但是flag有幾個缺點(diǎn):
不顯示支持短選項(xiàng)。當(dāng)然上一篇文章中也提到過可以通過將兩個選項(xiàng)共享同一個變量迂回實(shí)現(xiàn),但寫起來比較繁瑣;
選項(xiàng)變量的定義比較繁瑣,每個選項(xiàng)都需要根據(jù)類型調(diào)用對應(yīng)的Type或TypeVar函數(shù);
默認(rèn)只支持有限的數(shù)據(jù)類型,當(dāng)前只有基本類型bool/int/uint/string和time.Duration;
為了解決這些問題,出現(xiàn)了不少第三方解析命令行選項(xiàng)的庫,今天的主角go-flags就是其中一個。第一次看到go-flags庫是在閱讀pgweb源碼的時候。
go-flags提供了比標(biāo)準(zhǔn)庫flag更多的選項(xiàng)。它利用結(jié)構(gòu)標(biāo)簽(struct tag)和反射提供了一個方便、簡潔的接口。它除了基本的功能,還提供了豐富的特性:
支持短選項(xiàng)(-v)和長選項(xiàng)(--verbose);
支持短選項(xiàng)合寫,如-aux;
同一個選項(xiàng)可以設(shè)置多個值;
支持所有的基礎(chǔ)類型和 map 類型,甚至是函數(shù);
支持命名空間和選項(xiàng)組;
等等。
上面只是粗略介紹了go-flags的特性,下面我們依次來介紹。
快速開始
學(xué)習(xí)從使用開始!我們先來看看go-flags的基本使用。
由于是第三方庫,使用前需要安裝,執(zhí)行下面的命令安裝:
$ go get github.com/jessevdk/go-flags
代碼中使用import導(dǎo)入該庫:
import "github.com/jessevdk/go-flags"
完整示例代碼如下:
package main
import (
? "fmt"
? "github.com/jessevdk/go-flags"
)
type Option struct {
? Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug message"`
}
func main() {
? var opt Option
? flags.Parse(&opt)
? fmt.Println(opt.Verbose)
}
使用go-flags的一般步驟:
定義選項(xiàng)結(jié)構(gòu),在結(jié)構(gòu)標(biāo)簽中設(shè)置選項(xiàng)信息。通過short和long設(shè)置短、長選項(xiàng)名字,description設(shè)置幫助信息。命令行傳參時,短選項(xiàng)前加-,長選項(xiàng)前加--;
聲明選項(xiàng)變量;
調(diào)用go-flags的解析方法解析。
編譯、運(yùn)行代碼(我的環(huán)境是 Win10 + Git Bash):
$ go build -o main.exe main.go
短選項(xiàng):
$ ./main.exe -v
[true]
長選項(xiàng):
$ ./main.exe --verbose
[true]
由于Verbose字段是切片類型,每次遇到-v或--verbose都會追加一個true到切片中。
多個短選項(xiàng):
$ ./main.exe -v -v
[true true]
多個長選項(xiàng):
$ ./main.exe --verbose --verbose
[true true]
短選項(xiàng) + 長選項(xiàng):
$ ./main.exe -v --verbose -v
[true true true]
短選項(xiàng)合寫:
$ ./main.exe -vvv
[true true true]
基本特性
支持豐富的數(shù)據(jù)類型
go-flags相比標(biāo)準(zhǔn)庫flag支持更豐富的數(shù)據(jù)類型:
所有的基本類型(包括有符號整數(shù)int/int8/int16/int32/int64,無符號整數(shù)uint/uint8/uint16/uint32/uint64,浮點(diǎn)數(shù)float32/float64,布爾類型bool和字符串string)和它們的切片;
map 類型。只支持鍵為string,值為基礎(chǔ)類型的 map;
函數(shù)類型。
如果字段是基本類型的切片,基本解析流程與對應(yīng)的基本類型是一樣的。切片類型選項(xiàng)的不同之處在于,遇到相同的選項(xiàng)時,值會被追加到切片中。而非切片類型的選項(xiàng),后出現(xiàn)的值會覆蓋先出現(xiàn)的值。
下面來看一個示例:
package main
import (
? "fmt"
? "github.com/jessevdk/go-flags"
)
type Option struct {
? IntFlag? ? ? ? int? ? ? ? ? ? `short:"i" long:"int" description:"int flag value"`
? IntSlice? ? ? ? []int? ? ? ? ? `long:"intslice" description:"int slice flag value"`
? BoolFlag? ? ? ? bool? ? ? ? ? ? `long:"bool" description:"bool flag value"`
? BoolSlice? ? ? []bool? ? ? ? ? `long:"boolslice" description:"bool slice flag value"`
? FloatFlag? ? ? float64? ? ? ? `long:"float", description:"float64 flag value"`
? FloatSlice? ? ? []float64? ? ? `long:"floatslice" description:"float64 slice flag value"`
? StringFlag? ? ? string? ? ? ? ? `short:"s" long:"string" description:"string flag value"`
? StringSlice? ? []string? ? ? ? `long:"strslice" description:"string slice flag value"`
? PtrStringSlice? []*string? ? ? `long:"pstrslice" description:"slice of pointer of string flag value"`
? Call? ? ? ? ? ? func(string)? ? `long:"call" description:"callback"`
? IntMap? ? ? ? ? map[string]int? `long:"intmap" description:"A map from string to int"`
}
func main() {
? var opt Option
? opt.Call = func (value string) {
? ? fmt.Println("in callback: ", value)
? }
? err := flags.Parse(&opt, os.Args[1:])
? if err != nil {
? ? fmt.Println("Parse error:", err)
? ? return
? }
? fmt.Printf("int flag: %v\n", opt.IntFlag)
? fmt.Printf("int slice flag: %v\n", opt.IntSlice)
? fmt.Printf("bool flag: %v\n", opt.BoolFlag)
? fmt.Printf("bool slice flag: %v\n", opt.BoolSlice)
? fmt.Printf("float flag: %v\n", opt.FloatFlag)
? fmt.Printf("float slice flag: %v\n", opt.FloatSlice)
? fmt.Printf("string flag: %v\n", opt.StringFlag)
? fmt.Printf("string slice flag: %v\n", opt.StringSlice)
? fmt.Println("slice of pointer of string flag: ")
? for i := 0; i < len(opt.PtrStringSlice); i++ {
? ? fmt.Printf("\t%d: %v\n", i, *opt.PtrStringSlice[i])
? }
? fmt.Printf("int map: %v\n", opt.IntMap)
}
基本類型和其切片比較簡單,就不過多介紹了。值得留意的是基本類型指針的切片,即上面的PtrStringSlice字段,類型為[]*string。由于結(jié)構(gòu)中存儲的是字符串指針,go-flags在解析過程中遇到該選項(xiàng)會自動創(chuàng)建字符串,將指針追加到切片中。
運(yùn)行程序,傳入--pstrslice選項(xiàng):
$ ./main.exe --pstrslice test1 --pstrslice test2
slice of pointer of string flag:
? ? 0: test1
? ? 1: test2
另外,我們可以在選項(xiàng)中定義函數(shù)類型。該函數(shù)的唯一要求是有一個字符串類型的參數(shù)。解析中每次遇到該選項(xiàng)就會以選項(xiàng)值為參數(shù)調(diào)用這個函數(shù)。上面代碼中,Call函數(shù)只是簡單的打印傳入的選項(xiàng)值。運(yùn)行代碼,傳入--call選項(xiàng):
$ ./main.exe --call test1 --call test2
in callback:? test1
in callback:? test2
最后,go-flags還支持 map 類型。雖然限制鍵必須是string類型,值必須是基本類型,也能實(shí)現(xiàn)比較靈活的配置。 map類型的選項(xiàng)值中鍵-值通過:分隔,如key:value,可設(shè)置多個。運(yùn)行代碼,傳入--intmap選項(xiàng):
$ ./main.exe --intmap key1:12 --intmap key2:58
int map: map[key1:12 key2:58]
常用設(shè)置
go-flags提供了非常多的設(shè)置選項(xiàng),具體可參見文檔。這里重點(diǎn)介紹兩個required和default。
required非空時,表示對應(yīng)的選項(xiàng)必須設(shè)置值,否則解析時返回ErrRequired錯誤。
default用于設(shè)置選項(xiàng)的默認(rèn)值。如果已經(jīng)設(shè)置了默認(rèn)值,那么required是否設(shè)置并不影響,也就是說命令行參數(shù)中該選項(xiàng)可以沒有。
看下面示例:
package main
import (
? "fmt"
? "log"
? "github.com/jessevdk/go-flags"
)
type Option struct {
? Required? ? string? `short:"r" long:"required" required:"true"`
? Default? ? string? `short:"d" long:"default" default:"default"`
}
func main() {
? var opt Option
? _, err := flags.Parse(&opt)
? if err != nil {
? ? log.Fatal("Parse error:", err)
? }
? fmt.Println("required: ", opt.Required)
? fmt.Println("default: ", opt.Default)
}
運(yùn)行程序,不傳入default選項(xiàng),Default字段取默認(rèn)值,不傳入required選項(xiàng),執(zhí)行報錯:
$ ./main.exe -r required-data
required:? required-data
default:? default
$ ./main.exe -d default-data -r required-data
required:? required-data
default:? default-data
$ ./main.exe
the required flag `/r, /required' was not specified
2020/01/09 18:07:39 Parse error:the required flag `/r, /required' was not specified
高級特性
選項(xiàng)分組
package main
import (
? "fmt"
? "log"
? "os"
? "github.com/jessevdk/go-flags"
)
type Option struct {
? Basic GroupBasicOption `description:"basic type" group:"basic"`
? Slice GroupSliceOption `description:"slice of basic type" group:"slice"`
}
type GroupBasicOption struct {
? IntFlag? ? int? ? `short:"i" long:"intflag" description:"int flag"`
? BoolFlag? bool? ? `short:"b" long:"boolflag" description:"bool flag"`
? FloatFlag? float64 `short:"f" long:"floatflag" description:"float flag"`
? StringFlag string? `short:"s" long:"stringflag" description:"string flag"`
}
type GroupSliceOption struct {
? IntSlice? ? ? ? int? ? ? ? ? ? `long:"intslice" description:"int slice"`
? BoolSlice? ? ? ? bool? ? ? ? `long:"boolslice" description:"bool slice"`
? FloatSlice? ? float64? ? `long:"floatslice" description:"float slice"`
? StringSlice? ? string? ? `long:"stringslice" description:"string slice"`
}
func main() {
? var opt Option
? p := flags.NewParser(&opt, flags.Default)
? _, err := p.ParseArgs(os.Args[1:])
? if err != nil {
? ? log.Fatal("Parse error:", err)
? }
? basicGroup := p.Command.Group.Find("basic")
? for _, option := range basicGroup.Options() {
? ? fmt.Printf("name:%s value:%v\n", option.LongNameWithNamespace(), option.Value())
? }
? sliceGroup := p.Command.Group.Find("slice")
? for _, option := range sliceGroup.Options() {
? ? fmt.Printf("name:%s value:%v\n", option.LongNameWithNamespace(), option.Value())
? }
}
上面代碼中我們將基本類型和它們的切片類型選項(xiàng)拆分到兩個結(jié)構(gòu)體中,這樣可以使代碼看起來更清晰自然,特別是在代碼量很大的情況下。這樣做還有一個好處,我們試試用--help運(yùn)行該程序:
$ ./main.exe --help
Usage:
? D:\code\golang\src\github.com\darjun\go-daily-lib\go-flags\group\main.exe [OPTIONS]
basic:
? /i, /intflag:? ? ? int flag
? /b, /boolflag? ? ? bool flag
? /f, /floatflag:? ? float flag
? /s, /stringflag:? string flag
slice:
? /intslice:? ? int slice
? /boolslice? ? bool slice
? /floatslice:? float slice
? /stringslice:? string slice
Help Options:
? /?? ? ? ? ? ? ? ? Show this help message
? /h, /help? ? ? ? ? Show this help message
輸出的幫助信息中,也是按照我們設(shè)定的分組顯示了,便于查看。
子命令
go-flags支持子命令。我們經(jīng)常使用的 Go 和 Git 命令行程序就有大量的子命令。例如go version、go build、go run、git status、git commit這些命令中version/build/run/status/commit就是子命令。使用go-flags定義子命令比較簡單:
package main
import (
? "errors"
? "fmt"
? "log"
? "strconv"
? "strings"
? "github.com/jessevdk/go-flags"
)
type MathCommand struct {
? Op string `long:"op" description:"operation to execute"`
? Args []string
? Result int64
}
func (this *MathCommand) Execute(args []string) error {
? if this.Op != "+" && this.Op != "-" && this.Op != "x" && this.Op != "/" {
? ? return errors.New("invalid op")
? }
? for _, arg := range args {
? ? num, err := strconv.ParseInt(arg, 10, 64)
? ? if err != nil {
? ? ? return err
? ? }
? ? this.Result += num
? }
? this.Args = args
? return nil
}
type Option struct {
? ? Math MathCommand `command:"math"`
}
func main() {
? ? var opt Option
? ? _, err := flags.Parse(&opt)
? ? if err != nil {
? ? ? ? log.Fatal(err)
? ? }
? ? fmt.Printf("The result of %s is %d", strings.Join(opt.Math.Args, opt.Math.Op), opt.Math.Result)
}
子命令必須實(shí)現(xiàn)go-flags定義的Commander接口:
type Commander interface {
? ? Execute(args []string) error
}
解析命令行時,如果遇到不是以-或--開頭的參數(shù),go-flags會嘗試將其解釋為子命令名。子命令的名字通過在結(jié)構(gòu)標(biāo)簽中使用command指定。子命令后面的參數(shù)都將作為子命令的參數(shù),子命令也可以有選項(xiàng)。
上面代碼中,我們實(shí)現(xiàn)了一個可以計(jì)算任意個整數(shù)的加、減、乘、除子命令math。
接下來看看如何使用:
$ ./main.exe math --op + 1 2 3 4 5
The result of 1+2+3+4+5 is 15
$ ./main.exe math --op - 1 2 3 4 5
The result of 1-2-3-4-5 is -13
$ ./main.exe math --op x 1 2 3 4 5
The result of 1x2x3x4x5 is 120
$ ./main.exe math --op ÷ 120 2 3 4 5
The result of 120÷2÷3÷4÷5 is 1
注意,不能使用乘法符號*和除法符號/,它們都不可識別。
其他
go-flags庫還有很多有意思的特性,例如支持 Windows 選項(xiàng)格式(/v和/verbose)、從環(huán)境變量中讀取默認(rèn)值、從 ini 文件中讀取默認(rèn)設(shè)置等等。大家有興趣可以自行去研究~
參考
go-flagsGithub 倉庫
go-flagsGoDoc 文檔
我
我的博客
歡迎關(guān)注我的微信公眾號【GoUpUp】,共同學(xué)習(xí),一起進(jìn)步~