(轉)萬字長文——Go 語言現代命令列框架 Cobra 詳解

liujiacai發表於2024-09-11

原文:https://juejin.cn/post/7231197051203256379

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

概念

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

  • 命令(COMMAND):命令表示要執行的操作。

  • 引數(ARG):是命令的引數,一般用來表示操作的物件。

  • 標誌(FLAG):是命令的修飾,可以調整操作的行為。

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

要編寫一個好的命令列程式,需要遵循的模式是 APPNAME VERB NOUN --ADJECTIVEAPPNAME COMMAND ARG --FLAG

在這裡 VERB 代表動詞,NOUN 代表名詞,ADJECTIVE 代表形容詞。

以下是一個現實世界中好的命令列程式的例子:

bash
程式碼解讀
複製程式碼
$ hugo server --port=1313

以上示例中,server 是一個命令(子命令),port 是一個標誌(1313 是標誌的引數,但不是命令的引數 ARG)。

下面是一個 git 命令的例子:

bash
程式碼解讀
複製程式碼
$ git clone URL --bare

以上示例中,clone 是一個命令(子命令),URL 是命令的引數,bare 是標誌。

快速開始

要使用 Cobra 建立命令列程式,需要先透過如下命令進行安裝:

bash
程式碼解讀
複製程式碼
$ go get -u github.com/spf13/cobra/cobra

安裝好後,就可以像其他 Go 語言庫一樣匯入 Cobra 包並使用了。

go
程式碼解讀
複製程式碼
import "github.com/spf13/cobra"

建立一個命令

假設我們要建立的命令列程式叫作 hugo,可以編寫如下程式碼建立一個命令:

hugo/cmd/root.go

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

go
程式碼解讀
複製程式碼
package main

import (
	"hugo/cmd"
)

func main() {
	cmd.Execute()
}

main.go 程式碼實現非常簡單,只在 main 函式中呼叫了 cmd.Execute() 函式,來執行命令。

編譯並執行命令

現在,我們就可以編譯並執行這個命令列程式了。

bash
程式碼解讀
複製程式碼
# 編譯
$ go build -o hugo
# 執行
$ ./hugo
run hugo...

筆記:示例程式碼裡沒有列印 Run 函式的 args 引數內容,你可以自行列印看看結果(提示:args 為命令列引數列表)。

以上我們編譯並執行了 hugo 程式,輸出內容正是 cobra.Command 結構體中 Run 函式內部程式碼的執行結果。

我們還可以使用 --help 檢視這個命令列程式的使用幫助。

bash
程式碼解讀
複製程式碼
$ ./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 推薦寫法。

當前專案目錄結構如下:

bash
程式碼解讀
複製程式碼
$ 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

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)
}

現在重新編譯並執行命令列程式。

bash
程式碼解讀
複製程式碼
$ go build -o hugo
$ ./hugo version                       
Hugo Static Site Generator v0.9 -- HEAD

可以發現 version 命令已經被加入進來了。

再次檢視幫助資訊:

bash
程式碼解讀
複製程式碼
$ ./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 為新新增的子命令。

檢視子命令幫助資訊:

bash
程式碼解讀
複製程式碼
$ ./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 上。

go
程式碼解讀
複製程式碼
var Verbose bool
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")

本地標誌

標誌也可以是本地的,這意味著它只適用於該指定命令。

go
程式碼解讀
複製程式碼
var Source string
rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")

父命令的本地標誌

預設情況下,Cobra 僅解析目標命令上的本地標誌,忽略父命令上的本地標誌。透過在父命令上啟用 Command.TraverseChildren 屬性,Cobra 將在執行目標命令之前解析每個命令的本地標誌。

go
程式碼解讀
複製程式碼
var rootCmd = &cobra.Command{
	Use:   "hugo",
	TraverseChildren: true,
}

提示:如果你不理解,沒關係,繼續往下看,稍後會有示例程式碼演示講解。

必選標誌

預設情況下,標誌是可選的。我們可以將其標記為必選,如果沒有提供,則會報錯。

go
程式碼解讀
複製程式碼
var Region string
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")

定義好以上幾個標誌後,為了展示效果,我們對 rootCmd.Run 方法做些修改,分別列印 VerboseSourceRegion 幾個變數。

go
程式碼解讀
複製程式碼
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

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,來對上面新增的這幾個標誌進行測試。

bash
程式碼解讀
複製程式碼
$ 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 命令:

bash
程式碼解讀
複製程式碼
$ ./hugo -r test-region
run hugo...
Verbose: false
Source: 
Region: test-region

現在 -r/--region 為必選標誌,不傳將會得到 Error: required flag(s) "region" not set 報錯。

執行 print 子命令:

bash
程式碼解讀
複製程式碼
$ ./hugo print -f test-flag
run print...
printFlag: test-flag
Source: 

以上執行結果可以發現,父命令的標誌 Source 內容為空。

現在使用如下命令執行 print 子命令:

bash
程式碼解讀
複製程式碼
$ ./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 報錯。

bash
程式碼解讀
複製程式碼
# 指定 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() 來獲取標誌的值了。

go
程式碼解讀
複製程式碼
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() 來初始化配置檔案。

go
程式碼解讀
複製程式碼
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 函式的列印程式碼:

go
程式碼解讀
複製程式碼
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 配置檔案內容如下:

yaml
程式碼解讀
複製程式碼
username: jianghushinian
password: 123456
server:
  ip: 127.0.0.1
  port: 8080

現在重新編譯並執行 hugo 命令:

bash
程式碼解讀
複製程式碼
# 編譯
$ 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 內建了以下驗證函式:

  • NoArgs:如果存在任何命令引數,該命令將報錯。

  • ArbitraryArgs:該命令將接受任意引數。

  • OnlyValidArgs:如果有任何命令引數不在 CommandValidArgs 欄位中,該命令將報錯。

  • MinimumNArgs(int):如果沒有至少 N 個命令引數,該命令將報錯。

  • MaximumNArgs(int):如果有超過 N 個命令引數,該命令將報錯。

  • ExactArgs(int):如果命令引數個數不為 N,該命令將報錯。

  • ExactValidArgs(int):如果命令引數個數不為 N,或者有任何命令引數不在 CommandValidArgs 欄位中,該命令將報錯。

  • RangeArgs(min, max):如果命令引數的數量不在預期的最小數量 min 和最大數量 max 之間,該命令將報錯。

內建驗證函式用法如下:

go
程式碼解讀
複製程式碼
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 命令:

bash
程式碼解讀
複製程式碼
# 編譯
$ 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

當然,我們也可以自定義驗證函式:

go
程式碼解讀
複製程式碼
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 命令:

bash
程式碼解讀
複製程式碼
# 編譯
$ 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 如下:

go
程式碼解讀
複製程式碼
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 命令:

bash
程式碼解讀
複製程式碼
# 編譯
$ go build -o hugo
# 執行
$ ./hugo
hugo PersistentPreRun
hugo PreRun
run hugo...
hugo PostRun
hugo PersistentPostRun

輸出順序符合預期。

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

bash
程式碼解讀
複製程式碼
$ ./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 函式同理。

go
程式碼解讀
複製程式碼
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 命令:

bash
程式碼解讀
複製程式碼
# 編譯
$ 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 函式。

go
程式碼解讀
複製程式碼
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 命令:

bash
程式碼解讀
複製程式碼
# 編譯
$ go build -o hugo
# 執行子命令
$ ./hugo version  
version PersistentPreRun
version PreRun
Hugo Static Site Generator v0.9 -- HEAD
hugo PersistentPostRun

定義自己的 Help 命令

如果你對 Cobra 自動生成的幫助命令不滿意,我們可以自定義幫助命令或模板。

go
程式碼解讀
複製程式碼
cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)

Cobra 提供了三個方法來實現自定義幫助命令,後兩者也適用於任何子命令。

預設情況下,我們可以使用 hugo help command 語法檢視子命令的幫助資訊,也可以使用 hugo command -h/--help 檢視。

使用 help 命令檢視幫助資訊:

bash
程式碼解讀
複製程式碼
$ ./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 檢視幫助資訊:

bash
程式碼解讀
複製程式碼
$ ./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 輸出。

go
程式碼解讀
複製程式碼
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 命令:

bash
程式碼解讀
複製程式碼
# 編譯
$ 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 語法,自定義模板如下:

go
程式碼解讀
複製程式碼
rootCmd.SetHelpTemplate(`Custom Help Template:
Usage:
	{{.UseLine}}
Description:
	{{.Short}}
Commands:
{{- range .Commands}}
	{{.Name}}: {{.Short}}
{{- end}}
`)

注意:為了單獨測試 cmd.SetHelpTemplate(s string),我已將上面 rootCmd.SetHelpCommandrootCmd.SetHelpFunc 部分程式碼註釋掉了。

重新編譯並執行 hugo 命令:

bash
程式碼解讀
複製程式碼
# 編譯
$ 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 時,將得到如下輸出:

bash
程式碼解讀
複製程式碼
$ ./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 提供瞭如下兩個方法,來控制輸出,具體效果我就不演示了,留給讀者自行探索。

go
程式碼解讀
複製程式碼
cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)

未知命令建議

在我們使用 git 命令時,有一個非常好用的功能,能夠對使用者輸錯的未知命令智慧提示。

示例如下:

bash
程式碼解讀
複製程式碼
$ 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

bash
程式碼解讀
複製程式碼
$ ./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

如果你想徹底關閉此功能,可以使用如下設定:

go
程式碼解讀
複製程式碼
command.DisableSuggestions = true

或者使用如下設定調整字串匹配的最小距離:

go
程式碼解讀
複製程式碼
command.SuggestionsMinimumDistance = 1

SuggestionsMinimumDistance 是一個正整數,表示輸錯的命令與正確的命令最多有幾個不匹配的字元(最小距離),才會給出建議。如當值為 1 時,使用者輸入 hugo versiox 會給出建議,而如果使用者輸入 hugo versixx 時,則不會給出建議,因為已經有兩個字母不匹配 version 了。

Shell 補全

前文在講新增子命令小節時,我們見到過 completion 子命令,可以為指定的 Shell 生成自動補全指令碼,現在我們就來講解它的用法。

直接執行 hugo completion 命令,我們可以檢視它支援的幾種 Shell 型別 bashfishpowershellzsh

bash
程式碼解讀
複製程式碼
$ ./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 型別,可以使用如下命令:

zsh
程式碼解讀
複製程式碼
$ echo $0   
/bin/zsh

可以發現,我使用的是 zsh,所以我就以 zsh 為例,來演示下 completion 命令補全用法。

使用 -h/--help 我們可以檢視使用說明:

bash
程式碼解讀
複製程式碼
$ ./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 提供了不同命令。

你可以根據提示選擇自己喜歡的方式來實現命令列補全功能。

我這裡只實現為當前會話提供命令列補全功能為例進行演示:

bash
程式碼解讀
複製程式碼
# 首先在專案根目錄下,安裝 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

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() 函式。

go
程式碼解讀
複製程式碼
func main() {
	cmd.Execute()
	cmd.GenDocs()
}

現在,重新編譯並執行 hugo 即可生成文件:

bash
程式碼解讀
複製程式碼
# 編譯
$ 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 命令列工具,可以透過命令的方式快速建立一個命令列專案。

安裝:

bash
程式碼解讀
複製程式碼
$ go install github.com/spf13/cobra-cli@latest

檢視使用幫助:

bash
程式碼解讀
複製程式碼
$ 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,模組初始化過程如下:

bash
程式碼解讀
複製程式碼
# 建立專案目錄
$ mkdir cog
# 進入專案目錄
$ cd cog
# 初始化模組
$ go mod init github.com/jianghushinian/blog-go-example/cobra/getting-started/cog

初始化命令列程式

有了初始化好的 Go 專案,我們就可以初始化命令列程式了。

bash
程式碼解讀
複製程式碼
# 初始化程式
$ 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 檔案內容如下:

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 檔案內容如下:

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 管理配置。

bash
程式碼解讀
複製程式碼
$ cobra-cli init --author "jianghushinian" --license mit --viper
Your Cobra application is ready at

以上命令我們指定可選標誌後對專案進行了重新初始化。

現在 LICENSE 檔案內容不再為空,而是 MIT 協議。

LICENSE
程式碼解讀
複製程式碼
The MIT License (MIT)

Copyright © 2023 jianghushinian

Permission is hereby granted...

並且 Go 檔案 Copyright 頭資訊中作者資訊也會被補全。

go
程式碼解讀
複製程式碼
/*
Copyright © 2023 jianghushinian

...
*/

筆記:cobra-cli 命令內建開源許可證支援 GPLv2GPLv3LGPLAGPLMIT2-Clause BSD3-Clause BSD。也可以參考官方文件來指定自定義許可證。

提示:如果你對開源許可證不熟悉,可以參考我的另一篇文章《開源協議簡介》

新增命令

我們可以使用 add 命令為程式新增新的命令,並且 add 命令同樣支援可選標誌。

bash
程式碼解讀
複製程式碼
$ 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 語言變數命名規範。

現在命令列程式目錄結構如下:

bash
程式碼解讀
複製程式碼
$ tree .
.
├── LICENSE
├── cmd
│   ├── config.go
│   ├── create.go
│   ├── root.go
│   └── serve.go
├── go.mod
├── go.sum
└── main.go

2 directories, 8 files

可以使用如下命令執行子命令:

bash
程式碼解讀
複製程式碼
$ go run main.go config create
create called

其他新新增的命令同理。

使用配置取代標誌

如果你不想每次生成或新增命令時都指定選項引數,則可以定義 ~/.cobra.yaml 檔案來儲存配置資訊:

yaml
程式碼解讀
複製程式碼
author: jianghushinian <jianghushinian007@outlook.com>
year: 2023
license: MIT
useViper: true

再次使用 init 命令初始化程式:

bash
程式碼解讀
複製程式碼
$ cobra-cli init                                                                      
Using config file: /Users/jianghushinian/.cobra.yaml

會提示使用了 ~/.cobra.yaml 配置檔案。

現在 LICENSE 檔案內容格式如下:

LICENSE
程式碼解讀
複製程式碼
The MIT License (MIT)

Copyright © 2023 jianghushinian <jianghushinian007@outlook.com>

...

Go 檔案 Copyright 頭資訊也會包含日期、使用者名稱、使用者郵箱。

go
程式碼解讀
複製程式碼
/*
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 上,歡迎點選檢視。

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

聯絡我

  • 微信: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
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

相關文章