基於cmdr
v1.0.3從 golang
flag
遷移到cmdr
java
採用一個新的命令行解釋器框架,最痛苦地莫過於編寫數據結構或者流式定義了。咱們首先回顧一下 cmdr
和其它大多數三方加強命令行解釋器都支持的最典型的兩種命令行界面定義方式,而後再來研究一下 cmdr
新增的最平滑的遷移方案。c++
有的加強工具(例如 cobra,viper)採用結構體數據定義方式來完成界面指定,如同 cmdr
的這樣:git
rootCmd = &cmdr.RootCommand{ Command: cmdr.Command{ BaseOpt: cmdr.BaseOpt{ Name: appName, Description: desc, LongDescription: longDesc, Examples: examples, }, Flags: []*cmdr.Flag{}, SubCommands: []*cmdr.Command{ // generatorCommands, // serverCommands, msCommands, testCommands, { BaseOpt: cmdr.BaseOpt{ Short: "xy", Full: "xy-print", Description: `test terminal control sequences`, Action: func(cmd *cmdr.Command, args []string) (err error) { fmt.Println("\x1b[2J") // clear screen for i, s := range args { fmt.Printf("\x1b[s\x1b[%d;%dH%s\x1b[u", 15+i, 30, s) } return }, }, }, { BaseOpt: cmdr.BaseOpt{ Short: "mx", Full: "mx-test", Description: `test new features`, Action: func(cmd *cmdr.Command, args []string) (err error) { fmt.Printf("*** Got pp: %s\n", cmdr.GetString("app.mx-test.password")) fmt.Printf("*** Got msg: %s\n", cmdr.GetString("app.mx-test.message")) return }, }, Flags: []*cmdr.Flag{ { BaseOpt: cmdr.BaseOpt{ Short: "pp", Full: "password", Description: "the password requesting.", }, DefaultValue: "", ExternalTool: cmdr.ExternalToolPasswordInput, }, { BaseOpt: cmdr.BaseOpt{ Short: "m", Full: "message", Description: "the message requesting.", }, DefaultValue: "", ExternalTool: cmdr.ExternalToolEditor, }, }, }, }, }, AppName: appName, Version: cmdr.Version, VersionInt: cmdr.VersionInt, Copyright: copyright, Author: "xxx <xxx@gmail.com>", } //... More
它的問題在於,若是你有 docker 那樣的較多的子命令以及選項須要安排的話,這個方案會至關難定位,寫起來也很痛苦,改起來更痛苦。github
比結構體數據定義方案更好一點的是採用流式調用鏈方式。它可能長得像這樣:golang
// root root := cmdr.Root(appName, "1.0.1"). Header("fluent - test for cmdr - no version - hedzr"). Description(desc, longDesc). Examples(examples) rootCmd = root.RootCommand() // soundex root.NewSubCommand(). Titles("snd", "soundex", "sndx", "sound"). Description("", "soundex test"). Group("Test"). Action(func(cmd *cmdr.Command, args []string) (err error) { for ix, s := range args { fmt.Printf("%5d. %s => %s\n", ix, s, cmdr.Soundex(s)) } return }) // xy-print root.NewSubCommand(). Titles("xy", "xy-print"). Description("test terminal control sequences", "test terminal control sequences,\nverbose long descriptions here."). Group("Test"). Action(func(cmd *cmdr.Command, args []string) (err error) { fmt.Println("\x1b[2J") // clear screen for i, s := range args { fmt.Printf("\x1b[s\x1b[%d;%dH%s\x1b[u", 15+i, 30, s) } return }) // mx-test mx := root.NewSubCommand(). Titles("mx", "mx-test"). Description("test new features", "test new features,\nverbose long descriptions here."). Group("Test"). Action(func(cmd *cmdr.Command, args []string) (err error) { fmt.Printf("*** Got pp: %s\n", cmdr.GetString("app.mx-test.password")) fmt.Printf("*** Got msg: %s\n", cmdr.GetString("app.mx-test.message")) fmt.Printf("*** Got fruit: %v\n", cmdr.GetString("app.mx-test.fruit")) fmt.Printf("*** Got head: %v\n", cmdr.GetInt("app.mx-test.head")) return }) mx.NewFlag(cmdr.OptFlagTypeString). Titles("pp", "password"). Description("the password requesting.", ""). Group(""). DefaultValue("", "PASSWORD"). ExternalTool(cmdr.ExternalToolPasswordInput) mx.NewFlag(cmdr.OptFlagTypeString). Titles("m", "message", "msg"). Description("the message requesting.", ""). Group(""). DefaultValue("", "MESG"). ExternalTool(cmdr.ExternalToolEditor) mx.NewFlag(cmdr.OptFlagTypeString). Titles("fr", "fruit"). Description("the message.", ""). Group(""). DefaultValue("", "FRUIT"). ValidArgs("apple", "banana", "orange") mx.NewFlag(cmdr.OptFlagTypeInt). Titles("hd", "head"). Description("the head lines.", ""). Group(""). DefaultValue(1, "LINES"). HeadLike(true, 1, 3000) // kv kvCmd := root.NewSubCommand(). Titles("kv", "kvstore"). Description("consul kv store operations...", ``) //...More
這種方式頗有效地改進的痛苦之源。要提及來,也沒有什麼缺點了。因此這也是 cmdr
主要推薦你採用的方案。docker
這種方式被有一些第三方解釋器所採用,能夠算是比較有價值的定義方式。其特色在於直觀、易於管理。數據結構
它的典型案例多是這樣子的:app
type argT struct { cli.Helper Port int `cli:"p,port" usage:"short and long format flags both are supported"` X bool `cli:"x" usage:"boolean type"` Y bool `cli:"y" usage:"boolean type, too"` } func main() { os.Exit(cli.Run(new(argT), func(ctx *cli.Context) error { argv := ctx.Argv().(*argT) ctx.String("port=%d, x=%v, y=%v\n", argv.Port, argv.X, argv.Y) return nil })) }
不過,因爲 cmdr
沒有打算支持這種方案,因此這裏僅介紹到這個程度。框架
說明一下,
cmdr
之因此不打算支持這種方案,是由於這樣作好處當然明顯,壞處也一樣使人煩惱:複雜的定義可能會由於被嵌套在 Tag 內而致使難以編寫,例如多行字符串在這裏就很難過。
cmdr
新增的兼容 flag
的定義方式那麼,咱們回顧了兩種或者三種典型的命令行界面定義方式以後,能夠發現他們和 flag
以前的區別是比較大的,當你一開始設計你的 app 時,若是爲了便宜和最快開始而採用了 flag
方案的話(畢竟,這是golang自帶的包嘛),再要想切換到一個加強版本的話,不管哪個都會令你痛一下。工具
flag
方式咱們看看當你採用 flag
方式時,你的 main 入口多是這樣的:
// old codes package main import "flag" var ( serv = flag.String("service", "hello_service", "service name") host = flag.String("host", "localhost", "listening host") port = flag.Int("port", 50001, "listening port") reg = flag.String("reg", "localhost:32379", "register etcd address") count = flag.Int("count", 3, "instance's count") connectTimeout = flag.Duration("connect-timeout", 5*time.Second, "connect timeout") ) func main(){ flag.Parse() // ... }
cmdr
爲了遷移爲使用 cmdr
,你能夠簡單地替換 import "flag"
語句爲這樣:
import ( // 「flag」 "github.com/hedzr/cmdr/flag" )
其它內容一概不變,也就是說完整的入口如今像這樣:
// new codes package main import ( // 「flag」 "github.com/hedzr/cmdr/flag" ) var ( serv = flag.String("service", "hello_service", "service name") host = flag.String("host", "localhost", "listening host") port = flag.Int("port", 50001, "listening port") reg = flag.String("reg", "localhost:32379", "register etcd address") count = flag.Int("count", 3, "instance's count") connectTimeout = flag.Duration("connect-timeout", 5*time.Second, "connect timeout") ) func main(){ flag.Parse() // ... }
怎麼樣,足夠簡單吧?
那麼咱們如今指望引入更多 cmdr
專有特性怎麼辦呢?
例如想要全名(完整單詞)做爲長選項,補充短選項定義,這能夠經過以下的序列來達成:
import ( // 「flag」 "github.com/hedzr/cmdr" "github.com/hedzr/cmdr/flag" ) var( // uncomment this line if you like long opt (such as --service) treatAsLongOpt = flag.TreatAsLongOpt(true) serv = flag.String("service", "hello_service", "service name", flag.WithShort("s"), flag.WithDescription("single line desc", `long desc`)) )
相似的能夠完成其餘加強特性的定義。
全部 cmdr
特性被濃縮在幾個少許的接口中了。此外,某些特性是當你使用 cmdr
時就當即得到了,無需其它表述或設定(例如短選項的組合,自動的幫助屏,多級命令等等)。
全部的這些須要指定適當參數的特性,包含在以下的這些定義中:
flag.WithTitles(short, long string, aliases ...string) (opt Option)
定義短選項,長選項,別名。
綜合來講,你必須在某個地方定義了一個選項的長名字,由於這是內容索引的依據,若是長名字缺失,那麼可能會有意料以外的錯誤。
別名是隨意的。
若是能夠,儘量提供短選項。
短選項通常來講是一個字母,然而使用兩個甚至更多字母是被容許的,這是爲了提供多種風格的命令行界面的兼容性。例如 wget
, rar
都採用了雙字母的短選項。而 golang flag
自身支持的是任意長度的短選項,沒有長選項支持。cmdr
在短選項上的寬鬆和兼容程度,是其它幾乎全部第三方命令行參數解釋器所不能達到的。
flag.WithShort(short string) (opt Option)
提供短選項定義。
flag.WithLong(long string) (opt Option)
提供長選項定義。
flag.WithAliases(aliases ...string) (opt Option)
提供別名定義。別名是任意多、可選的。
flag.WithDescription(oneLine, long string) (opt Option)
提供描述行文本。
oneLine
提供單行描述文本,這一般是在參數被列表時。long
提供的多行描述文本是可選的,你能夠提供空字串給它,這個文本在參數被單獨顯示幫助詳情時會給予用戶更多的描述信息。
flag.WithExamples(examples string) (opt Option)
能夠提供參數用法的命令行實例樣本。
這個字串能夠是多行的,它遵守必定的格式要求,咱們之前的文章中對該格式有過描述。這樣的格式要求是爲了在 man/info page 中可以得到更視覺敏銳的條目,因此你能夠自行斷定要不要遵照規則。
flag.WithGroup(group string) (opt Option)
命令或者參數都是能夠被分組的。
分組是能夠被排序的。給 group
字串一個帶句點的前綴,則這個前綴會被切割出來用於排序,排序規則是 A-Z0-9a-z 按照 ASCII 順序。因此:
1001.c++
, 1100.golang
, 1200.java
, …;abcd.c++
, b999.golang
, zzzz.java
, …;是有順序的。
因爲第一個句點以前的排序用子串被切掉了,所以你的 group
名字能夠不受這個序號的影響。
給分組一個空字串,意味着使用內置的 空
分組,這個分組被排列在其餘全部分組以前。
給分組一個 cmdr.UnsortedGroup
常量,則它會被概括到最後一個分組中。值得注意的是,最後一個分組,依賴的是 cmdr.UnsortedGroup
常量的具體值zzzz.unsorted
,因此,你仍然有機會定義一個別的序號來繞過這個「最後」。
flag.WithHidden(hidden bool) (opt Option)
hidden爲true是,該選項不會被列舉到幫助屏中。
flag.WithDeprecated(deprecation string) (opt Option)
通常來講,你須要給 deprecation
提供一個版本號。這意味着,你提醒最終用戶該選項從某個版本號開始就已經被廢棄了。
按照 Deprecated 的禮貌規則,咱們廢棄一個選項時,首先標記它,並給出替代提示,而後在若干次版本迭代以後正式取消它。
flag.WithAction(action func(cmd *Command, args []string) (err error)) (opt Option)
按照 cmdr
的邏輯,一個選項在被顯式命中時,你能夠提供一個即時的響應動做,這可能容許你完成一些特別的操做,例如爲相關聯的其它一組選項調整默認值什麼的。
flag.WithToggleGroup(group string) (opt Option)
若是你打算定義一組選項,帶有互斥效果,如同 radio button group 那樣,那麼你能夠爲它們提供相同的 WithToggleGroup group name
,這個名字和 WithGroup group name
沒有任何關聯關係。
flag.WithDefaultValue(val interface{}, placeholder string) (opt Option)
提供選項的默認值以及佔位符。
默認值的數據類型至關重要,由於這個數據類型時後續抽取該選項真實值的參考依據。
例如,int數據必定要提供一個 int 數值,Duration數據必定要提供一個 3*time.Second
這樣的確切數值。
flag.WithExternalTool(envKeyName string) (opt Option)
提供一個環境變量名,例如 EDITOR。那麼,若是該選項的 value 部分沒有在命令行中被提供時,cmdr
會搜索環境變量的值,將其做爲控制檯終端應用程序運行,並收集該運行的結果(一個臨時文件的文件內容)用於爲該選項複製。
如同 git commit -m
那樣。
flag.WithValidArgs(list ...string) (opt Option)
提供一個枚舉表,用於約束用戶所提供的值。
flag.WithHeadLike(enable bool, min, max int64) (opt Option)
當該選項被設定爲 enable=true 時,識別用戶輸入的諸如 -1973
, -211
之類的整數短選項,將其整數數值做爲本選項的數值。
如同 head -9
等效於 head -n 9
那樣。
好了。不少內容。不過仍是堆出來了,本身欣慰一下。
嗯,cmdr
的 v1.0.3
是一個 pre-release 版本,咱們已經提供一個 flag
的最平滑遷移的基本實現。
最近的日子裏,咱們會考慮完成子命令部分,並最終釋出 v1.1.0
,請期待。
若是認爲這樣作有價值的話,考慮去鼓勵一下。