cmdr
是另外一個命令行參數處理器(Golang)。react
Golang 本身帶有 flags
進行命令行參數處理,算是便利的,然而和 Google 一向的作法相同,很是獨,很是反人類。linux
在計算機人機交互界面的歷史上,命令行的交互方式只有一種是貫穿始終,獲得傳承和延續的,那就是 getopt
以及 getopt_long
。提及 getopt
來也能夠講述一個怪長的故事,然而本文不作此打算。不管如何,你須要知道的就是,getopt及其交互界面已是POSIX的一部分,一個卓有成效的程序員、開發者、科學家,或者計算機從業者,對於這個界面都已是訓練有素,無需成本了。你可能在用着它,但你或許只是沒有意識到它的存在而已。GNU的大部分命令行小刀都採用了這樣的界面,因此,例如tar, gwk, gzip, ls, rm, …,以及沒法列舉的那些工具都是這樣的界面。git
因此,自行其是,本身搞一套,並不是不能夠。但我能夠不買帳。程序員
那麼,這並不是我獨自一人的自賞。咱們只須要知道,在 Golang 的開源圈子裏,已經有了數十種 getopt-like 的復刻本,用覺得 Golang 開發的應用程序提供更好的命令行界面。這裏面不乏 viper/cobra, cli 那樣的巨做,也有一些小巧精幹的實現。github
cmdr
也是這麼一個 getopt-like 的實現。和已有的其它實現不一樣之處在於,cmdr基本上原樣複製了 getopt 的表現。也就是說,一個典型的 Unix/Linux 應用程序,例如 cp,mv 等等,是怎麼作的,那麼基於 cmdr
的應用程序也就是怎麼作的。這裏講的固然是關於命令行參數怎麼被解釋的問題,而非應用程序的具體邏輯。golang
讓咱們來看看都有哪些具體方面。docker
POSIX 表示可移植操做系統接口(英語:Portable Operating System Interface,縮寫爲POSIX)是 IEEE(電氣和電子工程師協會,Institute of Electrical and Electronics Engineers)爲要在各類UNIX操做系統上運行軟件,而定義API的一系列互相關聯的標準的總稱,其正式稱呼爲IEEE Std 1003,而國際標準名稱爲ISO/IEC 9945。此標準源於一個大約開始於1985年的項目。POSIX這個名稱是由 理查德·斯托曼(RMS)應IEEE的要求而提議的一個易於記憶的名稱。它基本上是Portable Operating System Interface(可移植操做系統接口)的縮寫,而X則代表其對Unix API的傳承。 電氣和電子工程師協會(Institute of Electrical and Electronics Engineers,IEEE)最初開發 POSIX 標準,是爲了提升 UNIX 環境下應用程序的可移植性。然而,POSIX 並不侷限於 UNIX。許多其它的操做系統,例如 DEC OpenVMS 和 Microsoft Windows NT,都支持 POSIX 標準。npm
下面是 POSIX 標準中關於程序名、參數的約定:bash
foo -a -b -c ---->foo -abc
)myprog -u "arnold,joe,jane"
。這種狀況下,須要本身解決這些參數的分離問題。‘--'
指明全部參數都結束了,其後任何參數都認爲是操做數。如下對 getopt 以及 getopt_long 提供的界面進行描述,cmdr
具有相同的能力。app
在如下的行文中,短參數
和短選項
是等同的概念,其它詞彙也相似如此,再也不贅述。
單個短橫線引導的單個字符的參數,被稱爲短參數。例如:-v
,-d
,等等。有的時候,短參數也可能有兩個字符甚至更多個字母。然而,短參數的用意就在於縮略,所以多字符的短參數不多見,且一般被用於組合,更像是典型的單字符短參數後綴以一個取值。例如 rar 的選項中有 -ep, -ep1, -ep3:
ep Exclude paths from names ep1 Exclude base directory from names ep3 Expand paths to full including the drive letter
然而在實現其處理器時,咱們能夠提供 -ep<n>
的處理器就夠了,因此你仍然能夠將其視爲 -ep
短參數的變形。
兩個短橫線引導的多個字符的參數,被稱爲長參數。例如:—debug
,--version
等等。
通常來講,長參數更具有描述性,一般使用單詞、詞組來構成長參數。例如 docker 的子命令 docker checkpoint create
:
$ docker checkpoint create --help Usage: docker checkpoint create [OPTIONS] CONTAINER CHECKPOINT Create a checkpoint from a running container Options: --checkpoint-dir string Use a custom checkpoint storage directory --leave-running Leave the container running after checkpoint
每條命令或參數選項能夠被一段文件以描述。
不管長短參數,能夠以任意順序出現,也能夠任意出現屢次。對於屢次出現的參數,通常來講是最後一次出現的爲準,以前出現過的會被覆蓋。
例如命令行:-1 -a yy -a dd -a cc
,則對於參數a來講,其有效值爲 」cc「,此前出現的都被覆蓋了。
對於getopt不帶值的參數,例如 "1abc"
,如下的命令行都是有效的:
-1 -a -b -c
-abc1
-ac -1b
順序是不敏感的,組合是任意的。
getopt的定義是參數後加一個冒號,例如 「1a:b::"
中的參數 a
,對它你須要指定命令行形如 -1 -a xxx
。
getopt的定義是參數後加兩個冒號,例如 「1a:b::"
中的參數 b
,對它你須要指定命令行形如 -1 -b
或者 -1 -bvalue
。
以 docker 的子命令 checkpoint 爲例:
事實上,命令與子命令是沒有區別的,若是有必要,能夠創建任意多級的命令和子命令嵌套層次。不過在實際的 Command-Line UI 設計中,超過4層的子命令嵌套都是極少數,由於這也會給使用工具的人帶來麻煩。
在現代的命令行界面中,自動完成(Shell Completion)已是一個關鍵性特性了。流行的命令行界面例如 Bash、Zsh、Fish 都提供了自動完成的特性。一般一個應用程序須要面向這個Shells 提供配套的自動完成腳本,從而得到自動完成能力。
一個已經支持自動完成的應用程序的命令行輸入多是這樣子的:
docker 的 自動完成
docker-completion.gif
docker 在 zsh 中的自動完成。能夠注意到 zsh 的 TAB 按鍵次數更簡練,並且列表選擇界面也更有效和更具備提示性。固然,zsh的自動完成也存在一些bug,例如一級命令列表超出終端屏幕可視行數時列表選擇界面就被破碎掉了。
docker-zsh-completion.gif
cmdr
的使用方法儘量簡單化了,接下來咱們作一個簡明的介紹。
一個簡單的入口能夠這樣:
package main import ( "fmt" "github.com/hedzr/cmdr" ) func main() { // logrus.SetLevel(logrus.DebugLevel) // logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true,}) // 可選的四個選項: cmdr.EnableVersionCommands = true cmdr.EnableVerboseCommands = true cmdr.EnableHelpCommands = true cmdr.EnableGenerateCommands = true if err := cmdr.Exec(rootCmd); err != nil { fmt.Printf("Error: %v", err) // or log, logrus } } var( rootCmd = &cmdr.RootCommand{ Command: cmdr.Command{ BaseOpt: cmdr.BaseOpt{ Name: "short", Flags: []*cmdr.Flag{ }, }, SubCommands: []*cmdr.Command{ serverCommands, // msCommands, }, }, AppName: "short", Version: cmdr.Version, VersionInt: cmdr.VersionInt, Copyright: "austr is an effective devops tool", Author: "Your Name <yourmail@gmail.com>", } serverCommands = &cmdr.Command{ BaseOpt: cmdr.BaseOpt{ Short: "s", Full: "server", Aliases: []string{"serve", "svr",}, Description: "server ops: for linux service/daemon.", Flags: []*cmdr.Flag{ { BaseOpt: cmdr.BaseOpt{ Short: "f", Full: "foreground", Aliases: []string{"fg",}, Description: "running at foreground", }, }, }, }, SubCommands: []*cmdr.Command{ { BaseOpt: cmdr.BaseOpt{ Short: "s", Full: "start", Aliases: []string{"run", "startup",}, Description: "startup this system service/daemon.", Action: func(cmd *cmdr.Command, args []string) (err error) { return }, }, }, { BaseOpt: cmdr.BaseOpt{ Short: "t", Full: "stop", Aliases: []string{"stp", "halt", "pause",}, Description: "stop this system service/daemon.", }, }, { BaseOpt: cmdr.BaseOpt{ Short: "r", Full: "restart", Aliases: []string{"reload",}, Description: "restart this system service/daemon.", }, }, { BaseOpt: cmdr.BaseOpt{ Full: "status", Aliases: []string{"st",}, Description: "display its running status as a system service/daemon.", }, }, { BaseOpt: cmdr.BaseOpt{ Short: "i", Full: "install", Aliases: []string{"setup",}, Description: "install as a system service/daemon.", }, }, { BaseOpt: cmdr.BaseOpt{ Short: "u", Full: "uninstall", Aliases: []string{"remove",}, Description: "remove from a system service/daemon.", }, }, }, } )
能夠看到的是,cmdr.RootCommand 和 cmdr.Command 的區別不大,只是多了應用程序信息的成員字段。而 cmdr.Command 和 cmdr.Flag 的區別也不大,它們都有相同的 BaseOpt 嵌入結構。
所以,定義命令和定義選項是很類似的,而後你須要進行正確的結構嵌套。若是感到嵌套結構迷亂了眼睛,則能夠抽出一個子命令、或者一組子命令到一個獨立的變量中,而後使用引用的方式嵌入到上級命令的恰當位置。
這種抽出的方式也適合於進行類似結構的共享,但要注意引用和深度拷貝的區別。此處不作進一步討論了,總之,若是感到沒有把握,不妨一級一級地老老實實地完成定義,通常的工具開發也不會有太多的嵌套的吧。
言歸正傳,完成了上面的定義以後,就能夠編譯成執行文件運行了(或者用 go run main.go)。你能夠在終端中嘗試使用它:
bin/short bin/short --help bin/short --version bin/short -# bin/short --debug --verbose server --help bin/short svr --help --debug -v bin/short s start -f ~~debug
每條命令(cmdr.Command
)均可以指定 Action
,一個 func 對象。你將要爲命令編寫的業務邏輯就在這裏。
若是有必要的話,一個選項(cmdr.Flag
)也能夠被提供一個 Action
,若是你須要在選項被掃描到時觸發點其餘邏輯的話。
對於命令而言,你能夠提供額外的 PreAction
和 PostAction
,它們分別是在命令的 Action
被執行的先後被調用的。特別是 PreAction
容許返回一個特別的錯誤值 cmdr.ShouldBeStopException
來告訴 cmdr
終止後續處理,因此你能夠有機會避免 命令的 Action 部分被執行。
因爲 RootCommand
也是一個 Command
,因此定義在 RootCommand
中的 PreAction
,postAction
有着特別的處理邏輯:
RootCommand.PreAction
將會在具體 Command.PreAction
執行以前被執行;RootCommand.PostAction
將會在具體 Command.PostAction
執行以後被執行。
這樣的特別邏輯是爲了便於開發者定義本身的前置、退出邏輯。例如一個微服務應該在開始提供服務以前完成註冊中心登記,以及在中止服務時撤銷登記,這些任務適合於在 RootCommand
的 Pre/PostAction
中來作。
~~debug
~~debug
是一個隱藏性的標誌。~~
和 long 參數 是類似的,不過它的不一樣在於,相應的參數的入口不會被創建在 標準名字空間中,所以你須要在頂級名字空間中抽取它的值。
~~debug
有着一個特別的做用,在調試階段,這個選項將會使得正常處理邏輯結束後,附加一段調試性的信息輸出,其中包含 全部有效的選項及其最終值,還包含這些選項的 yaml 文本形式。
一個式樣是:
image-20190514162434050.png
你能夠經過:
bin/wget-demo ~~debug
來查看類似的輸出結果。
我相信這個功能能夠幫助你解決不少問題,沒必要再來猜來猜去的了。
全部的選項值都被放在標準名字空間中,cmdr.RxxtPrefix
定義了標準名字空間的層級,其默認值爲 app
。
這意味着 RootCommand 的 Flags,例如 --version
,能夠用 cmdr.GetBool("app.version")
來抽取其值。相似的,--debug
的抽取語句爲 cmdr.GetBool("app.debug")
。
前面說過~~debug
有點特殊,這樣的不加前綴的選項的值能夠直接抽取:cmdr.GetBool("debug")
。
每一級命令或子命令就會創建一個嵌套的名字空間,其名稱取自命令的 Full
字段,也就是長參數名。所以 bin/short server start -f
的 -f
的抽取語句爲 cmdr.GetBool("app.server.start.foreground")
。
你固然能夠執行不一樣的 cmdr.RxxtPrefix
,例如:
cmdr.RxxtPrefix = []string{"server",} // 等價於使用 」server.xxx" 而不是 「app.xxx」
一個選項的值是能夠多種形態的,但總的來講咱們支持四種數據類型:
更多的類型,咱們暫不直接支持。將來或會予以加強。
可使用環境變量重載去覆蓋命令行參數。
因此:
CMDR_APP_SERVER_START_FOREGROUND=1 bin/short server start
等價於 bin/short server start -f
。
若是你但願使用非 」CMDR_「 的環境變量前綴,你能夠設置 cmdr.EnvPrefix
來自行控制前綴。例如
cmdr.EnvPrefix = []string{ "Rx", "cd", } // 等價於使用 RX_CD_ 前綴
環境變量的優先級較低,若是配置文件或者命令行參數有指定值,則環境變量的設定值就被掩蓋了。
這不符合慣例,咱們考慮在下一版本中解決此問題。
咱們將會實現的優先級爲:defaultValue -> config-file -> env-var -> command-line opts。
默認狀況下,cmdr
自動查看以下文件:
/etc/<appname>/<appname>.yml
/usr/local/etc/<appname>/<appname>.yml
$HOME/.<appname>/<appname>.yml
cmdr
也會自動裝載相應的 conf.d
子目錄中的全部 yaml 文件,並依次載入和覆蓋選項的定義值。所以你能夠切分大型配置文件到多個小文件中,以便於運維部署和管理。
對於開發者來講,cmdr
還會首先檢查項目目錄下的 ./ci/etc/<appname>/<appname>.yml
是否有效並試圖自動加載它及其 conf.d 子目錄。
cmdr
支持 conf.d 文件夾的監視,其中的變化會被傳送給全部註冊的 listeners。關於這個方面的細節,能夠查看:
cmdr.AddOnConfigLoadedListener(c)
cmdr.RemoveOnConfigLoadedListener(c)
cmdr.SetOnConfigLoadedListener(c, enabled)
沒法定製加載位置、沒法忽略加載位置,等等。
其餘的配置文件格式也暫時不支持。
咱們已經實現了一個 wget 的命令行界面復刻版本,可是僅提供小部分命令行參數的處理,由於完整的復刻版本基本上只是一個重複的勞做了,做爲示例咱們已經實現了足夠多的選項,足以說明 cmdr
的能力了。
wget-demo 的幫助屏是這樣的:
image-20190514131624710.png
和 gnu 的 wget 相比較而言,看起來也算是沒有區別了。
wget-demo 的源碼能夠在這裏找到:
https://github.com/hedzr/cmdr...
semver是符合規範的。
關於 semver 的含義能夠查看以下兩個連接,無需多言:
cmdr
是在早前若干個非正式實現的基礎上重寫的一個新的實現,其首要目標就是完徹底全地 Unix/Linux 命令行界面,而不是 golang 風格的、或者其它的部分實現的風格。
getopt
以及 getopt_long
都有本身的參數定義方式,不過在這個方面,cmdr
不打算實現它們的仿真風格,由於那並不方便也不算直觀。
cmdr
盡力作到的是,命令和參數定義完成以後就完成了一切。除此而外,你無需作別的事就能獲得:
conf.d
子目錄,且自動監視其變動目前已經實現的是主體的大部分特性,細節還沒有打磨完美,還須要繼續投入力量進行改善。然而做爲建設的主要目標已經可做爲已達成了。
更多 cmdr
用法,從此繼續進行描述。