Go 每日一庫之 flag

緣起

我一直在想,有什麼方式可讓人比較輕易地保持每日學習,持續輸出的狀態。寫博客是一種方式,但不是天天都有想寫的,值得寫的東西。 有時候一個技術比較複雜,寫博客的時候常常會寫着寫着發現本身的理解有誤差,或者細節尚未徹底掌握,要去查資料,瞭解了以後又繼續寫,如此反覆。 這樣會致使一篇博客的耗時過長。python

我在天天瀏覽思否掘金Github的過程當中,發現一些比較好的想法,有JS 每日一題NodeJS 每日一庫天天一道面試題等等等等。 github.com/parro-it/aw…這個倉庫收集 NodeJS 小型庫,一天看一個不是夢!這也是我這個系列的靈感。 我計劃天天學習一個 Go 語言的庫,輸出一篇介紹型的博文。天天一庫固然是理想狀態,我心中的預期是一週 3-5 個。git

今天是第一天,咱們從一個基礎庫聊起————Go 標準庫中的flaggithub

簡介

flag用於解析命令行選項。有過類 Unix 系統使用經驗的童鞋對命令行選項應該不陌生。例如命令ls -al列出當前目錄下全部文件和目錄的詳細信息,其中-al就是命令行選項。golang

命令行選項在實際開發中很經常使用,特別是在寫工具的時候。面試

  • 指定配置文件的路徑,如redis-server ./redis.conf以當前目錄下的配置文件redis.conf啓動 Redis 服務器;
  • 自定義某些參數,如python -m SimpleHTTPServer 8080啓動一個 HTTP 服務器,監聽 8080 端口。若是不指定,則默認監聽 8000 端口。

快速使用

學習一個庫的第一步固然是使用它。咱們先看看flag庫的基本使用:redis

package main

import (
  "fmt"
  "flag"
)

var (
  intflag int
  boolflag bool
  stringflag string
)

func init() {
  flag.IntVar(&intflag, "intflag", 0, "int flag value")
  flag.BoolVar(&boolflag, "boolflag", false, "bool flag value")
  flag.StringVar(&stringflag, "stringflag", "default", "string flag value")
}

func main() {
  flag.Parse()

  fmt.Println("int flag:", intflag)
  fmt.Println("bool flag:", boolflag)
  fmt.Println("string flag:", stringflag)
}
複製代碼

能夠先編譯程序,而後運行(我使用的是 Win10 + Git Bash):shell

$ go build -o main.exe main.go
$ ./main.exe -intflag 12 -boolflag 1 -stringflag test
複製代碼

輸出:npm

int flag: 12
bool flag: true
string flag: test
複製代碼

若是不設置某個選項,相應變量會取默認值:segmentfault

$ ./main.exe -intflag 12 -boolflag 1
複製代碼

輸出:bash

int flag: 12
bool flag: true
string flag: default
複製代碼

能夠看到沒有設置的選項stringflag爲默認值default

還能夠直接使用go run,這個命令會先編譯程序生成可執行文件,而後執行該文件,將命令行中的其它選項傳給這個程序。

$ go run main.go -intflag 12 -boolflag 1
複製代碼

可使用-h顯示選項幫助信息:

$ ./main.exe -h
Usage of D:\code\golang\src\github.com\darjun\cmd\flag\main.exe:
  -boolflag
        bool flag value
  -intflag int
        int flag value
  -stringflag string
        string flag value (default "default")
複製代碼

總結一下,使用flag庫的通常步驟:

  • 定義一些全局變量存儲選項的值,如這裏的intflag/boolflag/stringflag
  • init方法中使用flag.TypeVar方法定義選項,這裏的Type能夠爲基本類型Int/Uint/Float64/Bool,還能夠是時間間隔time.Duration。定義時傳入變量的地址、選項名、默認值和幫助信息;
  • main方法中調用flag.Parseos.Args[1:]中解析選項。由於os.Args[0]爲可執行程序路徑,會被剔除。

注意點:

flag.Parse方法必須在全部選項都定義以後調用,且flag.Parse調用以後不能再定義選項。若是按照前面的步驟,基本不會出現問題。 由於init在全部代碼以前執行,將選項定義都放在init中,main函數中執行flag.Parse時全部選項都已經定義了。

選項格式

flag庫支持三種命令行選項格式。

-flag
-flag=x
-flag x
複製代碼

---均可以使用,它們的做用是同樣的。有些庫使用-表示短選項,--表示長選項。相對而言,flag使用起來更簡單。

第一種形式只支持布爾類型的選項,出現即爲true,不出現爲默認值。 第三種形式不支持布爾類型的選項。由於這種形式的布爾選項在類 Unix 系統中可能會出現意想不到的行爲。看下面的命令:

cmd -x *
複製代碼

其中,*是 shell 通配符。若是有名字爲 0、false的文件,布爾選項-x將會取false。反之,布爾選項-x將會取true。並且這個選項消耗了一個參數。 若是要顯示設置一個布爾選項爲false,只能使用-flag=false這種形式。

遇到第一個非選項參數(即不是以---開頭的)或終止符--,解析中止。運行下面程序:

$ ./main.exe noflag -intflag 12
複製代碼

將會輸出:

int flag: 0
bool flag: false
string flag: default
複製代碼

由於解析遇到noflag就中止了,後面的選項-intflag沒有被解析到。因此全部選項都取的默認值。

運行下面的程序:

$ ./main.exe -intflag 12 -- -boolflag=true
複製代碼

將會輸出:

int flag: 12
bool flag: false
string flag: default
複製代碼

首先解析了選項intflag,設置其值爲 12。遇到--後解析終止了,後面的--boolflag=true沒有被解析到,因此boolflag選項取默認值false

解析終止以後若是還有命令行參數,flag庫會存儲下來,經過flag.Args方法返回這些參數的切片。 能夠經過flag.NArg方法獲取未解析的參數數量,flag.Arg(i)訪問位置i(從 0 開始)上的參數。 選項個數也能夠經過調用flag.NFlag方法獲取。

稍稍修改一下上面的程序:

func main() {
  flag.Parse()
    
  fmt.Println(flag.Args())
  fmt.Println("Non-Flag Argument Count:", flag.NArg())
  for i := 0; i < flag.NArg(); i++ {
    fmt.Printf("Argument %d: %s\n", i, flag.Arg(i))
  }
  
  fmt.Println("Flag Count:", flag.NFlag())
}
複製代碼

編譯運行該程序:

$ go build -o main.exe main.go
$ ./main.exe -intflag 12 -- -stringflag test
複製代碼

輸出:

[-stringflag test]
Non-Flag Argument Count: 2
Argument 0: -stringflag
Argument 1: test
複製代碼

解析遇到--終止後,剩餘參數-stringflag test保存在flag中,能夠經過Args/NArg/Arg等方法訪問。

整數選項值能夠接受 1234(十進制)、0664(八進制)和 0x1234(十六進制)的形式,而且能夠是負數。實際上flag在內部使用strconv.ParseInt方法將字符串解析成int。 因此理論上,ParseInt接受的格式均可以。

布爾類型的選項值能夠爲:

  • 取值爲true的:一、t、T、true、TRUE、True;
  • 取值爲false的:0、f、F、false、FALSE、False。

另外一種定義選項的方式

上面咱們介紹了使用flag.TypeVar定義選項,這種方式須要咱們先定義變量,而後變量的地址。 還有一種方式,調用flag.Type(其中Type能夠爲Int/Uint/Bool/Float64/String/Duration等)會自動爲咱們分配變量,返回該變量的地址。用法與前一種方式相似:

package main

import (
  "fmt"
  "flag"
)

var (
  intflag *int
  boolflag *bool
  stringflag *string
)

func init() {
  intflag = flag.Int("intflag", 0, "int flag value")
  boolflag = flag.Bool("boolflag", false, "bool flag value")
  stringflag = flag.String("stringflag", "default", "string flag value")
}

func main() {
  flag.Parse()
    
  fmt.Println("int flag:", *intflag)
  fmt.Println("bool flag:", *boolflag)
  fmt.Println("string flag:", *stringflag)
}
複製代碼

編譯並運行程序:

$ go build -o main.exe main.go
$ ./main.exe -intflag 12
複製代碼

將輸出:

int flag: 12
bool flag: false
string flag: default
複製代碼

除了使用時須要解引用,其它與前一種方式基本相同。

高級用法

定義短選項

flag庫並無顯示支持短選項,可是能夠經過給某個相同的變量設置不一樣的選項來實現。即兩個選項共享同一個變量。 因爲初始化順序不肯定,必須保證它們擁有相同的默認值。不然不傳該選項時,行爲是不肯定的。

package main

import (
  "fmt"
  "flag"
)

var logLevel string

func init() {
  const (
    defaultLogLevel = "DEBUG"
    usage = "set log level value"
  )
  
  flag.StringVar(&logLevel, "log_type", defaultLogLevel, usage)
  flag.StringVar(&logLevel, "l", defaultLogLevel, usage + "(shorthand)")
}

func main() {
  flag.Parse()

  fmt.Println("log level:", logLevel)
}
複製代碼

編譯、運行程序:

$ go build -o main.exe main.go
$ ./main.exe -log_type WARNING
$ ./main.exe -l WARNING
複製代碼

使用長、短選項均輸出:

log level: WARNING
複製代碼

不傳入該選項,輸出默認值:

$ ./main.exe
log level: DEBUG
複製代碼

解析時間間隔

除了能使用基本類型做爲選項,flag庫還支持time.Duration類型,即時間間隔。時間間隔支持的格式很是之多,例如"300ms"、"-1.5h"、"2h45m"等等等等。 時間單位能夠是 ns/us/ms/s/m/h/day 等。實際上flag內部會調用time.ParseDuration。具體支持的格式能夠參見time(需fq)庫的文檔。

package main

import (
  "flag"
  "fmt"
  "time"
)

var (
  period time.Duration
)

func init() {
  flag.DurationVar(&period, "period", 1*time.Second, "sleep period")
}

func main() {
  flag.Parse()
  fmt.Printf("Sleeping for %v...", period)
  time.Sleep(period)
  fmt.Println()
}
複製代碼

根據傳入的命令行選項period,程序睡眠相應的時間,默認 1 秒。編譯、運行程序:

$ go build -o main.exe main.go
$ ./main.exe
Sleeping for 1s...

$ ./main.exe -period 1m30s
Sleeping for 1m30s...
複製代碼

自定義選項

除了使用flag庫提供的選項類型,咱們還能夠自定義選項類型。咱們分析一下標準庫中提供的案例:

package main

import (
  "errors"
  "flag"
  "fmt"
  "strings"
  "time"
)

type interval []time.Duration

func (i *interval) String() string {
  return fmt.Sprint(*i)
}

func (i *interval) Set(value string) error {
  if len(*i) > 0 {
    return errors.New("interval flag already set")
  }
  for _, dt := range strings.Split(value, ",") {
    duration, err := time.ParseDuration(dt)
    if err != nil {
      return err
    }
    *i = append(*i, duration)
  }
  return nil
}

var (
  intervalFlag interval
)

func init() {
  flag.Var(&intervalFlag, "deltaT", "comma-seperated list of intervals to use between events")
}

func main() {
  flag.Parse()

  fmt.Println(intervalFlag)
}
複製代碼

首先定義一個新類型,這裏定義類型interval

新類型必須實現flag.Value接口:

// src/flag/flag.go
type Value interface {
  String() string
  Set(string) error
}
複製代碼

其中String方法格式化該類型的值,flag.Parse方法在執行時遇到自定義類型的選項會將選項值做爲參數調用該類型變量的Set方法。 這裏將以,分隔的時間間隔解析出來存入一個切片中。

自定義類型選項的定義必須使用flag.Var方法。

編譯、執行程序:

$ go build -o main.exe main.go
$ ./main.exe -deltaT 30s
[30s]
$ ./main.exe -deltaT 30s,1m,1m30s
[30s 1m0s 1m30s]
複製代碼

若是指定的選項值非法,Set方法返回一個error類型的值,Parse執行終止,打印錯誤和使用幫助。

$ ./main.exe -deltaT 30x
invalid value "30x" for flag -deltaT: time: unknown unit x in duration 30x
Usage of D:\code\golang\src\github.com\darjun\go-daily-lib\flag\self-defined\main.exe:
  -deltaT value
        comma-seperated list of intervals to use between events
複製代碼

解析程序中的字符串

有時候選項並非經過命令行傳遞的。例如,從配置表中讀取或程序生成的。這時候可使用flag.FlagSet結構的相關方法來解析這些選項。

實際上,咱們前面調用的flag庫的方法,都會間接調用FlagSet結構的方法。flag庫中定義了一個FlagSet類型的全局變量CommandLine專門用於解析命令行選項。 前面調用的flag庫的方法只是爲了提供便利,它們內部都是調用的CommandLine的相應方法:

// src/flag/flag.go
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

func Parse() {
  CommandLine.Parse(os.Args[1:])
}

func IntVar(p *int, name string, value int, usage string) {
  CommandLine.Var(newIntValue(value, p), name, usage)
}

func Int(name string, value int, usage string) *int {
  return CommandLine.Int(name, value, usage)
}

func NFlag() int { return len(CommandLine.actual) }

func Arg(i int) string {
  return CommandLine.Arg(i)
}

func NArg() int { return len(CommandLine.args) }
複製代碼

一樣的,咱們也能夠本身建立FlagSet類型變量來解析選項。

package main

import (
  "flag"
  "fmt"
)

func main() {
  args := []string{"-intflag", "12", "-stringflag", "test"}

  var intflag int
  var boolflag bool
  var stringflag string

  fs := flag.NewFlagSet("MyFlagSet", flag.ContinueOnError)
  fs.IntVar(&intflag, "intflag", 0, "int flag value")
  fs.BoolVar(&boolflag, "boolflag", false, "bool flag value")
  fs.StringVar(&stringflag, "stringflag", "default", "string flag value")

  fs.Parse(args)
  
  fmt.Println("int flag:", intflag)
  fmt.Println("bool flag:", boolflag)
  fmt.Println("string flag:", stringflag)
}
複製代碼

NewFlagSet方法有兩個參數,第一個參數是程序名稱,輸出幫助或出錯時會顯示該信息。第二個參數是解析出錯時如何處理,有幾個選項:

  • ContinueOnError:發生錯誤後繼續解析,CommandLine就是使用這個選項;
  • ExitOnError:出錯時調用os.Exit(2)退出程序;
  • PanicOnError:出錯時產生 panic。

隨便看一眼flag庫中的相關代碼:

// src/flag/flag.go
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
}
複製代碼

與直接使用flag庫的方法有一點不一樣,FlagSet調用Parse方法時須要顯示傳入字符串切片做爲參數。由於flag.Parse在內部調用了CommandLine.Parse(os.Args[1:])。 示例代碼都放在GitHub上了。

參考

  1. flag庫文檔

個人博客

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

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索