簡介
在上一篇文章中,我們介紹了flag
庫。flag
庫是用於解析命令列選項的。但是flag
有幾個缺點:
- 不顯示支援短選項。當然上一篇文章中也提到過可以通過將兩個選項共享同一個變數迂迴實現,但寫起來比較繁瑣;
- 選項變數的定義比較繁瑣,每個選項都需要根據型別呼叫對應的
Type
或TypeVar
函式; - 預設只支援有限的資料型別,當前只有基本型別
bool/int/uint/string
和time.Duration
;
為了解決這些問題,出現了不少第三方解析命令列選項的庫,今天的主角go-flags
就是其中一個。第一次看到go-flags
庫是在閱讀pgweb原始碼的時候。
go-flags
提供了比標準庫flag
更多的選項。它利用結構標籤(struct tag)和反射提供了一個方便、簡潔的介面。它除了基本的功能,還提供了豐富的特性:
- 支援短選項(-v)和長選項(--verbose);
- 支援短選項合寫,如
-aux
; - 同一個選項可以設定多個值;
- 支援所有的基礎型別和 map 型別,甚至是函式;
- 支援名稱空間和選項組;
- 等等。
上面只是粗略介紹了go-flags
的特性,下面我們依次來介紹。
快速開始
學習從使用開始!我們先來看看go-flags
的基本使用。
由於是第三方庫,使用前需要安裝,執行下面的命令安裝:
$ go get github.com/jessevdk/go-flags
複製程式碼
程式碼中使用import
匯入該庫:
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
的一般步驟:
- 定義選項結構,在結構標籤中設定選項資訊。通過
short
和long
設定短、長選項名字,description
設定幫助資訊。命令列傳參時,短選項前加-
,長選項前加--
; - 宣告選項變數;
- 呼叫
go-flags
的解析方法解析。
編譯、執行程式碼(我的環境是 Win10 + Git Bash):
$ go build -o main.exe main.go
複製程式碼
短選項:
$ ./main.exe -v
[true]
複製程式碼
長選項:
$ ./main.exe --verbose
[true]
複製程式碼
由於Verbose
欄位是切片型別,每次遇到-v
或--verbose
都會追加一個true
到切片中。
多個短選項:
$ ./main.exe -v -v
[true true]
複製程式碼
多個長選項:
$ ./main.exe --verbose --verbose
[true true]
複製程式碼
短選項 + 長選項:
$ ./main.exe -v --verbose -v
[true true true]
複製程式碼
短選項合寫:
$ ./main.exe -vvv
[true true true]
複製程式碼
基本特性
支援豐富的資料型別
go-flags
相比標準庫flag
支援更豐富的資料型別:
- 所有的基本型別(包括有符號整數
int/int8/int16/int32/int64
,無符號整數uint/uint8/uint16/uint32/uint64
,浮點數float32/float64
,布林型別bool
和字串string
)和它們的切片; - map 型別。只支援鍵為
string
,值為基礎型別的 map; - 函式型別。
如果欄位是基本型別的切片,基本解析流程與對應的基本型別是一樣的。切片型別選項的不同之處在於,遇到相同的選項時,值會被追加到切片中。而非切片型別的選項,後出現的值會覆蓋先出現的值。
下面來看一個示例:
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
。
由於結構中儲存的是字串指標,go-flags
在解析過程中遇到該選項會自動建立字串,將指標追加到切片中。
執行程式,傳入--pstrslice
選項:
$ ./main.exe --pstrslice test1 --pstrslice test2
slice of pointer of string flag:
0: test1
1: test2
複製程式碼
另外,我們可以在選項中定義函式型別。該函式的唯一要求是有一個字串型別的引數。解析中每次遇到該選項就會以選項值為引數呼叫這個函式。
上面程式碼中,Call
函式只是簡單的列印傳入的選項值。執行程式碼,傳入--call
選項:
$ ./main.exe --call test1 --call test2
in callback: test1
in callback: test2
複製程式碼
最後,go-flags
還支援 map 型別。雖然限制鍵必須是string
型別,值必須是基本型別,也能實現比較靈活的配置。
map
型別的選項值中鍵-值通過:
分隔,如key:value
,可設定多個。執行程式碼,傳入--intmap
選項:
$ ./main.exe --intmap key1:12 --intmap key2:58
int map: map[key1:12 key2:58]
複製程式碼
常用設定
go-flags
提供了非常多的設定選項,具體可參見文件。這裡重點介紹兩個required
和default
。
required
非空時,表示對應的選項必須設定值,否則解析時返回ErrRequired
錯誤。
default
用於設定選項的預設值。如果已經設定了預設值,那麼required
是否設定並不影響,也就是說命令列引數中該選項可以沒有。
看下面示例:
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)
}
複製程式碼
執行程式,不傳入default
選項,Default
欄位取預設值,不傳入required
選項,執行報錯:
$ ./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
複製程式碼
高階特性
選項分組
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())
}
}
複製程式碼
上面程式碼中我們將基本型別和它們的切片型別選項拆分到兩個結構體中,這樣可以使程式碼看起來更清晰自然,特別是在程式碼量很大的情況下。
這樣做還有一個好處,我們試試用--help
執行該程式:
$ ./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
複製程式碼
輸出的幫助資訊中,也是按照我們設定的分組顯示了,便於檢視。
子命令
go-flags
支援子命令。我們經常使用的 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)
}
複製程式碼
子命令必須實現go-flags
定義的Commander
介面:
type Commander interface {
Execute(args []string) error
}
複製程式碼
解析命令列時,如果遇到不是以-
或--
開頭的引數,go-flags
會嘗試將其解釋為子命令名。子命令的名字通過在結構標籤中使用command
指定。
子命令後面的引數都將作為子命令的引數,子命令也可以有選項。
上面程式碼中,我們實現了一個可以計算任意個整數的加、減、乘、除子命令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 選項格式(/v
和/verbose
)、從環境變數中讀取預設值、從 ini 檔案中讀取預設設定等等。大家有興趣可以自行去研究~
參考
我
歡迎關注我的微信公眾號【GoUpUp】,共同學習,一起進步~
本文由部落格一文多發平臺 OpenWrite 釋出!