原文:https://juejin.cn/post/7231197051203256379
Cobra 是一個 Go 語言開發的命令列(CLI)框架,它提供了簡潔、靈活且強大的方式來建立命令列程式。它包含一個用於建立命令列程式的庫(Cobra 庫),以及一個用於快速生成基於 Cobra 庫的命令列程式工具(Cobra 命令)。Cobra 是由 Go 團隊成員 spf13 為 Hugo 專案建立的,並已被許多流行的 Go 專案所採用,如 Kubernetes、Helm、Docker (distribution)、Etcd 等。
概念
Cobra 建立在命令、引數和標誌這三個結構之上。要使用 Cobra 編寫一個命令列程式,需要明確這三個概念。
-
命令(COMMAND):命令表示要執行的操作。
-
引數(ARG):是命令的引數,一般用來表示操作的物件。
-
標誌(FLAG):是命令的修飾,可以調整操作的行為。
一個好的命令列程式在使用時讀起來像句子,使用者會自然的理解並知道如何使用該程式。
要編寫一個好的命令列程式,需要遵循的模式是 APPNAME VERB NOUN --ADJECTIVE
或 APPNAME COMMAND ARG --FLAG
。
在這裡 VERB
代表動詞,NOUN
代表名詞,ADJECTIVE
代表形容詞。
以下是一個現實世界中好的命令列程式的例子:
$ hugo server --port=1313
以上示例中,server
是一個命令(子命令),port
是一個標誌(1313
是標誌的引數,但不是命令的引數 ARG)。
下面是一個 git
命令的例子:
$ git clone URL --bare
以上示例中,clone
是一個命令(子命令),URL
是命令的引數,bare
是標誌。
快速開始
要使用 Cobra 建立命令列程式,需要先透過如下命令進行安裝:
$ go get -u github.com/spf13/cobra/cobra
安裝好後,就可以像其他 Go 語言庫一樣匯入 Cobra 包並使用了。
import "github.com/spf13/cobra"
建立一個命令
假設我們要建立的命令列程式叫作 hugo
,可以編寫如下程式碼建立一個命令:
hugo/cmd/root.go
var rootCmd = &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at https://gohugo.io`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run hugo...")
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
cobra.Command
是一個結構體,代表一個命令,其各個屬性含義如下:
Use
是命令的名稱。
Short
代表當前命令的簡短描述。
Long
表示當前命令的完整描述。
Run
屬性是一個函式,當執行命令時會呼叫此函式。
rootCmd.Execute()
是命令的執行入口,其內部會解析 os.Args[1:]
引數列表(預設情況下是這樣,也可以透過 Command.SetArgs
方法設定引數),然後遍歷命令樹,為命令找到合適的匹配項和對應的標誌。
建立 main.go
按照編寫 Go 程式的慣例,我們要為 hugo
程式編寫一個 main.go
檔案,作為程式的啟動入口。
hugo/main.go
package main
import (
"hugo/cmd"
)
func main() {
cmd.Execute()
}
main.go
程式碼實現非常簡單,只在 main
函式中呼叫了 cmd.Execute()
函式,來執行命令。
編譯並執行命令
現在,我們就可以編譯並執行這個命令列程式了。
# 編譯
$ go build -o hugo
# 執行
$ ./hugo
run hugo...
筆記:示例程式碼裡沒有列印
Run
函式的args
引數內容,你可以自行列印看看結果(提示:args
為命令列引數列表)。
以上我們編譯並執行了 hugo
程式,輸出內容正是 cobra.Command
結構體中 Run
函式內部程式碼的執行結果。
我們還可以使用 --help
檢視這個命令列程式的使用幫助。
$ ./hugo --help
A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at https://gohugo.io
Usage:
hugo [flags]
Flags:
-h, --help help for hugo
這裡列印了 cobra.Command
結構體中 Long
屬性的內容,如果 Long
屬性不存在,則列印 Short
屬性內容。
hugo
命令用法為 hugo [flags]
,如 hugo --help
。
這個命令列程式自動支援了 -h/--help
標誌。
以上就是使用 Cobra 編寫一個命令列程式最常見的套路,這也是 Cobra 推薦寫法。
當前專案目錄結構如下:
$ tree hugo
hugo
├── cmd
│ └── root.go
├── go.mod
├── go.sum
└── main.go
Cobra 程式目錄結構基本如此,main.go
作為命令列程式的入口,不要寫過多的業務邏輯,所有命令都應該放在 cmd/
目錄下,以後不管編寫多麼複雜的命令列程式都可以這麼來設計。
新增子命令
與定義 rootCmd
一樣,我們可以使用 cobra.Command
定義其他命令,並透過 rootCmd.AddCommand()
方法將其新增為 rootCmd
的一個子命令。
hugo/cmd/version.go
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
現在重新編譯並執行命令列程式。
$ go build -o hugo
$ ./hugo version
Hugo Static Site Generator v0.9 -- HEAD
可以發現 version
命令已經被加入進來了。
再次檢視幫助資訊:
$ ./hugo -h
A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at https://gohugo.io
Usage:
hugo [flags]
hugo [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
version Print the version number of Hugo
Flags:
-h, --help help for hugo
Use "hugo [command] --help" for more information about a command.
這次的幫助資訊更為豐富,除了可以使用 hugo [flags]
語法,由於子命令的加入,又多了一個 hugo [command]
語法可以使用,如 hugo version
。
現在有三個可用命令:
completion
可以為指定的 Shell 生成自動補全指令碼,將在 Shell 補全 小節進行講解。
help
用來檢視幫助,同 -h/--help
類似,可以使用 hugo help command
語法檢視 command
命令的幫助資訊。
version
為新新增的子命令。
檢視子命令幫助資訊:
$ ./hugo help version
All software has versions. This is Hugo's
Usage:
hugo version [flags]
Flags:
-h, --help help for version
使用命令列標誌
Cobra 完美適配 pflag,結合 pflag 可以更靈活的使用標誌功能。
提示:對 pflag 不熟悉的讀者可以參考我的另一篇文章《Go 命令列引數解析工具 pflag 使用》。
持久標誌
如果一個標誌是持久的
,則意味著該標誌將可用於它所分配的命令以及該命令下的所有子命令。
對於全域性標誌,可以定義在根命令 rootCmd
上。
var Verbose bool
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
本地標誌
標誌也可以是本地的
,這意味著它只適用於該指定命令。
var Source string
rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
父命令的本地標誌
預設情況下,Cobra 僅解析目標命令上的本地標誌,忽略父命令上的本地標誌。透過在父命令上啟用 Command.TraverseChildren
屬性,Cobra 將在執行目標命令之前解析每個命令的本地標誌。
var rootCmd = &cobra.Command{
Use: "hugo",
TraverseChildren: true,
}
提示:如果你不理解,沒關係,繼續往下看,稍後會有示例程式碼演示講解。
必選標誌
預設情況下,標誌是可選的。我們可以將其標記為必選,如果沒有提供,則會報錯。
var Region string
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
定義好以上幾個標誌後,為了展示效果,我們對 rootCmd.Run
方法做些修改,分別列印 Verbose
、Source
、Region
幾個變數。
var rootCmd = &cobra.Command{
Use: "hugo",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run hugo...")
fmt.Printf("Verbose: %v\n", Verbose)
fmt.Printf("Source: %v\n", Source)
fmt.Printf("Region: %v\n", Region)
},
}
另外,為了測試啟用 Command.TraverseChildren
的效果,我又新增了一個 print
子命令。
hugo/cmd/print.go
var printCmd = &cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run print...")
fmt.Printf("printFlag: %v\n", printFlag)
fmt.Printf("Source: %v\n", Source)
},
}
func init() {
rootCmd.AddCommand(printCmd)
// 本地標誌
printCmd.Flags().StringVarP(&printFlag, "flag", "f", "", "print flag for local")
}
現在,我們重新編譯並執行 hugo
,來對上面新增的這幾個標誌進行測試。
$ go build -o hugo
$ ./hugo -h
A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at https://gohugo.io
Usage:
hugo [flags]
hugo [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
print
version Print the version number of Hugo
Flags:
-h, --help help for hugo
-r, --region string AWS region (required)
-s, --source string Source directory to read from
-v, --verbose verbose output
Use "hugo [command] --help" for more information about a command.
以上幫助資訊清晰明瞭,我就不過多解釋了。
執行 hugo
命令:
$ ./hugo -r test-region
run hugo...
Verbose: false
Source:
Region: test-region
現在 -r/--region
為必選標誌,不傳將會得到 Error: required flag(s) "region" not set
報錯。
執行 print
子命令:
$ ./hugo print -f test-flag
run print...
printFlag: test-flag
Source:
以上執行結果可以發現,父命令的標誌 Source
內容為空。
現在使用如下命令執行 print
子命令:
$ ./hugo -s test-source print -f test-flag
run print...
printFlag: test-flag
Source: test-source
在 print
子命令前,我們指定了 -s test-source
標誌,-s/--source
是父命令 hugo
的標誌,也能夠被正確解析,這就是啟用 Command.TraverseChildren
的效果。
如果我們將 rootCmd
的 TraverseChildren
屬性置為 false
,則會得到 Error: unknown shorthand flag: 's' in -s
報錯。
# 指定 rootCmd.TraverseChildren = false 後,重新編譯程式
$ go build -o hugo
# 執行同樣的命令,現在會得到報錯
$ ./hugo -s test-source print -f test-flag
Error: unknown shorthand flag: 's' in -s
Usage:
hugo print [OPTIONS] [COMMANDS] [flags]
Flags:
-f, --flag string print flag for local
-h, --help help for print
Global Flags:
-v, --verbose verbose output
unknown shorthand flag: 's' in -s
處理配置
除了將命令列標誌的值繫結到變數,我們也可以將標誌繫結到 Viper,這樣就可以使用 viper.Get()
來獲取標誌的值了。
var author string
func init() {
rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}
提示:對 Viper 不熟悉的讀者可以參考我的另一篇文章《在 Go 中如何使用 Viper 來管理配置》。
另外,我們可以使用 cobra.OnInitialize()
來初始化配置檔案。
var cfgFile string
func init() {
cobra.OnInitialize(initConfig)
rootCmd.Flags().StringVarP(&cfgFile, "config", "c", "", "config file")
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
if err := viper.ReadInConfig(); err != nil {
fmt.Println("Can't read config:", err)
os.Exit(1)
}
}
傳遞給 cobra.OnInitialize()
的函式 initConfig
函式將在呼叫命令的 Execute
方法時執行。
為了展示使用 Cobra 處理配置的效果,需要修改 rootCmd.Run
函式的列印程式碼:
var rootCmd = &cobra.Command{
Use: "hugo",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run hugo...")
fmt.Printf("Verbose: %v\n", Verbose)
fmt.Printf("Source: %v\n", Source)
fmt.Printf("Region: %v\n", Region)
fmt.Printf("Author: %v\n", viper.Get("author"))
fmt.Printf("Config: %v\n", viper.AllSettings())
},
}
提供 config.yaml
配置檔案內容如下:
username: jianghushinian
password: 123456
server:
ip: 127.0.0.1
port: 8080
現在重新編譯並執行 hugo
命令:
# 編譯
$ go build -o hugo
# 執行
$ ./hugo -r test-region --author jianghushinian -c ./config.yaml
run hugo...
Verbose: false
Source:
Region: test-region
Author: jianghushinian
Config: map[author:jianghushinian password:123456 server:map[ip:127.0.0.1 port:8080] username:jianghushinian]
筆記:Cobra 同時支援 pflag 和 Viper 兩個庫,實際上這三個庫出自同一作者 spf13。
引數驗證
在執行命令列程式時,我們可能需要對命令引數進行合法性驗證,cobra.Command
的 Args
屬性提供了此功能。
Args
屬性型別為一個函式:func(cmd *Command, args []string) error
,可以用來驗證引數。
Cobra 內建了以下驗證函式:
-
NoArgs
:如果存在任何命令引數,該命令將報錯。 -
ArbitraryArgs
:該命令將接受任意引數。 -
OnlyValidArgs
:如果有任何命令引數不在Command
的ValidArgs
欄位中,該命令將報錯。 -
MinimumNArgs(int)
:如果沒有至少 N 個命令引數,該命令將報錯。 -
MaximumNArgs(int)
:如果有超過 N 個命令引數,該命令將報錯。 -
ExactArgs(int)
:如果命令引數個數不為 N,該命令將報錯。 -
ExactValidArgs(int)
:如果命令引數個數不為 N,或者有任何命令引數不在Command
的ValidArgs
欄位中,該命令將報錯。 -
RangeArgs(min, max)
:如果命令引數的數量不在預期的最小數量min
和最大數量max
之間,該命令將報錯。
內建驗證函式用法如下:
var versionCmd = &cobra.Command{
Use: "version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
},
Args: cobra.MaximumNArgs(2), // 使用內建的驗證函式,位置引數多於 2 個則報錯
}
重新編譯並執行 hugo
命令:
# 編譯
$ go build -o hugo
# 兩個命令引數滿足驗證函式的要求
$ ./hugo version a b
Hugo Static Site Generator v0.9 -- HEAD
# 超過兩個引數則報錯
$ ./hugo version a b c
Error: accepts at most 2 arg(s), received 3
當然,我們也可以自定義驗證函式:
var printCmd = &cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run print...")
// 命令列位置引數列表:例如執行 `hugo print a b c d` 將得到 [a b c d]
fmt.Printf("args: %v\n", args)
},
// 使用自定義驗證函式
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires at least one arg")
}
if len(args) > 4 {
return errors.New("the number of args cannot exceed 4")
}
if args[0] != "a" {
return errors.New("first argument must be 'a'")
}
return nil
},
}
重新編譯並執行 hugo
命令:
# 編譯
$ go build -o hugo
# 4 個引數滿足條件
$ ./hugo print a b c d
run print...
args: [a b c d]
# 沒有引數則報錯
$ ./hugo print
Error: requires at least one arg
# 第一個引數不滿足驗證函式邏輯,也會報錯
$ ./hugo print x
Error: first argument must be 'a'
Hooks
在執行 Run
函式前後,我麼可以執行一些鉤子函式,其作用和執行順序如下:
-
PersistentPreRun
:在PreRun
函式執行之前執行,對此命令的子命令同樣生效。 -
PreRun
:在Run
函式執行之前執行。 -
Run
:執行命令時呼叫的函式,用來編寫命令的業務邏輯。 -
PostRun
:在Run
函式執行之後執行。 -
PersistentPostRun
:在PostRun
函式執行之後執行,對此命令的子命令同樣生效。
修改 rootCmd
如下:
var rootCmd = &cobra.Command{
Use: "hugo",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("hugo PersistentPreRun")
},
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("hugo PreRun")
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run hugo...")
},
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("hugo PostRun")
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("hugo PersistentPostRun")
},
}
重新編譯並執行 hugo
命令:
# 編譯
$ go build -o hugo
# 執行
$ ./hugo
hugo PersistentPreRun
hugo PreRun
run hugo...
hugo PostRun
hugo PersistentPostRun
輸出順序符合預期。
其中 PersistentPreRun
、PersistentPostRun
兩個函式對子命令同樣生效。
$ ./hugo version
hugo PersistentPreRun
Hugo Static Site Generator v0.9 -- HEAD
hugo PersistentPostRun
以上幾個函式都有對應的 <Hooks>E
版本,E
表示 Error
,即函式執行出錯將會返回 Error
,執行順序不變:
-
PersistentPreRunE
-
PreRunE
-
RunE
-
PostRunE
-
PersistentPostRunE
如果定義了 <Hooks>E
函式,則 <Hooks>
函式不會執行。比如同時定義了 Run
和 RunE
,則只會執行 RunE
,不會執行 Run
,其他 Hooks
函式同理。
var rootCmd = &cobra.Command{
Use: "hugo",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("hugo PersistentPreRun")
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("hugo PersistentPreRunE")
return nil
},
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("hugo PreRun")
},
PreRunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("hugo PreRunE")
return errors.New("PreRunE err")
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run hugo...")
},
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("hugo PostRun")
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("hugo PersistentPostRun")
},
}
重新編譯並執行 hugo
命令:
# 編譯
$ go build -o hugo
# 執行
$ ./hugo
hugo PersistentPreRunE
hugo PreRunE
Error: PreRunE err
Usage:
hugo [flags]
hugo [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
print
version Print the version number of Hugo
Flags:
--author string Author name for copyright attribution (default "YOUR NAME")
-c, --config string config file
-h, --help help for hugo
-r, --region string AWS region (required)
-s, --source string Source directory to read from
-v, --verbose verbose output
Use "hugo [command] --help" for more information about a command.
PreRunE err
可以發現,雖然同時定義了 PersistentPreRun
、PersistentPreRunE
兩個鉤子函式,但只有 PersistentPreRunE
會被執行。
在執行 PreRunE
時返回了一個錯誤 PreRunE err
,程式會終止執行並列印錯誤資訊。
如果子命令定義了自己的 Persistent*Run
函式,則不會繼承父命令的 Persistent*Run
函式。
var versionCmd = &cobra.Command{
Use: "version",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("version PersistentPreRun")
},
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("version PreRun")
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
},
}
重新編譯並執行 hugo
命令:
# 編譯
$ go build -o hugo
# 執行子命令
$ ./hugo version
version PersistentPreRun
version PreRun
Hugo Static Site Generator v0.9 -- HEAD
hugo PersistentPostRun
定義自己的 Help 命令
如果你對 Cobra 自動生成的幫助命令不滿意,我們可以自定義幫助命令或模板。
cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)
Cobra 提供了三個方法來實現自定義幫助命令,後兩者也適用於任何子命令。
預設情況下,我們可以使用 hugo help command
語法檢視子命令的幫助資訊,也可以使用 hugo command -h/--help
檢視。
使用 help
命令檢視幫助資訊:
$ ./hugo help version
hugo PersistentPreRunE
All software has versions. This is Hugo's
Usage:
hugo version [flags]
Flags:
-h, --help help for version
Global Flags:
--author string Author name for copyright attribution (default "YOUR NAME")
-v, --verbose verbose output
hugo PersistentPostRun
使用 -h/--help
檢視幫助資訊:
$ ./hugo version -h
All software has versions. This is Hugo's
Usage:
hugo version [flags]
Flags:
-h, --help help for version
Global Flags:
--author string Author name for copyright attribution (default "YOUR NAME")
-v, --verbose verbose output
二者唯一的區別是,使用 help
命令檢視幫助資訊時會執行鉤子函式。
我們可以使用 rootCmd.SetHelpCommand
來控制 help
命令輸出,使用 rootCmd.SetHelpFunc
來控制 -h/--help
輸出。
rootCmd.SetHelpCommand(&cobra.Command{
Use: "help",
Short: "Custom help command",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Custom help command")
},
})
rootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
fmt.Println(strings)
})
重新編譯並執行 hugo
命令:
# 編譯
$ go build -o hugo
# 使用 `help` 命令檢視幫助資訊
$ ./hugo help version
hugo PersistentPreRunE
Custom help command
hugo PersistentPostRun
# 使用 `-h` 檢視根命令幫助資訊
$ ./hugo -h
[-h]
# 使用 `-h` 檢視 version 命令幫助資訊
$ ./hugo version -h
[version -h]
可以發現,使用 help
命令檢視幫助資訊輸出結果是 rootCmd.SetHelpCommand
中 Run
函式的執行輸出。使用 -h
檢視幫助資訊輸出結果是 rootCmd.SetHelpFunc
函式的執行輸出,strings
代表的是命令列標誌和引數列表。
現在我們再來測試下 rootCmd.SetHelpTemplate
的作用,它用來設定幫助資訊模板,支援標準的 Go Template 語法,自定義模板如下:
rootCmd.SetHelpTemplate(`Custom Help Template:
Usage:
{{.UseLine}}
Description:
{{.Short}}
Commands:
{{- range .Commands}}
{{.Name}}: {{.Short}}
{{- end}}
`)
注意:為了單獨測試
cmd.SetHelpTemplate(s string)
,我已將上面rootCmd.SetHelpCommand
和rootCmd.SetHelpFunc
部分程式碼註釋掉了。
重新編譯並執行 hugo
命令:
# 編譯
$ go build -o hugo
# 檢視幫助
$ ./hugo -h
Custom Help Template:
Usage:
hugo [flags]
Description:
Hugo is a very fast static site generator
Commands:
completion: Generate the autocompletion script for the specified shell
help: Help about any command
print:
version: Print the version number of Hugo
# 檢視子命令幫助
$ ./hugo help version
hugo PersistentPreRunE
Custom Help Template:
Usage:
hugo version [flags]
Description:
Print the version number of Hugo
Commands:
hugo PersistentPostRun
可以發現,無論使用 help
命令檢視幫助資訊,還是使用 -h
檢視幫助資訊,其輸出內容都遵循我們自定義的模版格式。
定義自己的 Usage Message
當使用者提供無效標誌或無效命令時,Cobra 透過向使用者顯示 Usage
來提示使用者如何正確的使用命令。
例如,當使用者輸入無效的標誌 --demo
時,將得到如下輸出:
$ ./hugo --demo
Error: unknown flag: --demo
Usage:
hugo [flags]
hugo [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
print
version Print the version number of Hugo
Flags:
--author string Author name for copyright attribution (default "YOUR NAME")
-c, --config string config file
-h, --help help for hugo
-s, --source string Source directory to read from
-v, --verbose verbose output
Use "hugo [command] --help" for more information about a command.
unknown flag: --demo
首先程式會報錯 Error: unknown flag: --demo
,報錯後會顯示 Usage
資訊。
這個輸出格式預設與 help
資訊一樣,我們也可以進行自定義。Cobra 提供瞭如下兩個方法,來控制輸出,具體效果我就不演示了,留給讀者自行探索。
cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)
未知命令建議
在我們使用 git
命令時,有一個非常好用的功能,能夠對使用者輸錯的未知命令智慧提示。
示例如下:
$ git statu
git: 'statu' is not a git command. See 'git --help'.
The most similar commands are
status
stage
stash
當我們輸入一個不存在的命令 statu
時,git
會提示命令不存在,並且給出幾個最相似命令的建議。
這個功能非常實用,幸運的是,Cobra 自帶了此功能。
如下,當我們輸入一個不存在的命令 vers
時,hugo
會自動給出建議命令 version
:
$ ./hugo vers
Error: unknown command "vers" for "hugo"
Did you mean this?
version
Run 'hugo --help' for usage.
unknown command "vers" for "hugo"
Did you mean this?
version
注意⚠️:根據我的實測,要想讓此功能生效,
Command.TraverseChildren
屬性要置為false
。
如果你想徹底關閉此功能,可以使用如下設定:
command.DisableSuggestions = true
或者使用如下設定調整字串匹配的最小距離:
command.SuggestionsMinimumDistance = 1
SuggestionsMinimumDistance
是一個正整數,表示輸錯的命令與正確的命令最多有幾個不匹配的字元(最小距離),才會給出建議。如當值為 1
時,使用者輸入 hugo versiox
會給出建議,而如果使用者輸入 hugo versixx
時,則不會給出建議,因為已經有兩個字母不匹配 version
了。
Shell 補全
前文在講新增子命令小節時,我們見到過 completion
子命令,可以為指定的 Shell 生成自動補全指令碼,現在我們就來講解它的用法。
直接執行 hugo completion
命令,我們可以檢視它支援的幾種 Shell 型別 bash
、fish
、powershell
、zsh
。
$ ./hugo completion
Generate the autocompletion script for hugo for the specified shell.
See each sub-command's help for details on how to use the generated script.
Usage:
hugo completion [command]
Available Commands:
bash Generate the autocompletion script for bash
fish Generate the autocompletion script for fish
powershell Generate the autocompletion script for powershell
zsh Generate the autocompletion script for zsh
Flags:
-h, --help help for completion
Global Flags:
--author string Author name for copyright attribution (default "YOUR NAME")
-v, --verbose verbose output
Use "hugo completion [command] --help" for more information about a command.
要想知道自己正在使用的 Shell 型別,可以使用如下命令:
$ echo $0
/bin/zsh
可以發現,我使用的是 zsh
,所以我就以 zsh
為例,來演示下 completion
命令補全用法。
使用 -h/--help
我們可以檢視使用說明:
$ ./hugo completion zsh -h
Generate the autocompletion script for the zsh shell.
If shell completion is not already enabled in your environment you will need
to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions in your current shell session:
source <(hugo completion zsh)
To load completions for every new session, execute once:
#### Linux:
hugo completion zsh > "${fpath[1]}/_hugo"
#### macOS:
hugo completion zsh > $(brew --prefix)/share/zsh/site-functions/_hugo
You will need to start a new shell for this setup to take effect.
Usage:
hugo completion zsh [flags]
Flags:
-h, --help help for zsh
--no-descriptions disable completion descriptions
Global Flags:
--author string Author name for copyright attribution (default "YOUR NAME")
-v, --verbose verbose output
根據幫助資訊,如果為當前會話提供命令列補全功能,可以使用 source <(hugo completion zsh)
命令來實現。
如果要讓命令列補全功能永久生效,Cobra 則非常貼心的為 Linux 和 macOS 提供了不同命令。
你可以根據提示選擇自己喜歡的方式來實現命令列補全功能。
我這裡只實現為當前會話提供命令列補全功能為例進行演示:
# 首先在專案根目錄下,安裝 hugo 命令列程式,安裝後軟體存放在 $GOPATH/bin 目錄下
$ go install .
# 新增命令列補全功能
$ source <(hugo completion zsh)
# 現在命令列補全已經生效,只需要輸入一個 `v`,然後按下鍵盤上的 `Tab` 鍵,命令將自動補全為 `version`
$ hugo v
# 命令已被自動補全
$ hugo version
version PersistentPreRun
version PreRun
Hugo Static Site Generator v0.9 -- HEAD
其實將命令 source <(hugo completion zsh)
新增到 ~/.zshrc
檔案中,也能實現每次進入 zsh
後自動載入 hugo
的命令列補全功能。
注意:在執行
source <(hugo completion zsh)
前需要將rootCmd
中的鉤子函式內部的fmt.Println
程式碼全部註釋掉,不然列印內容會被當作命令來執行,將會得到Error: unknown command "PersistentPreRunE" for "hugo"
類似報錯資訊,雖然命令列補全功能依然能夠生效,但「沒有訊息才是最好的訊息」。
生成文件
Cobra 支援生成 Markdown
、ReStructured Text
、Man Page
三種格式文件。
這裡以生成 Markdown
格式文件為例,來演示下 Cobra 這一強大功能。
我們可以定義一個標誌 md-docs
來決定是否生成文件:
hugo/cmd/root.go
var MarkdownDocs bool
func init() {
rootCmd.Flags().BoolVarP(&MarkdownDocs, "md-docs", "m", false, "gen Markdown docs")
...
}
func GenDocs() {
if MarkdownDocs {
if err := doc.GenMarkdownTree(rootCmd, "./docs/md"); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
}
在 main.go
中呼叫 GenDocs()
函式。
func main() {
cmd.Execute()
cmd.GenDocs()
}
現在,重新編譯並執行 hugo
即可生成文件:
# 編譯
$ go build -o hugo
# 生成文件
$ ./hugo --md-docs
# 檢視生成的文件
$ tree docs/md
docs/md
├── hugo.md
├── hugo_completion.md
├── hugo_completion_bash.md
├── hugo_completion_fish.md
├── hugo_completion_powershell.md
├── hugo_completion_zsh.md
├── hugo_print.md
└── hugo_version.md
可以發現,Cobra 不僅為 hugo
命令生成了文件,並且還生成了子命令的文件以及命令列補全的文件。
使用 Cobra 命令建立專案
文章讀到這裡,我們可以發現,其實 Cobra 專案是遵循一定套路的,目錄結構、檔案、模板程式碼都比較固定。
此時,腳手架工具就派上用場了。Cobra 提供了 cobra-cli
命令列工具,可以透過命令的方式快速建立一個命令列專案。
安裝:
$ go install github.com/spf13/cobra-cli@latest
檢視使用幫助:
$ cobra-cli -h
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
cobra-cli [command]
Available Commands:
add Add a command to a Cobra Application
completion Generate the autocompletion script for the specified shell
help Help about any command
init Initialize a Cobra Application
Flags:
-a, --author string author name for copyright attribution (default "YOUR NAME")
--config string config file (default is $HOME/.cobra.yaml)
-h, --help help for cobra-cli
-l, --license string name of license for the project
--viper use Viper for configuration
Use "cobra-cli [command] --help" for more information about a command.
可以發現,cobra-cli
腳手架工具僅提供了少量命令和標誌,所以上手難度不大。
初始化模組
要使用 cobra-cli
生成一個專案,首先要手動建立專案根目錄並使用 go mod
命令進行初始化。
假設我們要編寫的命令列程式叫作 cog
,模組初始化過程如下:
# 建立專案目錄
$ mkdir cog
# 進入專案目錄
$ cd cog
# 初始化模組
$ go mod init github.com/jianghushinian/blog-go-example/cobra/getting-started/cog
初始化命令列程式
有了初始化好的 Go 專案,我們就可以初始化命令列程式了。
# 初始化程式
$ cobra-cli init
Your Cobra application is ready at
# 檢視生成的專案目錄結構
$ tree .
.
├── LICENSE
├── cmd
│ └── root.go
├── go.mod
├── go.sum
└── main.go
2 directories, 5 files
# 執行命令列程式
$ go run main.go
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
使用 cobra-cli
初始化程式非常方便,只需要一個簡單的 init
命令即可完成。
目錄結構跟我們手動編寫的程式相同,只不過多了一個 LICENSE
檔案,用來存放專案的開源許可證。
透過 go run main.go
執行這個命令列程式,即可列印 rootCmd.Run
的輸出結果。
使用腳手架自動生成的 cog/main.go
檔案內容如下:
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package main
import "github.com/jianghushinian/blog-go-example/cobra/getting-started/cog/cmd"
func main() {
cmd.Execute()
}
自動生成的 cog/cmd/root.go
檔案內容如下:
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "cog",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cog.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
以上兩個檔案跟我們手動編寫的程式碼沒什麼兩樣,套路完全相同,唯一不同的是每個檔案頭部都會多出來一個 Copyright
頭資訊,用來標記程式碼的 LICENSE
。
可選標誌
cobra-cli
提供瞭如下三個標誌分別用來設定專案的作者、許可證型別、是否使用 Viper 管理配置。
$ cobra-cli init --author "jianghushinian" --license mit --viper
Your Cobra application is ready at
以上命令我們指定可選標誌後對專案進行了重新初始化。
現在 LICENSE
檔案內容不再為空,而是 MIT
協議。
The MIT License (MIT)
Copyright © 2023 jianghushinian
Permission is hereby granted...
並且 Go 檔案 Copyright
頭資訊中作者資訊也會被補全。
/*
Copyright © 2023 jianghushinian
...
*/
筆記:
cobra-cli
命令內建開源許可證支援GPLv2
、GPLv3
、LGPL
、AGPL
、MIT
、2-Clause BSD
或3-Clause BSD
。也可以參考官方文件來指定自定義許可證。
提示:如果你對開源許可證不熟悉,可以參考我的另一篇文章《開源協議簡介》。
新增命令
我們可以使用 add
命令為程式新增新的命令,並且 add
命令同樣支援可選標誌。
$ cobra-cli add serve
$ cobra-cli add config
$ cobra-cli add create -p 'configCmd' --author "jianghushinian" --license mit --viper
這裡分別新增了三個命令 serve
、config
、create
,前兩者都是 rootCmd
的子命令,create
命令則透過 -p 'configCmd'
引數指定為 config
的子命令。
注意⚠️:使用
-p 'configCmd'
標誌指定當前命令的父命令時,configCmd
必須是小駝峰命名法,因為cobra-cli
為config
生成的命令程式碼自動命名為configCmd
,而不是config_cmd
或其他形式,這符合 Go 語言變數命名規範。
現在命令列程式目錄結構如下:
$ tree .
.
├── LICENSE
├── cmd
│ ├── config.go
│ ├── create.go
│ ├── root.go
│ └── serve.go
├── go.mod
├── go.sum
└── main.go
2 directories, 8 files
可以使用如下命令執行子命令:
$ go run main.go config create
create called
其他新新增的命令同理。
使用配置取代標誌
如果你不想每次生成或新增命令時都指定選項引數,則可以定義 ~/.cobra.yaml
檔案來儲存配置資訊:
author: jianghushinian <jianghushinian007@outlook.com>
year: 2023
license: MIT
useViper: true
再次使用 init
命令初始化程式:
$ cobra-cli init
Using config file: /Users/jianghushinian/.cobra.yaml
會提示使用了 ~/.cobra.yaml
配置檔案。
現在 LICENSE
檔案內容格式如下:
The MIT License (MIT)
Copyright © 2023 jianghushinian <jianghushinian007@outlook.com>
...
Go 檔案 Copyright
頭資訊也會包含日期、使用者名稱、使用者郵箱。
/*
Copyright © 2023 jianghushinian <jianghushinian007@outlook.com>
...
*/
如果你不想把配置儲存在 ~/.cobra.yaml
中,cobra-cli
還提供了 --config
標誌來指定任意目錄下的配置檔案。
至此,cobra-cli
的功能我們就都講解完成了,還是非常方便實用的。
總結
在我們日常開發中,編寫命令列程式是必不可少,很多開源軟體都具備強大的命令列工具,如 K8s、Docker、Git 等。
一款複雜的命令列程式通常有上百種使用組合,所以如何組織和編寫出好用的命令列程式是很考驗開發者功底的,而 Cobra 則為我們開發命令列程式提供了足夠的便利。這也是為什麼我將其稱為命令列框架,而不僅僅是一個 Go 第三方庫。
Cobra 功能非常強大,要使用它來編寫命令列程式首先要明白三個概念:命令、引數和標誌。
Cobra 不僅支援子命令,還能夠完美相容 pflag 和 Viper 包,因為這三個包都是同一個作者開發的。關於標誌,Cobra 支援持久標誌、本地標誌以及將標誌標記為必選。Cobra 可以將標誌繫結到 Viper,方便使用 viper.Get()
來獲取標誌的值。對於命令列引數,Cobra 提供了不少驗證函式,我們也可以自定義驗證函式。
Cobra 還提供了幾個 Hooks 函式 PersistentPreRun
、PreRun
、PostRun
、PersistentPostRun
,可以分別在執行 Run
前後來處理一段邏輯。
如果覺得 Cobra 提供的預設幫助資訊不能滿足需求,我們還可以定義自己的 Help 命令和 Usage Message,非常靈活。
Cobra 還支援未知命令的智慧提示功能以及 Shell 自動補全功能,此外,它還支援自動生成 Markdown
、ReStructured Text
、Man Page
三種格式的文件。這對命令列工具的使用者來說非常友好,還能極大減少開發者的工作量。
最後,Cobra 的命令列工具 cobra-cli
進一步提高了編寫命令列程式的效率,非常推薦使用。
本文完整程式碼示例我放在了 GitHub 上,歡迎點選檢視。
希望此文能對你有所幫助。
聯絡我
- 微信:jianghushinian
- 郵箱:jianghushinian007@outlook.com
- 部落格地址:jianghushinian.cn/
參考
- Cobra 官網:cobra.dev/
- Cobra 原始碼:github.com/spf13/cobra
- Cobra 文件:pkg.go.dev/github.com/…
- Cobra-CLI 文件:github.com/spf13/cobra…
- 本文示例程式碼:github.com/jianghushin…
連結:https://juejin.cn/post/7231197051203256379
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。