golang 標準庫之FLAG

來源:互聯網
上載者:User

評論有人提到沒有例子,不知道講的是什麼。因此,為了大家能夠更好地理解,特意加了一個樣本。其實本文更多講解的是 flag 的實現原理,加上樣本之後,就更好地知道怎麼使用了。建議閱讀 《Go語言標準庫》一書的對應章節:flag – 命令列參數解析。

在寫命令列程式(工具、server)時,對命令參數進行解析是常見的需求。各種語言一般都會提供解析命令列參數的方法或庫,以方便程式員使用。如果命令列參數純粹自己寫代碼解析,對於比較複雜的,還是挺費勁的。在go標準庫中提供了一個包:flag,方便進行命令列解析。

註:區分幾個概念
1)命令列參數(或參數):是指運行程式提供的參數
2)已定義命令列參數:是指程式中通過flag.Xxx等這種形式定義了的參數
3)非flag(non-flag)命令列參數(或保留的命令列參數):後文解釋

一、使用樣本
我們以 nginx 為例,執行 nginx -h,輸出如下:

nginx version: nginx/1.10.0
Usage: nginx [-?hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives]
Options:
   
   -?,-h         : this help
   
   -v            : show version and exit
   
   -V            : show version and configure options then exit
   
   -t            : test configuration and exit
   
   -T            : test configuration, dump it and exit
   
   -q            : suppress non-error messages during configuration testing
   
   -s signal     : send signal to a master process: stop, quit, reopen, reload
   
   -p prefix     : set prefix path (default: /usr/local/nginx/)
   
   -c filename   : set configuration file (default: conf/nginx.conf)
   
  -g directives : set global directives out of configuration file
我們通過 `flag` 實作類別似 nginx 的這個輸出,建立檔案 nginx.go,內容如下:

package main
import (

   "flag"

   "fmt"

   "os"

)

// 實際中應該用更好的變數名
var (
  
    h bool
    v, V bool
    t, T bool
    q    *bool
    s string
    p string
    c string
  
    g string
  
)

func init() {
 
   flag.BoolVar(&h, "h", false, "this help")
 
 
 
   flag.BoolVar(&v, "v", false, "show version and exit")
 
   flag.BoolVar(&V, "V", false, "show version and configure options then exit")
 
 
 
   flag.BoolVar(&t, "t", false, "test configuration and exit")
 
   flag.BoolVar(&T, "T", false, "test configuration, dump it and exit")
 
 
 
   // 另一種綁定方式
 
   q = flag.Bool("q", false, "suppress non-error messages during configuration testing")
 
 
 
   // 注意 `signal`。預設是 -s string,有了 `signal` 之後,變為 -s signal
 
   flag.StringVar(&s, "s", "", "send `signal` to a master process: stop, quit, reopen, reload")
 
   flag.StringVar(&p, "p", "/usr/local/nginx/", "set `prefix` path")
 
   flag.StringVar(&c, "c", "conf/nginx.conf", "set configuration `file`")
 
   flag.StringVar(&g, "g", "conf/nginx.conf", "set global `directives` out of configuration file")
 
 
 
   // 改變預設的 Usage
 
   flag.Usage = usage
 
}

func main() {
 
  flag.Parse()
 
 
 
  if h {
 
      flag.Usage()
 
  }
 
}

func usage() {
    fmt.Fprintf(os.Stderr, `nginx version: nginx/1.10.0
Usage: nginx [-hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives]
Options:
`)
 
   flag.PrintDefaults()
 
}
執行:go run nginx.go -h,(或 go build -o nginx && ./nginx -h)輸出如下:

nginx version: nginx/1.10.0
Usage: nginx [-hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives]

Options:
   
   -T    test configuration, dump it and exit
   
   -V    show version and configure options then exit
   
   -c file
   
         set configuration file (default "conf/nginx.conf")
   
   -g directives
   
         set global directives out of configuration file (default "conf/nginx.conf")
   
   -h    this help
   
   -p prefix
   
         set prefix path (default "/usr/local/nginx/")
   
   -q    suppress non-error messages during configuration testing
   
   -s signal
   
         send signal to a master process: stop, quit, reopen, reload
   
   -t    test configuration and exit
   
  -v    show version and exit
仔細理解以上例子,如果有不理解的,看完下文的講解再回過頭來看。

二、flag包概述
flag包實現了命令列參數的解析。

定義flags有兩種方式:

1)flag.Xxx(),其中Xxx可以是Int、String等;返回一個相應類型的指標,如:
var ip = flag.Int("flagname", 1234, "help message for flagname")
2)flag.XxxVar(),將flag綁定到一個變數上,如:

var flagvar int
flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
另外,還可以建立自訂flag,只要實現flag.Value介面即可(要求receiver是指標),這時候可以通過如下方式定義該flag:

flag.Var(&flagVal, "name", "help message for flagname")
例如,解析我喜歡的程式設計語言,我們希望直接解析到 slice 中,我們可以定義如下 Value:

type sliceValue []string

func newSliceValue(vals []string, p *[]string) *sliceValue {
 
   *p = vals
 
   return (*sliceValue)(p)
 
}

func (s *sliceValue) Set(val string) error {
 
  *s = sliceValue(strings.Split(val, ","))
 
  return nil
 
}
func (s *sliceValue) Get() interface{} { return []string(*s) }
func (s *sliceValue) String() string { return strings.Join([]string(*s), ",") }
之後可以這麼使用:
var languages []string
flag.Var(newSliceValue([]string{}, &languages), "slice", "I like programming `languages`")
這樣通過 -slice “go,php” 這樣的形式傳遞參數,languages 得到的就是 [go, php]。

flag 中對 Duration 這種非基本類型的支援,使用的就是類似這樣的方式。

在所有的flag定義完成之後,可以通過調用flag.Parse()進行解析。

命令列flag的文法有如下三種形式:
-flag // 只支援bool類型
-flag=x
-flag x // 只支援非bool類型

其中第三種形式只能用於非bool類型的flag,原因是:如果支援,那麼對於這樣的命令 cmd -x *,如果有一個檔案名稱字是:0或false等,則命令的原意會改變(之所以這樣,是因為bool類型支援-flag這種形式,如果bool類型不支援-flag這種形式,則bool類型可以和其他類型一樣處理。也正因為這樣,Parse()中,對bool類型進行了特殊處理)。預設的,提供了-flag,則對應的值為true,否則為flag.Bool/BoolVar中指定的預設值;如果希望顯示設定為false則使用-flag=false。

int類型可以是十進位、十六進位、八進位甚至是負數;bool類型可以是1, 0, t, f, true, false, TRUE, FALSE, True, False。Duration可以接受任何time.ParseDuration能解析的類型

三、類型和函數
在看類型和函數之前,先看一下變數
ErrHelp:該錯誤類型用於當命令列指定了-help參數但沒有定義時。
Usage:這是一個函數,用於輸出所有定義了的命令列參數和協助資訊(usage message)。一般,當命令列參數解析出錯時,該函數會被調用。我們可以指定自己的Usage函數,即:flag.Usage = func(){}

1、函數
go標準庫中,經常這麼做:

定義了一個類型,提供了很多方法;為了方便使用,會執行個體化一個該類型的執行個體(通用),這樣便可以直接使用該執行個體調用方法。比如:encoding/base64中提供了StdEncoding和URLEncoding執行個體,使用時:base64.StdEncoding.Encode()

在flag包中,進行了進一步封裝:將FlagSet的方法都重新定義了一遍,也就是提供了一序列函數,而函數中只是簡單的調用已經執行個體化好了的FlagSet執行個體:commandLine 的方法,這樣commandLine執行個體便不需要export。這樣,使用者是這麼調用:flag.Parse()而不是flag.commandLine.Parse()。(Go 1.2 版本起改為了 CommandLine)

這裡不詳細介紹各個函數,在類型方法中介紹。

2、類型(資料結構)
1)ErrorHandling
type ErrorHandling int
該類型定義了在參數解析出錯時錯誤處理方式定義了。三個該類型的常量:

const (

  ContinueOnError ErrorHandling = iota

  ExitOnError

  PanicOnError

)
三個常量在源碼的 FlagSet 的方法 parseOne() 中使用了。

2)Flag

// A Flag represents the state of a flag.

type Flag struct {

  Name     string // name as it appears on command line

  Usage    string // help message

  Value    Value  // value as set

  DefValue string // default value (as text); for usage message

}
Flag類型代表一個flag的狀態。

比如:autogo -f abc.txt,代碼 flag.String(“f”, “a.txt”, “usage”),則該Flag執行個體(可以通過flag.Lookup(“f”)獲得)相應的值為:f, usage, abc.txt, a.txt。

3)FlagSet

// A FlagSet represents a set of defined flags.
type FlagSet struct {

  // Usage is the function called when an error occurs while parsing flags.

  // The field is a function (not a method) that may be changed to point to

  // a custom error handler.

  Usage func()

  name string // FlagSet的名字。CommandLine 給的是 os.Args[0]

  parsed bool // 是否執行過Parse()

  actual map[string]*Flag // 存放實際傳遞了的參數(即命令列參數)

  formal map[string]*Flag // 存放所有已定義命令列參數

  args []string // arguments after flags // 開始存放所有參數,最後保留 非flag(non-flag)參數

  exitOnError bool // does the program exit if there's an error?

  errorHandling ErrorHandling // 當解析出錯時,處理錯誤的方式

  output io.Writer // nil means stderr; use out() accessor

}
4)Value介面

// Value is the interface to the dynamic value stored in a flag.
// (The default value is represented as a string.)
type Value interface {
    String() string
    Set(string) error
}
所有參數類型需要實現Value介面,flag包中,為int、float、bool等實現了該介面。藉助該介面,我們可以自訂flag

四、主要類型的方法(包括類型執行個體化)
flag包中主要是FlagSet類型。

1、執行個體化方式
NewFlagSet()用於執行個體化FlagSet。預定義的FlagSet執行個體 CommandLine 的定義方式:

// The default set of command-line flags, parsed from os.Args.
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
可見,預設的FlagSet執行個體在解析出錯時會退出程式。

由於FlagSet中的欄位沒有export,其他方式獲得FlagSet執行個體後,比如:FlagSet{}或new(FlagSet),應該調用Init()方法,初始化name和errorHandling。

2、定義flag參數方法
這一序列的方法都有兩種形式,在一開始已經說了兩種方式的區別。這些方法用於定義某一類型的flag參數。

3、解析參數(Parse)
func (f *FlagSet) Parse(arguments []string) error
從參數列表中解析定義的flag。參數arguments不包括命令名,即應該是os.Args[1:]。
事實上,flag.Parse()函數就是這麼做的:
// Parse parses the command-line flags from os.Args[1:].  Must be called
// after all flags are defined and before flags are accessed by the program.
func Parse() {
    // Ignore errors; CommandLine is set for ExitOnError.
    CommandLine.Parse(os.Args[1:])
}
該方法應該在flag參數定義後而具體參數值被訪問前調用。

如果提供了-help參數(命令中給了)但沒有定義(代碼中),該方法返回ErrHelp錯誤。預設的CommandLine,在Parse出錯時會退出(ExitOnError)

為了更深入的理解,我們看一下Parse(arguments []string)的源碼:

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

}
真正解析參數的方法是非匯出方法 parseOne。

結合parseOne方法,我們來解釋non-flag以及包文檔中的這句話:

Flag parsing stops just before the first non-flag argument (“-” is a non-flag argument) or after the terminator “–“.

我們需要瞭解解析什麼時候停止

根據Parse()中for迴圈終止的條件(不考慮解析出錯),我們知道,當parseOne返回false, nil時,Parse解析終止。正常解析完成我們不考慮。看一下parseOne的源碼發現,有兩處會返回false, nil。

1)第一個non-flag參數

s := f.args[0]
if len(s) == 0 || s[0] != '-' || len(s) == 1 {
    return false, nil
}
也就是,當遇到單獨的一個”-“或不是”-“開始時,會停止解析。比如:

./autogo – -f或./autogo build -f

這兩種情況,-f都不會被正確解析。像該例子中的”-“或build(以及之後的參數),我們稱之為non-flag參數

2)兩個連續的”–“

if s[1] == '-' {
   num_minuses++
   if len(s) == 2 { // "--" terminates the flags
       f.args = f.args[1:]
       return false, nil
   }
}
也就是,當遇到連續的兩個”-“時,解析停止。

說明:這裡說的”-“和”–“,位置和”-f”這種的一樣。也就是說,下面這種情況並不是這裡說的:

./autogo -f —

這裡的”–“會被當成是f的值

parseOne方法中接下來是處理-flag=x,然後是-flag(bool類型)(這裡對bool進行了特殊處理),接著是-flag x這種形式,最後,將解析成功的Flag執行個體存入FlagSet的actual map中。

另外,在parseOne中有這麼一句:

1 f.args = f.args[1:]
也就是說,每執行成功一次parseOne,f.args會少一個。所以,FlagSet中的args最後留下來的就是所有non-flag參數。

4、Arg(i int)和Args()、NArg()、NFlag()
Arg(i int)和Args()這兩個方法就是擷取non-flag參數的;NArg()獲得non-flag個數;NFlag()獲得FlagSet中actual長度(即被設定了的參數個數)。

5、Visit/VisitAll
這兩個函數分別用於訪問FlatSet的actual和formal中的Flag,而具體的訪問方式由調用者決定。

6、PrintDefaults()
列印所有已定義參數的預設值(調用VisitAll),預設輸出到標準錯誤,除非指定了FlagSet的output(通過SetOutput()設定)

7、Set(name, value string)
設定某個flag的值(通過Flag的name)

五、總結
使用建議:雖然上面講了那麼多,一般來說,我們只簡單的定義flag,然後parse,就如同開始的例子一樣。

如果項目需要複雜或更進階的命令列解析方式,可以試試goptions包

如果想要像go工具那樣的多命令(子命令)處理方式,可以試試command

 

更新:

2017-10-14:複雜的命令解析,推薦 https://github.com/urfave/cli 或者 https://github.com/spf13/cobra

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.