萬字長文:Go 語言現代命令行框架 Cobra 詳解

Cobra 是一個 Go 語言開發的命令行(CLI)框架,它提供了簡潔、靈活且強大的方式來創建命令行程序。它包含一個用於創建命令行程序的庫(Cobra 庫),以及一個用於快速生成基於 Cobra 庫的命令行程序工具(Cobra 命令)。Cobra 是由 Go 團隊成員 spf13 爲 Hugo 項目創建的,並已被許多流行的 Go 項目所採用,如 Kubernetes、Helm、Docker (distribution)、Etcd 等。

概念

Cobra 建立在命令、參數和標誌這三個結構之上。要使用 Cobra 編寫一個命令行程序,需要明確這三個概念。

一個好的命令行程序在使用時讀起來像句子,用戶會自然的理解並知道如何使用該程序。

要編寫一個好的命令行程序,需要遵循的模式是 APPNAME VERB NOUN --ADJECTIVEAPPNAME 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 方法做些修改,分別打印 VerboseSourceRegion 幾個變量。

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 的效果。

如果我們將 rootCmdTraverseChildren 屬性置爲 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.CommandArgs 屬性提供了此功能。

Args 屬性類型爲一個函數:func(cmd *Command, args []string) error,可以用來驗證參數。

Cobra 內置了以下驗證函數:

內置驗證函數用法如下:

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 函數前後,我麼可以執行一些鉤子函數,其作用和執行順序如下:

  1. PersistentPreRun:在 PreRun 函數執行之前執行,對此命令的子命令同樣生效。

  2. PreRun:在 Run 函數執行之前執行。

  3. Run:執行命令時調用的函數,用來編寫命令的業務邏輯。

  4. PostRun:在 Run 函數執行之後執行。

  5. 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

輸出順序符合預期。

其中 PersistentPreRunPersistentPostRun 兩個函數對子命令同樣生效。

$ ./hugo version 
hugo PersistentPreRun
Hugo Static Site Generator v0.9 -- HEAD
hugo PersistentPostRun

以上幾個函數都有對應的 <Hooks>E 版本,E 表示 Error,即函數執行出錯將會返回 Error,執行順序不變:

  1. PersistentPreRunE

  2. PreRunE

  3. RunE

  4. PostRunE

  5. PersistentPostRunE

如果定義了 <Hooks>E 函數,則 <Hooks> 函數不會執行。比如同時定義了 RunRunE,則只會執行 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

可以發現,雖然同時定義了 PersistentPreRunPersistentPreRunE 兩個鉤子函數,但只有 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.SetHelpCommandRun 函數的執行輸出。使用 -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.SetHelpCommandrootCmd.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 類型 bashfishpowershellzsh

$ ./hugo completion  
Generate the autocompletion script for hugo for the specified shell.
See each sub-command'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 支持生成 MarkdownReStructured TextMan 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 命令內置開源許可證支持 GPLv2GPLv3LGPLAGPLMIT2-Clause BSD3-Clause BSD。也可以參考官方文檔來指定自定義許可證。

提示:如果你對開源許可證不熟悉,可以參考我的另一篇文章《開源協議簡介》: https://jianghushinian.cn/2023/01/15/open-source-license-introduction/。

添加命令

我們可以使用 add 命令爲程序添加新的命令,並且 add 命令同樣支持可選標誌。

$ cobra-cli add serve
$ cobra-cli add config
$ cobra-cli add create -p 'configCmd' --author "jianghushinian" --license mit --viper

這裏分別添加了三個命令 serveconfigcreate,前兩者都是 rootCmd 的子命令,create 命令則通過 -p 'configCmd' 參數指定爲 config 的子命令。

注意⚠️:使用 -p 'configCmd' 標誌指定當前命令的父命令時,configCmd 必須是小駝峯命名法,因爲 cobra-cliconfig 生成的命令代碼自動命名爲 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 函數 PersistentPreRunPreRunPostRunPersistentPostRun,可以分別在執行 Run 前後來處理一段邏輯。

如果覺得 Cobra 提供的默認幫助信息不能滿足需求,我們還可以定義自己的 Help 命令和 Usage Message,非常靈活。

Cobra 還支持未知命令的智能提示功能以及 Shell 自動補全功能,此外,它還支持自動生成 MarkdownReStructured TextMan Page 三種格式的文檔。這對命令行工具的使用者來說非常友好,還能極大減少開發者的工作量。

最後,Cobra 的命令行工具 cobra-cli 進一步提高了編寫命令行程序的效率,非常推薦使用。

本文完整代碼示例我放在了 GitHub 上,歡迎點擊查看。

希望此文能對你有所幫助。

聯繫我

參考

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/WZW6gBo1VGrM9bLC18Z80A