項目地址:spf13/cobra: A Commander for modern Go CLI interactions (github.com)
文檔地址:cobra/user_guide.md at master · spf13/cobra (github.com)
Overview
cobra是一個用於創建命令行工具的庫(框架),可以創建出類似git或者go一樣的工具,進行我們平時熟悉的git clone/pull、go get/install等操作。kubernetes工具中就使用了cobra。
上述這些命令行工具的--help/-h既好看又好理解,開始時我也會好奇這些工具是如何實現多級子命令支持的?cobra的目標即是提供快速構建出此類工具的能力(cobra是框架,具體的業務邏輯當然還是要自己寫)。例如可以為我們app或系統工具的bin文件添加--config來啟動服務、添加--version來查看版本號等等。相比起標准庫里的flag包,cobra強大了很多。
官網列出了cobra的一大堆優點:
-
易用的subcommand模式(即嵌套命令或子命令)
-
強大的flags支持(參考標准庫flagSet)
-
支持global、local、cascading級別的flag設置
-
錯誤時的智能提示
-
自動生成的、美觀的help信息,並默認支持--help和-h打印help信息
-
提供命令自動補全功能(bash, zsh, fish, powershell環境)
-
提供命令man page自動生成功能
-
有兄弟項目viper可以快捷的實現配置文件解析和flag綁定(暫不建議直接使用,viper包裝了一些配置文件解析的細節,應當在熟悉cobra之后再去單獨了解)
本文的核心目的為:通過手寫一個示例來迅速入門cobra。
先來簡單看下cobra框架中主要概念:
kubectl get pod|service [podName|serviceName] -n <namespace>
以上述kubectl get為例,cobra將kubectl稱作做rootcmd(即根命令),get稱做rootcmd的subcmd,pod|service則是get的subcmd,podName、serviceName是pod/service的args,-n/--namespace稱作flag。同時我們還觀察到-n這個flag其實寫在任意一個cmd之后都會正常生效,這說明這是一個global flag,global flag對於rootcmd及所有子命令生效。
此外,當為cmd指定了一個與subcmd同名的args時,subcmd優先生效。
一、使用cobra的建議的項目目錄結構
cobra文檔告訴我們,使用cobra時最好遵循如下的項目目錄設置(即:把命令行定義相關的部分單獨寫在一個名為cmd的包內):
▾ appName/ ▾ cmd/ add.go your.go commands.go here.go main.go
使用cobra的項目,其程序入口main.go一般極為簡潔(因為相應的業務邏輯都在cmd里調用或實現):
package main import ( "{pathToYourApp}/cmd" ) func main() { cmd.Execute() }
二、寫一個簡單地mytool工具
本文示例不會去寫一個常駐的后台服務,只寫一個命令行工具mytool進行演示。
我們為mytool工具預設如下功能,朝着這個目標前進:
// 打印mytool的版本號,預設為1.0-beta mytool version // names目錄中創建name同名文件,將desc內容寫入文件中,成功后打印"${name} is added" mytool add "Donald.Trump" --desc "He knows everything" // names目錄中刪除name同名文件,成功后打印"${name} is deleted" mytool del "Donald.Trump" // names目錄中讀取name同名文件中的信息,如未找到對應name那么顯示"${name} not found" mytool get "Donald.Trump" // names目錄中讀取所有文件並打印相關信息 mytool get all
1. 首先,創建如下的項目結構:
然后使用go mod初始化包管理文件go.mod:go mod init mytool
2. 編寫cmd/root.go文件,正如其名這是所有命令行的root:
package cmd import ( "fmt" "github.com/spf13/cobra" "os" ) var rootCmd = &cobra.Command{ Use: "mytool", Short: "mytool is a tool to record names", // 子命令的簡單說明,務必簡短,因為會出現在其上級命令的help結果中,過長會導致換行、不美觀,此外還會出現在自動補全時,過長容易影響視線 Long: `The best tool to record names in the world! // 子命令的功用詳細說明,可以寫的比較詳細些 Just try it!!! Complete documentation is available at http://mytool.com`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Use mytool -h or --help for help.") }, } func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } }
3. 然后創建main/main.go文件,並編譯后試試效果:

可以看到基礎的-h功能展示了出來。接下來我們為框架補充實體邏輯。
4. 先增加一個version功能試試
// cmd目錄下新增version.go文件,內容如下: package cmd import ( "fmt" "github.com/spf13/cobra" ) var versionCmd = &cobra.Command{ Use: "version", Short: "show version of mytool", Long: `All tools have a version, this is mytool's"`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("mytool-1.0-beta") }, } func initVersion() { rootCmd.AddCommand(versionCmd) }
我們定義了一個全新的cobra.Command:versionCmd,我們希望他成為mytool的一個subcmd,只要使用rootCmd.AddCommand(versionCmd)就可以實現了。這個操作可以在cmd包中任意地方寫,但統一起見我們在每個cmd文件中寫一個init函數來執行對應cmd的add操作和未來可能存在的flag綁定等操作,然后在root.go中增加initAll()函數來調用組織子命令的init函數(需要注意的是init()已經被cobra占用,所以我們自己的init函數加上各種后綴即可)。
package cmd import ( "fmt" "github.com/spf13/cobra" "os" ) var rootCmd = &cobra.Command{ Use: "mytool", Short: "mytool is a tool to record names", Long: `The best tool to record names in the world! Just try it!!! Complete documentation is available at http://mytool.com`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Use mytool -h or --help for help.") }, } func initAll() { initVersion() } func Execute() { initAll() if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } }
可以看到關於version的help已經呈現了出來,也能正常查看mytool的version了,但實測還沒有子命令自動預測補全的功能,這個在下邊嘗試。
5. 增加add子命令
cmd/add.go,這個子命令使用指定的name args作為參數,使用--desc作為flag
package cmd import ( "fmt" "github.com/spf13/cobra" "io/ioutil" ) var ( name string nameDesc string ) var addNameCmd = &cobra.Command{ Use: "add", Short: "mytool name add operations", Long: `mytool add: add name info in names dir`, Run: func(cmd *cobra.Command, args []string) { if len(args) != 1 { fmt.Println(cmd.Help()) return } name = args[0] err := ioutil.WriteFile(fmt.Sprintf("./names/%s",name), []byte(nameDesc), 0644) if err != nil { fmt.Println(err) return } fmt.Printf("%s is added\n", name) }, } func initAdd() { addNameCmd.Flags().StringVarP(&nameDesc, "desc", "D", "", "description of this person") rootCmd.AddCommand(addNameCmd) }
然后在root.go initAll()中增加initAdd()的調用:
func initAll() { initVersion() initAdd() }
可以看到add子命令已經出現在help中了,且多了一個completion子命令,嘗試一下可以發現,這個子命令可以用於生成各個shell環境下的自動補全腳本,我這里就略過不做嘗試了。
6. 接下來照貓畫虎添加delete.go和get.go文件
package cmd import ( "fmt" "github.com/spf13/cobra" "os" ) var delNameCmd = &cobra.Command{ Use: "del", Short: "mytool name del operations", Long: `mytool del: del name info in names dir`, Run: func(cmd *cobra.Command, args []string) { if len(args) != 1 { fmt.Println("Only one name can be deleted in one time!") return } name = args[0] err := os.Remove(fmt.Sprintf("./names/%s",name)) if err != nil { fmt.Printf("%s not found\n", name) return } fmt.Printf("%s is deleted\n", name) }, } func initDelete() { rootCmd.AddCommand(delNameCmd) }
package cmd import ( "fmt" "github.com/spf13/cobra" "io/ioutil" ) var getNameCmd = &cobra.Command{ Use: "get", Short: "mytool name get operations", Long: `mytool get: get name info in names dir`, Run: func(cmd *cobra.Command, args []string) { if len(args) != 1 { fmt.Println("Only one name can be get in one time! Or use get all to get all names!") return } name = args[0] nameInfo,err := ioutil.ReadFile(fmt.Sprintf("./names/%s",name)) if err != nil { fmt.Printf("%s not found\n", name) return } fmt.Println(name,":",string(nameInfo)) }, } var getAllNamesCmd = &cobra.Command{ Use: "all", Short: "mytool name get all operations", Long: `mytool get all: get all names info in names dir`, Run: func(cmd *cobra.Command, args []string) { if len(args) != 0 { fmt.Println(getNameCmd.Help()) return } files, err := ioutil.ReadDir("./names") if err != nil { fmt.Println("Dir names not found!") return } for _, file := range files { nameInfo,_ := ioutil.ReadFile(fmt.Sprintf("./names/%s", file.Name())) fmt.Println(file.Name(),":",string(nameInfo)) } }, } func initGet() { getNameCmd.AddCommand(getAllNamesCmd) rootCmd.AddCommand(getNameCmd) }
至此所有cmd添加完畢,我們只需要在root.go中集成相關init函數即可。
func initAll() { initVersion() initAdd() initDelete() initGet() }
經測試各項功能正常。
三、思考
1. 我做了什么?
在本次筆記中,根據官方文檔編寫了一個mytool的指令,其實現了基本的subcommand添加、flag添加、args判斷,基於此初步了解了cobra的命令添加體系和組織邏輯。
2. 有哪些地方需要注意的 以及 我還能做些什么?
實際項目中的情況會比示例復雜的多,但只要清楚其脈絡就可以融會貫通。
- 如何設置無需指定參數的flag:通過指定包含默認值的bool類型的flag來實現無參flag設置,例如如何將本例中的get all改為無參flag get --all
- args的自動校驗:本例通過簡單的len(args)進行參數校驗,cobra提供了便捷的Command內置字段Args來支持args校驗,其值可以是cobra內置args校驗函數,如NoArgs,MinimumNArgs(int),MaximumNArgs(int),ExactArgs(int)等等,也可以自定義匿名函數進行args的提前校驗
- 如何強制指定flag:rootCmd.MarkFlagRequired(<flagName>)、rootCmd.MarkPersistentFlagRequired(<flagName>)
- cobra.Command的Run函數返回error:默認的Run函數不會返回error,所以實際上本例中的Execute()沒有遇到異常panic時都會返回nil。如果想要捕捉各個cmd的運行時的業務error,可以使用RunE參數來代替Run參數,RunE支持error返回
- help和usage的自定義:一般不需要
- preRun和PostRun鈎子(hooks):cobra提供了preRun和PostRun的hooks,參考webhooks理解即可,他們提供了在運行Run函數之前/之后執行其他更多操作的能力,這部分的使用很簡單,只需要為Command對象的幾個字段賦值相應的匿名函數即可。以本文為例,我們需要在執行任意命令組合之前都檢查names文件夾是否存在,如果不存在則創建之,這個邏輯就可以寫在rootcmd的preRun函數中,這樣我們就不必在每次用到names文件夾時都檢查其是否存在了。
-
自動補全和man page生成功能測試:completion功能很簡單,而關於man page生成則可參考:cobra/README.md at master · spf13/cobra (github.com)
- 多級子命令:通常我們最多寫3層子命令(包含rootCmd在內)就可以了,建議按命令繼承的層次來組織cmd內的目錄層次,這樣可以快速找到對應子命令的實現邏輯。
cobra作者spf13還有一個viper項目,此項目提供便捷的針對json,yaml,toml,ini,hcl等類型配置文件的解析,配合cobra使用更佳,當然僅使用cobra也很好,自己解析配置文件也很便捷。
自動補全相關:
app completion bash > /etc/bash_completion.d/app
上述指令可以快捷的生成針對app指令的bash補全配置,並直接放入/etc/bash_completion.d文件中,新會話登錄后即可實現自動補全。