Go 命令行解析 flag 包之快速上手
- 2019 年 11 月 27 日
- 筆記
本篇文章是 Go 標準庫 flag 包的快速上手篇。
概述
開發一個命令行工具,視複雜程度,一般要選擇一個合適的命令行解析庫,簡單的需求用 Go 標準庫 flag 就夠了,flag 的使用非常簡單。
當然,除了標準庫 flag 外,也有不少的第三方庫。比如,為了替代 flag 而生的 pflag,它支援 POSIX 風格的命令行解析。關於 POSIX 風格,本文末尾有個簡單的介紹。
更多與命令行處理相關的庫,可以打開 awesome-go#command-line 命令行一節查看,star 最多的是 spf13/cobra 和 urfave/cli ,與 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 風格?可以查看這篇文檔 命令參數語法。它同時提供了長短選項的標準。
要明白的是,標準終究只是標準,很多命令其實並不遵循它。但自己在設計命令行規範的時候,最好還是要有一套標準,而參考最統一的標準肯定是沒錯的。
總結
本文介紹了 Go 中 flag 包的使用,一般的場景已經足夠使用了。最後,簡單地談了一個比較趣味性的話題,命令行的風格,是否有種感覺,程式設計師之間的門派之爭真是無處不在。