golang-flag - 命令行參數解析


flag - 命令行參數解析

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

注:區分幾個概念

  1. 命令行參數(或參數):是指運行程序提供的參數
  2. 已定義命令行參數:是指程序中通過flag.Xxx等這種形式定義了的參數
  3. 非flag(non-flag)命令行參數(或保留的命令行參數):后文解釋

1.1. 使用示例

我們以 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

 

仔細理解以上例子,如果有不理解的,看完下文的講解再回過頭來看。

1.2. flag 包概述

flag 包實現了命令行參數的解析。

1.2.1. 定義 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") 

1.2.2. 自定義 Value

另外,還可以創建自定義 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 這種非基本類型的支持,使用的就是類似這樣的方式。

1.2.3. 解析 flag

在所有的 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 能解析的類型。

1.3. 類型和函數

在看類型和函數之前,先看一下變量。

ErrHelp:該錯誤類型用於當命令行指定了 ·-help` 參數但沒有定義時。

Usage:這是一個函數,用於輸出所有定義了的命令行參數和幫助信息(usage message)。一般,當命令行參數解析出錯時,該函數會被調用。我們可以指定自己的 Usage 函數,即:flag.Usage = func(){}

1.3.1. 函數

go標准庫中,經常這么做:

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

在 flag 包使用了有類似的方法,比如 CommandLine 實例,只不過 flag 進行了進一步封裝:將 FlagSet 的方法都重新定義了一遍,也就是提供了一序列函數,而函數中只是簡單的調用已經實例化好了的 FlagSet 實例:CommandLine 的方法。這樣,使用者是這么調用:flag.Parse() 而不是 flag. CommandLine.Parse()。(Go 1.2 起,將 CommandLine 導出,之前是非導出的)

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

1.3.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 的狀態。

比如,對於命令:./nginx -c /etc/nginx.conf,相應代碼是:

flag.StringVar(&c, "c", "conf/nginx.conf", "set configuration `file`")

則該 Flag 實例(可以通過 flag.Lookup("c") 獲得)相應各個字段的值為:

&Flag{
    Name: c,
    Usage: set configuration file,
    Value: /etc/nginx.conf,
    DefValue: conf/nginx.conf,
}

 

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。(上文已經給了具體的例子)

1.4. 主要類型的方法(包括類型實例化)

flag 包中主要是 FlagSet 類型。

1.4.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,否則 name 為空,errorHandling 為 ContinueOnError。

1.4.2. 定義 flag 參數的方法

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

1.4.3. 解析參數(Parse)

func (f *FlagSet) Parse(arguments []string) error
從參數列表中解析定義的 flag。方法參數 arguments 不包括命令名,即應該是os.Args[1:]。事實

 

從參數列表中解析定義的 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
}

 

也就是,當遇到單獨的一個"-"或不是"-"開始時,會停止解析。比如:

./nginx - -c 或 ./nginx build -c

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

2)兩個連續的"--"

if s[1] == '-' { num_minuses++ if len(s) == 2 { // "--" terminates the flags f.args = f.args[1:] return false, nil } } 

也就是,當遇到連續的兩個"-"時,解析停止。

說明:這里說的"-"和"--",位置和"-c"這種的一樣。也就是說,下面這種情況並不是這里說的:

./nginx -c --

這里的"--"會被當成是 c 的值

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

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

f.args = f.args[1:]

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

1.4.4. Arg(i int) 和 Args()、NArg()、NFlag()

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

1.4.5. Visit/VisitAll

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

1.4.6. PrintDefaults()

打印所有已定義參數的默認值(調用 VisitAll 實現),默認輸出到標准錯誤,除非指定了 FlagSet 的 output(通過SetOutput() 設置)

1.4.7. Set(name, value string)

設置某個 flag 的值(通過 name 查找到對應的 Flag)

1.5. 總結

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

如果項目需要復雜或更高級的命令行解析方式,可以使用 https://github.com/urfave/cli 或者 https://github.com/spf13/cobra 這兩個強大的庫。

 

go語言中文網


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM