Cobra 眼睛蛇 - 強大的 Golang CLI 框架

閱讀過 k8s 源碼的同學,應該都知道k8s Schedulerkubeadmkubelet等核心組件的命令行交互全都是通過 spf13 寫的Cobra庫來實現。本文就來介紹下 Cobra 的相關概念及具體用法。(內容譯自官方文檔)

Cobra logo

關於

Cobra 是一個用於 Go 的 CLI 框架。它包含一個用於創建 CLI 應用程序的庫和一個快速生成 Cobra 程序和命令文件的工具。

它是由 Go 團隊成員 spf13 爲 hugo 創建的,已經被最流行的 Go 項目所採用。

Tips:知名 Golang 配置庫viper也是該作者開發的。

Cobra 提供:

安裝

使用 Cobra 很簡單。首先,使用go get安裝最新版本的庫。這個命令將安裝cobra生成器可執行文件以及庫及其依賴項:

go get -u github.com/spf13/cobra/cobra

接下來,在你的應用中加入 Cobra:

import "github.com/spf13/cobra"

概念

Cobra 是建立在 commands、arguments 和 flags 的結構上的。

Commands代表行爲,Args是 commands 的參數,Flags 是這些行爲的修飾符。

最好的應用程序在使用時讀起來像一個通順的句子。用戶將知道如何使用該應用程序,因爲他們會自然地理解如何使用它。

要遵循的模式是 APPNAME VERB NOUN --ADJECTIVE.APPNAME COMMAND ARG --FLAG

一些真實世界的例子可以更好地說明這一點。

在下面的例子中,'server'是命令,'port'是 flag:

hugo server --port=1313

在這個命令中,我們告訴 Git clone bare url。

git clone URL --bare

Commands

命令是應用程序的中心點。應用程序支持的每個交互都將包含在命令中。一個命令可以有子命令並且可以選擇運行一個動作。

在上面的示例中,“服務器” 是命令。

Flags

flag是一種修改命令行爲的方法。Cobra 支持完全 posix 兼容的flag以及 Go 的flag包。Cobra 命令可以定義持續到子命令的 flag,以及僅對該命令可用的 flag。

在上面的例子中,'port'是 flag。

flag 功能由 pflag 庫提供,它是 flag 標準庫的一個分支,它在添加 POSIX 合規性的同時保持相同的接口。

入門

雖然歡迎您提供自己的項目組織,但通常基於 Cobra 的應用程序將遵循以下項目結構:

  ▾ appName/
    ▾ cmd/
        add.go
        your.go
        commands.go
        here.go
      main.go

在 Cobra 應用程序中,通常 main.go 文件非常簡單。它有一個目的:初始化 Cobra。

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

使用 Cobra 生成器

Cobra 提供了自己的程序,可以創建您的應用程序並添加您想要的任何命令。這是將 Cobra 合併到您的應用程序中的最簡單方法。

在這裏 https://github.com/spf13/cobra/blob/master/cobra/README.md 您可以找到有關它的更多信息。

使用 Cobra 庫

要手動使用 Cobra,您需要創建一個乾淨的 main.go 文件和一個 rootCmd 文件。您可以選擇提供您認爲合適的其他命令。

創建 rootCmd

Cobra 不需要任何特殊的構造函數。只需創建您的命令。

理想情況下,您將它放在 app/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 http://hugo.spf13.com`,
  Run: func(cmd *cobra.Command, args []string) {
    // Do Stuff Here
  },
}

func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

您還將在 init() 函數中定義 flags 和 handle 配置。

例如 cmd/root.go:

import (
  "fmt"
  "os"

  homedir "github.com/mitchellh/go-homedir"
  "github.com/spf13/cobra"
  "github.com/spf13/viper"
)

func init() {
  cobra.OnInitialize(initConfig)
  rootCmd.PersistentFlags().StringVar(&cfgFile, "config""""config file (default is $HOME/.cobra.yaml)")
  rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase""b""""base project directory eg. github.com/spf13/")
  rootCmd.PersistentFlags().StringP("author""a""YOUR NAME""Author name for copyright attribution")
  rootCmd.PersistentFlags().StringVarP(&userLicense, "license""l""""Name of license for the project (can provide `licensetext` in config)")
  rootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
  viper.BindPFlag("projectbase", rootCmd.PersistentFlags().Lookup("projectbase"))
  viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
  viper.SetDefault("author""NAME HERE <EMAIL ADDRESS>")
  viper.SetDefault("license""apache")
}

func initConfig() {
  // Don't forget to read config either from cfgFile or from home directory!
  if cfgFile != "" {
    // Use config file from the flag.
    viper.SetConfigFile(cfgFile)
  } else {
    // Find home directory.
    home, err := homedir.Dir()
    if err != nil {
      fmt.Println(err)
      os.Exit(1)
    }

    // Search config in home directory with name ".cobra" (without extension).
    viper.AddConfigPath(home)
    viper.SetConfigName(".cobra")
  }

  if err := viper.ReadInConfig(); err != nil {
    fmt.Println("Can'read config:", err)
    os.Exit(1)
  }
}

創建你的 main.go

使用 root 命令,您需要讓主函數執行它。爲清楚起見,應在根目錄上運行 Execute,儘管它可以在任何命令上調用。

在 Cobra 應用程序中,通常 main.go 文件非常簡單。它的一個目的是初始化 Cobra。

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

創建附加命令

可以定義其他命令,通常每個命令在 cmd/ 目錄中都有自己的文件。

如果你想創建一個版本命令,你可以創建 cmd/version.go 並用以下內容填充它:

package cmd

import (
  "fmt"

  "github.com/spf13/cobra"
)

func init() {
  rootCmd.AddCommand(versionCmd)
}

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

使用 Flags

Flags 提供修飾符來控制操作命令的操作方式。

爲命令分配 Flags

由於 Flags 是在不同的位置定義和使用的,因此我們需要在外部定義一個具有正確範圍的變量來分配要使用的 Flags。

var Verbose bool
var Source string

有兩種不同的方法來分配標誌。

持久 flag

flag 可以是 “持久的”,這意味着該 flag 將可用於分配給它的命令以及該命令下的每個命令。對於全局 flag,將 flag 分配爲根上的持久 flag。

rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose""v", false, "verbose output")

本地 flag

也可以在本地分配一個 flag,它只適用於該特定命令。

rootCmd.Flags().StringVarP(&Source, "source""s""""Source directory to read from")

父命令的本地 flag

默認情況下,Cobra 僅解析目標命令上的本地 flag,忽略父命令上的任何本地 flag。通過啓用Command.TraverseChildrenCobra 將在執行目標命令之前解析每個命令的本地 flag。

command := cobra.Command{
  Use: "print [OPTIONS] [COMMANDS]",
  TraverseChildren: true,
}

使用配置綁定標誌

你也可以用 viper https://github.com/spf13/viper 綁定你的標誌:

var author string

func init() {
  rootCmd.PersistentFlags().StringVar(&author, "author""YOUR NAME""Author name for copyright attribution")
  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}

在此示例中,持久 flag author與 綁定 viper。請注意,當用戶未提供標誌時,變量 author 將不會設置爲配置中的值。--author

在本例中,持久標誌author與 viper 綁定。注意,當 user 沒有提供--author flag 時,變量 author 不會被設置爲 config 中的值。

更多信息請參見 viper 文檔。

必需的 flag

默認情況下,flag 是可選的。相反,如果您希望您的命令在未設置 flag 時報告錯誤,請將其標記爲必需:

rootCmd.Flags().StringVarP(&Region, "region""r""""AWS region (required)")
rootCmd.MarkFlagRequired("region")

位置和自定義參數

位置參數的驗證可以使用CommandArgs字段指定。

內置了以下驗證器:

設置自定義驗證器的示例:

var cmd = &cobra.Command{
  Short: "hello",
  Args: func(cmd *cobra.Command, args []string) error {
    if len(args) < 1 {
      return errors.New("requires at least one arg")
    }
    if myapp.IsValidColor(args[0]) {
      return nil
    }
    return fmt.Errorf("invalid color specified: %s", args[0])
  },
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("Hello, World!")
  },
}

例子

在下面的示例中,我們定義了三個命令。兩個在頂層,一個 (cmdTimes) 是頂級命令之一的子級。在這種情況下,root 是不可執行的,這意味着需要一個子命令。這是通過不爲 “rootCmd” 提供 “運行” 來實現的。

我們只爲單個命令定義了一個 flag。

有關 flag 的更多文檔,請訪問 https://github.com/spf13/pflag

package main

import (
  "fmt"
  "strings"

  "github.com/spf13/cobra"
)

func main() {
  var echoTimes int

  var cmdPrint = &cobra.Command{
    Use:   "print [string to print]",
    Short: "Print anything to the screen",
    Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdEcho = &cobra.Command{
    Use:   "echo [string to echo]",
    Short: "Echo anything to the screen",
    Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdTimes = &cobra.Command{
    Use:   "times [# times] [string to echo]",
    Short: "Echo anything to the screen more times",
    Long: `echo things multiple times back to the user by providing
a count and a string.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      for i := 0; i < echoTimes; i++ {
        fmt.Println("Echo: " + strings.Join(args, " "))
      }
    },
  }

  cmdTimes.Flags().IntVarP(&echoTimes, "times""t", 1, "times to echo the input")

  var rootCmd = &cobra.Command{Use: "app"}
  rootCmd.AddCommand(cmdPrint, cmdEcho)
  cmdEcho.AddCommand(cmdTimes)
  rootCmd.Execute()
}

更加完整示例,請查看 Hugo。

幫助命令

當您有子命令時,Cobra 會自動向您的應用程序添加幫助命令。這將在用戶運行 “應用程序幫助” 時調用。此外,幫助還將支持所有其他命令作爲輸入。比如說,你有一個名爲 “create” 的命令,沒有任何額外的配置;Cobra 將在調用 “app help create” 時工作。每個命令都會自動添加 “–help” 標誌。

例子

以下輸出由 Cobra 自動生成。除了命令和標誌定義之外,什麼都不需要。

$ cobra help

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 [command]

Available Commands:
  add         Add a command to a Cobra Application
  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
  -l, --license string   name of license for the project
      --viper            use Viper for configuration (default true)

Use "cobra [command] --help" for more information about a command.

幫助就像任何其他命令一樣只是一個命令。它沒有特殊的邏輯或行爲。事實上,如果需要,您可以提供自己的。

定義你自己的 help

您可以爲默認命令提供自己的幫助命令或您自己的模板,以與以下功能一起使用:

cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)

後兩者也適用於任何子命令。

用法信息

當用戶提供無效標誌或無效命令時,Cobra 通過向用戶顯示 “usage” 來響應。

Example

您可能會從上面的幫助中認識到這一點。這是因爲默認幫助將用法作爲其輸出的一部分嵌入。

$ cobra --invalid
Error: unknown flag: --invalid
Usage:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  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
  -l, --license string   name of license for the project
      --viper            use Viper for configuration (default true)

Use "cobra [command] --help" for more information about a command.

定義自己的 usage

您可以提供自己的使用函數或模板供 Cobra 使用。像幫助一樣,函數和模板可以通過公共方法覆蓋:

cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)

Version Flag

如果在根命令上設置了 Version 字段,Cobra 會添加一個頂級 “--version” 標誌。使用 “--version” 標誌運行應用程序將使用版本模板將版本打印到標準輸出。可以使用cmd.SetVersionTemplate(s string)函數自定義模板。

PreRun 和 PostRun 鉤子

可以在命令的 main Run函數之前或之後運行函數。PersistentPreRunPreRun函數將在Run之前執行。PersistentPostRunPostRun將在Run之後執行。如果Persistent*Run函數沒有聲明自己的,則將由子函數繼承。這些函數的運行順序如下:

下面是使用所有這些特性的兩個命令的示例。當子命令被執行時,它將運行根命令的PersistentPreRun,而不是根命令的PersistentPostRun:

package main

import (
  "fmt"

  "github.com/spf13/cobra"
)

func main() {

  var rootCmd = &cobra.Command{
    Use:   "root [sub]",
    Short: "My root command",
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args)
    },
    PreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd Run with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PersistentPostRun with args: %v\n", args)
    },
  }

  var subCmd = &cobra.Command{
    Use:   "sub [no options!]",
    Short: "My subcommand",
    PreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd Run with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PersistentPostRun with args: %v\n", args)
    },
  }

  rootCmd.AddCommand(subCmd)

  rootCmd.SetArgs([]string{""})
  rootCmd.Execute()
  fmt.Println()
  rootCmd.SetArgs([]string{"sub""arg1""arg2"})
  rootCmd.Execute()
}

輸出

Inside rootCmd PersistentPreRun with args: []
Inside rootCmd PreRun with args: []
Inside rootCmd Run with args: []
Inside rootCmd PostRun with args: []
Inside rootCmd PersistentPostRun with args: []

Inside rootCmd PersistentPreRun with args: [arg1 arg2]
Inside subCmd PreRun with args: [arg1 arg2]
Inside subCmd Run with args: [arg1 arg2]
Inside subCmd PostRun with args: [arg1 arg2]
Inside subCmd PersistentPostRun with args: [arg1 arg2]

發生 “unknown command” 時的建議

當 “unknown command” 錯誤發生時,Cobra 將自動打印建議。這使得 Cobra 在發生錯字時的行爲與 git 命令類似。例如:

$ hugo srever
Error: unknown command "srever" for "hugo"

Did you mean this?
        server

Run 'hugo --help' for usage.

建議基於註冊的每個子命令,並使用 Levenshtein distance 的實現。每個匹配最小距離爲 2(忽略大小寫) 的已註冊命令都將顯示爲建議。

如果你需要禁用建議或調整命令中的字符串距離,請使用:

command.DisableSuggestions = true

command.SuggestionsMinimumDistance = 1

您還可以使用SuggestFor屬性顯式地設置指定命令的建議名稱。這允許對字符串距離不近,但在您的命令集中有意義的字符串以及一些您不需要別名的字符串提供建議。

例子:

$ kubectl remove
Error: unknown command "remove" for "kubectl"

Did you mean this?
        delete

Run 'kubectl help' for usage.

爲您的命令生成文檔

Cobra 可以基於以下格式的子命令、標誌等生成文檔:

生成 bash 補全

Cobra 可以生成一個 bash 完成文件。如果在命令中添加更多信息,這些補全功能就會非常強大和靈活。你可以在 Bash 補全中閱讀更多信息。

官網鏈接:https://cobra.dev/

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