Go 命令行參數解析工具 pflag 使用

在使用 Go 進行開發的過程中,命令行參數解析是我們經常遇到的需求。儘管 Go 標準庫提供了 flag 包用於實現命令行參數解析,但只能滿足基本需要,不支持高級特性。於是 Go 社區中出現了一個叫 pflag 的第三方包,功能更加全面且足夠強大。在本文中,我們將學習並掌握如何使用 pflag。

特點

pflag 作爲 Go 內置 flag 包的替代品,具有如下特點:

使用

基本用法

我們可以像使用 Go 標準庫中的 flag 包一樣使用 pflag。

package main

import (
 "fmt"

 "github.com/spf13/pflag"
)

type host struct {
 value string
}

func (h *host) String() string {
 return h.value
}

func (h *host) Set(v string) error {
 h.value = v
 return nil
}

func (h *host) Type() string {
 return "host"
}

func main() {
 var ip *int = pflag.Int("ip", 1234, "help message for ip")

 var port int
 pflag.IntVar(&port, "port", 8080, "help message for port")

 var h host
 pflag.Var(&h, "host", "help message for host")

 // 解析命令行參數
 pflag.Parse()

 fmt.Printf("ip: %d\n", *ip)
 fmt.Printf("port: %d\n", port)
 fmt.Printf("host: %+v\n", h)

 fmt.Printf("NFlag: %v\n", pflag.NFlag()) // 返回已設置的命令行標誌個數
 fmt.Printf("NArg: %v\n", pflag.NArg())   // 返回處理完標誌後剩餘的參數個數
 fmt.Printf("Args: %v\n", pflag.Args())   // 返回處理完標誌後剩餘的參數列表
 fmt.Printf("Arg(1): %v\n", pflag.Arg(1)) // 返回處理完標誌後剩餘的參數列表中第 i 項
}

以上示例演示的 pflag 用法跟 flag 包用法一致,可以做到二者無縫替換。

示例分別使用 pflag.Int()pflag.IntVar()pflag.Var() 三種不同方式來聲明標誌。其中 ipport 都是 int 類型標誌,host 標誌則爲自定義的 host 類型,它實現了 pflag.Value 接口,通過實現接口類型,標誌能夠支持任意類型,增加靈活性。

通過 --help/-h 參數查看命令行程序使用幫助:

$ go run main.go --help                      
Usage of ./main:
      --host host   help message for host
      --ip int      help message for ip (default 1234)
      --port int    help message for port (default 8080)
pflag: help requested

可以發現,幫助信息中的標誌位置是經過重新排序的,並不是標誌定義的順序。

與 flag 包不同的是,pflag 包參數定界符是兩個 -,而不是一個 -,在 pflag 中 --- 具有不同含義,這點稍後會進行介紹。

ip 標誌的默認參數爲 1234port 標誌的默認參數爲 8080

注意:在有些終端下執行程序退出後,還會多打印一行 exit status 2,這並不意味着程序沒有正常退出,而是因爲 --help 意圖就是用來查看使用幫助,所以程序在打印使用幫助信息後,主動調用 os.Exit(2) 退出了。

通過如下方式使用命令行程序:

$ go run main.go --ip 1 x y --host localhost a b 
ip: 1
port: 8080
host: {value:localhost}
NFlag: 2
NArg: 4
Args: [x y a b]
Arg(1): y

ip 標誌的默認值已被命令行參數 1 所覆蓋,由於沒有傳遞 port 標誌,所以打印結果爲默認值 8080host 標誌的值也能夠被正常打印。

還有 4 個非選項參數數 xyab 也都被 pflag 識別並記錄了下來。這點比 flag 要強大,在 flag 包中,非選項參數數只能寫在所有命令行參數最後,xy 出現在這裏程序是會報錯的。

進階用法

除了像 flag 包一樣的用法,pflag 還支持一些獨有的用法,以下是用法示例。

package main

import (
 "fmt"
 "os"

 "github.com/spf13/pflag"
)

type host struct {
 value string
}

func (h *host) String() string {
 return h.value
}

func (h *host) Set(v string) error {
 h.value = v
 return nil
}

func (h *host) Type() string {
 return "host"
}

func main() {
 flagset := pflag.NewFlagSet("test", pflag.ExitOnError)

 var ip = flagset.IntP("ip", "i", 1234, "help message for ip")

 var boolVar bool
 flagset.BoolVarP(&boolVar, "boolVar", "b", true, "help message for boolVar")

 var h host
 flagset.VarP(&h, "host", "H", "help message for host")

 flagset.SortFlags = false

 flagset.Parse(os.Args[1:])

 fmt.Printf("ip: %d\n", *ip)
 fmt.Printf("boolVar: %t\n", boolVar)
 fmt.Printf("host: %+v\n", h)

 i, err := flagset.GetInt("ip")
 fmt.Printf("i: %d, err: %v\n", i, err)
}

首先我們通過 pflag.NewFlagSet 自定義了 FlagSet 對象 flagset,之後的標誌定義和解析都通過 flagset 來完成。

前文示例中 pflag.Int() 這種用法,實際上使用的是全局 FlagSet 對象 CommandLineCommandLine 定義如下:

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

現在同樣使用三種不同方式來聲明標誌,分別爲 flagset.IntP()flagset.BoolVarP()flagset.VarP()。不難發現,這三個方法的命名結尾都多了一個 P,它們的能力也得以升級,三個方法都多了一個 shorthand string 參數(flagset.IntP 的第 2 個參數,flagset.BoolVarPflagset.VarP 的第 3 個參數)用來設置簡短標誌。

從聲明標誌的方法名中我們能夠總結出一些規律:

一個完整標誌在命令行傳參時使用的分界符爲 --,而一個簡短標誌的分界符則爲 -

flagset.SortFlags = false 作用是禁止打印幫助信息時對標誌進行重排序。

示例最後,使用 flagset.GetInt() 獲取參數的值。

通過 --help/-h 參數查看命令行程序使用幫助:

$ go run main.go --help
Usage of test:
  -i, --ip int      help message for ip (default 1234)
  -b, --boolVar     help message for boolVar (default true)
  -H, --host host   help message for host
pflag: help requested

這次的幫助信息中,標誌順序沒有被改變,就是聲明的順序。

每一個標誌都會對應一個簡短標誌,如 -b--boolVar 是等價的,可以更加方便的設置參數。

指定如下命令行參數運行示例:

$ go run main.go --ip 1 -H localhost --boolVar=false
ip: 1
boolVar: false
host: {value:localhost}
i: 1, err: <nil>

通過 --ip 1 使用完整標誌指定 ip 參數值。

通過 -H localhost 使用簡短標誌指定 host 參數值。

布爾類型的標誌指定參數 --boolVar=false 需要使用等號 = 而非空格。

命令行標誌語法

命令行標誌遵循如下語法:

Eec2OZ

標誌解析在終止符 -- 之後停止。

整數標誌接受 1234、0664、0x1234,並且可能爲負數。

布爾標誌接受 1, 0, t, f, true, false, TRUE, FALSE, True, False。

Duration 標誌接受任何對 time.ParseDuration 有效的輸入。

標誌名 Normalize

藉助 pflag.NormalizedName 我們能夠給標誌起一個或多個別名、規範化標誌名等。

package main

import (
 "fmt"
 "os"
 "strings"

 "github.com/spf13/pflag"
)

func normalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
 // alias
 switch name {
 case "old-flag-name":
  name = "new-flag-name"
  break
 }

 // --my-flag == --my_flag == --my.flag
 from := []string{"-", "_"}
 to := "."
 for _, sep := range from {
  name = strings.Replace(name, sep, to, -1)
 }
 return pflag.NormalizedName(name)
}

func main() {
 flagset := pflag.NewFlagSet("test", pflag.ExitOnError)

 var ip = flagset.IntP("new-flag-name", "i", 1234, "help message for new-flag-name")
 var myFlag = flagset.IntP("my-flag", "m", 1234, "help message for my-flag")

 flagset.SetNormalizeFunc(normalizeFunc)
 flagset.Parse(os.Args[1:])

 fmt.Printf("ip: %d\n", *ip)
 fmt.Printf("myFlag: %d\n", *myFlag)
}

要使用 pflag.NormalizedName,我們需要創建一個函數 normalizeFunc,然後將其通過 flagset.SetNormalizeFunc(normalizeFunc) 注入到 flagset 使其生效。

normalizeFunc 函數中,我們給 new-flag-name 標誌起了一個別名 old-flag-name

另外,還對標誌名進行了規範化處理,帶有 -_ 分割符的標誌名,會統一規範化成以 . 作爲分隔符的標誌名。

使用示例如下:

$ go run pflag.go --old-flag-name 2 --my-flag 200
ip: 2
myFlag: 200

$ go run pflag.go --new-flag-name 3 --my_flag 300
ip: 3
myFlag: 300

NoOptDefVal

NoOptDefValno option default values 的簡寫。

創建標誌後,可以爲標誌設置 NoOptDefVal 屬性,如果標誌具有 NoOptDefVal 屬性並且在命令行上設置了標誌而沒有參數選項,則標誌將設置爲 NoOptDefVal 指定的值。

如下示例:

var ip = flag.IntP("flagname""f", 1234, "help message")
flag.Lookup("flagname").NoOptDefVal = "4321"

不同參數結果如下:

YUlQcT

棄用 / 隱藏標誌

使用 flags.MarkDeprecated 可以棄用一個標誌,使用 flags.MarkShorthandDeprecated 可以棄用一個簡短標誌,使用 flags.MarkHidden 可以隱藏一個標誌。

package main

import (
 "fmt"
 "os"

 "github.com/spf13/pflag"
)

func main() {
 flags := pflag.NewFlagSet("test", pflag.ExitOnError)

 var ip = flags.IntP("ip", "i", 1234, "help message for ip")

 var boolVar bool
 flags.BoolVarP(&boolVar, "boolVar", "b", true, "help message for boolVar")

 var h string
 flags.StringVarP(&h, "host", "H", "127.0.0.1", "help message for host")

 // 棄用標誌
 flags.MarkDeprecated("ip", "deprecated")
 flags.MarkShorthandDeprecated("boolVar", "please use --boolVar only")

 // 隱藏標誌
 flags.MarkHidden("host")

 flags.Parse(os.Args[1:])

 fmt.Printf("ip: %d\n", *ip)
 fmt.Printf("boolVar: %t\n", boolVar)
 fmt.Printf("host: %+v\n", h)
}

查看使用幫助:

$ go run main.go -h                                 
Usage of test:
      --boolVar   help message for boolVar (default true)
pflag: help requested

從打印結果可以發現,棄用標誌 ip 時,其對應的簡短標誌 i 也會跟着被棄用;棄用 boolVar 所對應的簡短標誌 b 時,boolVar 標誌會被保留;host 標誌則完全被隱藏。

指定如下命令行參數運行示例:

$ go run main.go --ip 1 --boolVar=false -H localhost
Flag --ip has been deprecated, deprecated
ip: 1
boolVar: false
host: localhost

打印信息中會提示用戶 ip 標誌已經棄用,不過使用 --ip 1 指定的參數值依然能夠生效。

隱藏的 host 標誌使用 -H localhost 指定參數值同樣能夠生效。

指定如下命令行參數運行示例:

$ go run main.go -i 1 -b=false --host localhost
Flag --ip has been deprecated, deprecated
Flag shorthand -b has been deprecated, please use --boolVar only
ip: 1
boolVar: false
host: localhost

打印信息中增加了一條簡短標誌 -b 已被棄用的提示,指定參數值依然生效。

對於棄用的 ip 標誌,使用簡短標誌形式傳慘 -i 1 同樣生效。

支持 flag 類型

由於 pflag 對 flag 包兼容,所以可以在一個程序中混用二者:

package main

import (
 "flag"
 "fmt"

 "github.com/spf13/pflag"
)

func main() {
 var ip *int = pflag.Int("ip", 1234, "help message for ip")
 var port *int = flag.Int("port", 80, "help message for port")

 pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
 pflag.Parse()

 fmt.Printf("ip: %d\n", *ip)
 fmt.Printf("port: %d\n", *port)
}

其中,ip 標誌是使用 pflag.Int() 聲明的,port 標誌則是使用 flag.Int() 聲明的。只需要通過 AddGoFlagSet 方法將 flag.CommandLine 註冊到 pflag 中,那麼 pflag 就可以使用 flag 中聲明的標誌集合了。

運行示例結果如下:

$ go run main.go --ip 10 --port 8000
ip: 10
port: 8000

總結

本文主要介紹了 Go 第三方標誌包 pflag 的特點及用法。

首先介紹了 pflag 的基本使用方法,包括聲明標誌、解析命令行參數、獲取標誌值等。接着介紹了 pflag 的進階用法,例如自定義 FlagSet、使用 pflag.<Type>P 方法來支持簡短標誌。之後又對命令行標誌語法進行了講解,對於布爾值、非布爾值和簡短標誌,都有各自不同的語法。我們還講解了如何藉助 pflag.NormalizedName 給標誌起一個或多個別名、規範化標誌名。然後介紹了 NoOptDefVal 的作用和如何棄用 / 隱藏標誌。最後通過示例演示瞭如何在一個程序中混用 flag 和 pflag。

彩蛋:不知道你有沒有發現,示例中的 ip 標誌的名稱其實代表的是 int pointer 而非 Internet Protocol Addressip 標誌源自官方示例,不過我順勢而爲又聲明瞭 porthost 標誌,算是一個程序中的諧音梗 :)。

參考

pflag 源碼: https://github.com/spf13/pflag 

pflag 文檔: https://pkg.go.dev/github.com/spf13/pflag 

程序參數語法約定: https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html

聯繫我

微信:jianghushinian

郵箱:jianghushinian007@outlook.com

博客地址:https://jianghushinian.cn

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/67JnB4bC3xOhmO-AJzoStw