Go 每日一庫之 go-flags

簡介

上一篇文章中,咱們介紹了flag庫。flag庫是用於解析命令行選項的。可是flag有幾個缺點:git

  • 不顯示支持短選項。固然上一篇文章中也提到過能夠經過將兩個選項共享同一個變量迂迴實現,但寫起來比較繁瑣;
  • 選項變量的定義比較繁瑣,每一個選項都須要根據類型調用對應的TypeTypeVar函數;
  • 默認只支持有限的數據類型,當前只有基本類型bool/int/uint/stringtime.Duration

爲了解決這些問題,出現了很多第三方解析命令行選項的庫,今天的主角go-flags就是其中一個。第一次看到go-flags庫是在閱讀pgweb源碼的時候。github

go-flags提供了比標準庫flag更多的選項。它利用結構標籤(struct tag)和反射提供了一個方便、簡潔的接口。它除了基本的功能,還提供了豐富的特性:golang

  • 支持短選項(-v)和長選項(--verbose);
  • 支持短選項合寫,如-aux
  • 同一個選項能夠設置多個值;
  • 支持全部的基礎類型和 map 類型,甚至是函數;
  • 支持命名空間和選項組;
  • 等等。

上面只是粗略介紹了go-flags的特性,下面咱們依次來介紹。web

快速開始

學習從使用開始!咱們先來看看go-flags的基本使用。segmentfault

因爲是第三方庫,使用前須要安裝,執行下面的命令安裝:微信

$ 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的通常步驟:ui

  • 定義選項結構,在結構標籤中設置選項信息。經過shortlong設置短、長選項名字,description設置幫助信息。命令行傳參時,短選項前加-,長選項前加--
  • 聲明選項變量;
  • 調用go-flags的解析方法解析。

編譯、運行代碼(個人環境是 Win10 + Git Bash):this

$ 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提供了很是多的設置選項,具體可參見文檔。這裏重點介紹兩個requireddefault

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 versiongo buildgo rungit statusgit 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 文件中讀取默認設置等等。你們有興趣能夠自行去研究~

參考

  1. go-flagsGithub 倉庫
  2. go-flagsGoDoc 文檔

個人博客

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

本文由博客一文多發平臺 OpenWrite 發佈!
相關文章
相關標籤/搜索