本篇文章是 Go 標準庫 flag 包的快速上手篇。
概述
開發一個命令行工具,視複雜程度,一般要選擇一個合適的命令行解析庫,簡單的需求用 Go 標準庫 flag 就夠了,flag 的使用非常簡單。
當然,除了標準庫 flag 外,也有不少的第三方庫。比如,為了替代 flag 而生的 pflag[1],它支持 POSIX 風格的命令行解析。關於 POSIX 風格,本文末尾有個簡單的介紹。
更多與命令行處理相關的庫,可以打開 awesome-go#command-line[2] 命令行一節查看,star 最多的是 spf13/cobra[3] 和 urfave/cli[4] ,與 flag / pflag 相比,它們更加複雜,是一個完全的全功能的框架。
有興趣都可以瞭解下。
目標案例
迴歸主題,繼續介紹 flag 吧。通過案例介紹包的使用會比較直觀。
舉一個例子說明吧。假設,現在要開發一個 Go 語言環境的版本管理工具,gvg(go version management by go)。
命令行的幫助信息如下:
NAME: gvg - go version management by go USAGE: gvg [global options] command [command options] [arguments...] VERSION: 0.0.1 COMMANDS: list list go versions install install a go version info show go version info use select a version uninstall uninstall a go version get get the latest code uninstall uninstall a go version help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help --version, -v print the version
這個命令不僅包含了全局的選項,還有 8 個子命令,部分子命令支持參數和選項。暫時,子命令的選項參數先不列出來了,實現時再看。
接下來,我們試著通過 flag 實現這個效果。本文只介紹 GLOBAL OPTIONS(全局選項)的實現。
如果想了解什麼是 Go 語言環境的版本管理,可以查看 如何靈活地進行 Go 版本管理 一文。
選項表示
最簡單的命令不需要任何參數和選項,複雜一點,要支持參數和選項的配置。gvg 沒有全局參數,或者說全局參數是子命令,全局選項有 --help -h 和 --version -h。
一個選項在 flag 包中用一個 Flag 表示,那 -h 可以用一個 Flag 表示。一個選項通常由幾個部分組成,如名稱、使用說明和默認值。如果將 -h 用代碼表示,如下:
h := flag.Bool("h", false, "show help")
定義了一個布爾類型的 Flag,名為 h,默認值是 false,使用說明為 "show help"。變量 h 是一個布爾型的指針,通過它可以取出命令行傳入的值。
除了使用 flag.Bool,還可以使用另外一種方式定義一個 Flag。我們可以用這種方式定義 -v 選項。
代碼如下:
var v bool flag.BoolVar(&v, "v", false, "print the version")
最後的三個參數含義與 flag.Bool 相同,主要區別在值的獲取方式,flag.BoolVar 是通過將變量地址傳入獲取值。從經驗來看,第二種方式使用的較多,或許因為第一種方式會發生變量逃逸。
更多類型
除了布爾類型,Flag 的類型還有整數(int、int64、uint、uint64)、浮點數(float64)、字符串(string)和時長(time.Duration)。
假設 gvg 的案例中,支持配置文件選項 --config-path。實現代碼如下:
var configPath flag.StringVar(&configPath, "config-path", "", "config file path")
通過 StringVar 定義了新的 Flag。使用方式與 BoolVar 相同,最後的三個參數分別是選項名稱、默認值和使用說明。
雖然 flag 支持的內置類型並不多,但已經滿足大部分需求了。如果有自定義的需求,也可以擴展新的類型實現,這部分內容下篇介紹。
長短選項
現在已經完成了 -h 和 -v 兩個選項,但目標是 -v --version 和 -h --help,即同時支持長短選項。
一個 Flag 應該有長短兩種形式,但 flag 包並不支持這種風格,需要曲線救國才能實現。(注:本文開開頭提到的 pflag 支持。)
這裡以 -v --version 為例,代碼如下:
flag.BoolVar(&v, "v", false, "print the version") flag.BoolVar(&v, "version", false, "print the version")
定義了兩個 Flag,同時綁定到了一個變量上。這種效果只能用 flag.BoolVar 方式定義新的 Flag,flag.Bool 沒辦法做到將同一個變量同時綁定兩個 Flag。
但其實這種也有缺點,先不說了,後面介紹幫助信息打印時就明白了。
命令行解析
定義好所有 Flag,還需要一步解析才能拿到正確的結果。這一步非常簡單,調用 flag.Parse() 即可。
如下是完整的代碼:
package main var h *bool var v bool func init() { flag.BoolVar(&h, "h", false, "show help") flag.BoolVar(&h, "help", false, "show help") flag.BoolVar(&v, "v", false, "print the version") flag.BoolVar(&v, "version", false, "print the version") } func main() { flag.Parse() fmt.Println("version", v) fmt.Println("help", h) }
現在就可以將它編譯為 gvg 命令了。
使用命令
在正式使用命令前,先介紹下 flag 的語法。官方文檔說明,命令行中 flag 選項的使用語法有如下幾種形式。
-flag -flag=x -flag x // 非布爾類型才支持這種方式
但其實,-- 也是支持的。因此,上面才可以實現 --version 的曲線救國。
使用下這個命令,將 help 設置為 false 和 version 設置為 true。我儘量把所有可能的寫法都列出來。
$ gvg -v $ gvg -version -h=false # 單個 - ,即 -version 支持 $ gvg --version=true --help=false $ gvg --version=1 --help=0 $ gvg --version=t --help=f $ gvg --version=T --help=F $ gvg --version true --help true # 寫法錯誤,因為無法識別出是 bool 值,還是參數或子命令 $ gvg -vh # 不支持這種風格
執行命令,輸出結果:
version true help false
到這裡,flag 的快速入門就介紹完了。參數留在子命令的時候介紹。
命令行風格
由於一些歷史原因,Unix 出現過很多不同的分支,命令行的風格也因此有很多標準,比如:
- Unix 風格,選項採用單 - 加一個字母,比如 -v,短選項就是它,優點是足夠簡潔;
- BSD 風格,選項沒有 -,沒有任何的前綴,不知道有參數的情況怎麼處理,沒有研究;
- GNU 風格,採用 --,如 --version,長選項,擴展性好,但是要多打幾個字母;
在網上找到一個搞笑漫畫。
查看系統進程有兩種寫法, ps aux(BSD 風格) 和 ps -elf(Unix 風格)。之前,我一直很鬱悶為什麼有這個區別。現在算是明白了。哈哈。
POSIX 的命令行風格算是取長補短的集合吧。什麼是 POSIX 風格?可以查看這篇文檔 命令參數語法[5]。它同時提供了長短選項的標準。
要明白的是,標準終究只是標準,很多命令其實並不遵循它。但自己在設計命令行規範的時候,最好還是要有一套標準,而參考最統一的標準肯定是沒錯的。
總結
本文介紹了 Go 中 flag 包的使用,一般的場景已經足夠使用了。最後,簡單地談了一個比較趣味性的話題,命令行的風格,是否有種感覺,程序員之間的門派之爭真是無處不在。
參考資料
[1]
pflag: github.com/spf13/pflag
[2]
awesome-go#command-line: https://awesome-go.com/#command-line
[3]
spf13/cobra: https://github.com/spf13/cobra
[4]
urfave/cli: https://github.com/urfave/cli
[5]
命令參數語法: http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html