Golang 之使用 Cobra

【導讀】go 命令行工具 cobra 非常強大易用,Kubernetes 源碼閱讀過程中也能見到這個框架。本文介紹了基於 cobra 的 cli 工具開發詳細說明。

Cobra 介紹

Cobra 是一個庫,其提供簡單的接口來創建強大現代的 CLI 接口,類似於 git 或者 go 工具。同時,它也是一個應用,用來生成個人應用框架,從而開發以 Cobra 爲基礎的應用。Docker 源碼中使用了 Cobra。

概念

Cobra 基於三個基本概念commands,argumentsflags。其中 commands 代表行爲,arguments 代表數值,flags 代表對行爲的改變。

基本模型如下:

APPNAME VERB NOUN --ADJECTIVE或者APPNAME COMMAND ARG --FLAG

例如:

# server是commands,port是flag
hugo server --port=1313

# clone是commands,URL是arguments,brae是flags
git clone URL --bare

Commands 是應用的中心點,同樣 commands 可以有子命令 (children commands),其分別包含不同的行爲。

Commands 的結構體如下:

type Command struct {
    Use string // The one-line usage message.
    Short string // The short description shown in the 'help' output.
    Long string // The long message shown in the 'help <this-command>' output.
    Run func(cmd *Command, args []string) // Run runs the command.
}

Flags 用來改變 commands 的行爲。其完全支持 POSIX 命令行模式和 Go 的 flag 包。這裏的 flag 使用的是 spf13/pflag 包,具體可以參考 Golang 之使用 Flag 和 Pflag.

安裝與導入

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

Cobra 文件結構

cjapp 的基本結構

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

main.go

其目的很簡單,就是初始化 Cobra。其內容基本如下:

package main

import (
  "fmt"
  "os"

  "{pathToYourApp}/cmd"
)

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

使用 cobra 生成器

windows 系統下使用:

go get github.com/spf13/cobra/cobra

或者在文件夾 github.com/spf13/cobra/cobra 下使用go install$GOPATH/bin路徑下生成cobra.exe可執行命令。

cobra init

命令cobra init [yourApp]將會創建初始化應用,同時提供正確的文件結構。同時,其非常智能,你只需給它一個絕對路徑,或者一個簡單的路徑。

cobra.exe init cjapp

<<'COMMENT'
Your Cobra application is ready at
/home/chenjian/gofile/src/cjapp.

Give it a try by going there and running `go run main.go`.
Add commands to it by running `cobra add [cmdname]`.
COMMENT
ls -Ra /home/chenjian/gofile/src/cjapp

<<'COMMENT'
/home/chenjian/gofile/src/cjapp:
.  ..  cmd  LICENSE  main.go

/home/chenjian/gofile/src/cjapp/cmd:
.  ..  root.go
COMMENT
cobra add
在路徑C:\Users\chenjian\GoglandProjects\src\cjapp下分別執行:

cobra add serve
<<'COMMENT'
serve created at /home/chenjian/gofile/src/cjapp/cmd/serve.go
COMMENT

cobra add config

<<'COMMENT'
config created at /home/chenjian/gofile/src/cjapp/cmd/config.go
COMMENT

cobra add create -p 'configCmd'
<<'COMMENT'
create created at /home/chenjian/gofile/src/cjapp/cmd/create.go
COMMENT

ls -Ra /home/chenjian/gofile/src/cjapp

<<'COMMENT'
/home/chenjian/gofile/src/cjapp:
.  ..  cmd  LICENSE  main.go

/home/chenjian/gofile/src/cjapp/cmd:
.  ..  config.go  create.go  root.go  serve.go
COMMENT

此時你可以使用:

go run main.go

<<'COMMENT'
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.

Usage:
  cjapp [command]

Available Commands:
  config      A brief description of your command
  help        Help about any command
  serve       A brief description of your command

Flags:
      --config string   config file (default is $HOME/.cjapp.yaml)
  -h, --help            help for cjapp
  -t, --toggle          Help message for toggle

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

go run main.go config
<<'COMMENT'
config called
COMMENT

go run main.go serve
<<'COMMENT'
serve called
COMMENT

go run main.go config create
<<'COMMENT'
create called
COMMENT

cobra 生成器配置

Cobra 生成器通過~/.cjapp.yaml(Linux 下) 或者 $HOME/.cjapp.yaml(windows) 來生成 LICENSE。

一個. cjapp.yaml 格式例子如下:

author: Chen Jian <chenjian158978@gmail.com>
license: MIT

或者可以自定義 LICENSE:

license:
  header: This file is part of {{ .appName }}.
  text: |
    {{ .copyright }}

    This is my license. There are many like it, but this one is mine.
    My license is my best friend. It is my life. I must master it as I must
    master my life.

人工構建 Cobra 應用

人工構建需要自己創建main.go文件和RootCmd文件。例如創建一個 Cobra 應用cjappmanu

RootCmd 文件

路徑爲cjappmanu/cmd/root.go

package cmd

import (
 "fmt"
 "os"

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

var RootCmd = &cobra.Command{
 Use:     "chenjian",
 Aliases: []string{"cj", "ccccjjjj"},
 Short:   "call me jack",
 Long: `A Fast and Flexible Static Site Generator built with
                love by spf13 and friends in Go.
                Complete documentation is available at https://o-my-chenjian.com`,
 Run: func(cmd *cobra.Command, args []string) {
  fmt.Printf("OK")
 },
}

var cfgFile, projectBase, userLicense string

func init() {
 cobra.OnInitialize(initConfig)

 // 在此可以定義自己的flag或者config設置,Cobra支持持久標籤(persistent flag),它對於整個應用爲全局
 // 在StringVarP中需要填寫`shorthand`,詳細見pflag文檔
 RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (defalut in $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")

 // Cobra同樣支持局部標籤(local flag),並只在直接調用它時運行
 RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

 // 使用viper可以綁定flag
 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 Execute()  {
 RootCmd.Execute()
}

func initConfig() {
 // 勿忘讀取config文件,無論是從cfgFile還是從home文件
 if cfgFile != "" {
  viper.SetConfigName(cfgFile)
 } else {
  // 找到home文件
  home, err := homedir.Dir()
  if err != nil {
   fmt.Println(err)
   os.Exit(1)
  }

  // 在home文件夾中搜索以“.cobra”爲名稱的config
  viper.AddConfigPath(home)
  viper.SetConfigName(".cobra")
 }
 // 讀取符合的環境變量
 viper.AutomaticEnv()

 if err := viper.ReadInConfig(); err != nil {
  fmt.Println("Can not read config:", viper.ConfigFileUsed())
 }
}

main.go

main.go的目的就是初始化 Cobra

package main

import (
 "fmt"
 "os"

 "cjappmanu/cmd"
)

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

附加命令

附加命令可以在/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 ChenJian",
 Long:  `All software has versions. This is Hugo's`,
 Run: func(cmd *cobra.Command, args []string) {
  fmt.Println("Chen Jian Version: v1.0 -- HEAD")
 },
}

同時,可以將命令添加到父項中,這個例子中 RootCmd 便是父項。只需要添加:

RootCmd.AddCommand(versionCmd)

處理 Flags

Persistent Flags

persistent 意思是說這個 flag 能任何命令下均可使用,適合全局 flag:

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

Local Flags

Cobra 同樣支持局部標籤 (local flag),並只在直接調用它時運行

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

Bind flag with Config

使用viper可以綁定 flag

var author string

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

Positional and Custom Arguments

Positional Arguments

Leagacy arg validation 有以下幾類:

Custom Arguments

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

實例

將 root.go 修改爲以下:

package cmd

import (
 "fmt"
 "os"
 "strings"

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

var cfgFile string
var echoTimes int

var RootCmd = &cobra.Command{
 Use: "app",
}

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

func init() {
 cobra.OnInitialize(initConfig)

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

 // 兩個頂層的命令,和一個cmdEcho命令下的子命令cmdTimes
 RootCmd.AddCommand(cmdPrint, cmdEcho)
 cmdEcho.AddCommand(cmdTimes)
}

func Execute() {
 RootCmd.Execute()
}

func initConfig() {
 // 勿忘讀取config文件,無論是從cfgFile還是從home文件
 if cfgFile != "" {
  viper.SetConfigName(cfgFile)
 } else {
  // 找到home文件
  home, err := homedir.Dir()
  if err != nil {
   fmt.Println(err)
   os.Exit(1)
  }

  // 在home文件夾中搜索以“.cobra”爲名稱的config
  viper.AddConfigPath(home)
  viper.SetConfigName(".cobra")
 }
 // 讀取符合的環境變量
 viper.AutomaticEnv()

 if err := viper.ReadInConfig(); err != nil {
  fmt.Println("Can not read config:", viper.ConfigFileUsed())
 }
}

操作如下:

go run main.go

<<'COMMENT'
Usage:
  app [command]

Available Commands:
  echo        Echo anything to the screen
  help        Help about any command
  print       Print anything to the screen
  version     Print the version number of ChenJian

Flags:
  -h, --help   help for app

Use "app [command] --help" for more information about a command.
COMMENT
go run main.go echo -h

<<'COMMENT'
echo is for echoing anything back.
Echo works a lot like print, except it has a child command.

Usage:
  app echo [string to echo] [flags]
  app echo [command]

Available Commands:
  times       Echo anything to the screen more times

Flags:
  -h, --help   help for echo

Use "app echo [command] --help" for more information about a command.
COMMENT
go run main.go echo times -h

<<'COMMENT'
echo things multiple times back to the user by providing
a count and a string.

Usage:
  app echo times [# times] [string to echo] [flags]

Flags:
  -h, --help        help for times
  -t, --times int   times to echo the input (default 1)
COMMENT
go run main.go print HERE I AM
<<'COMMENT'
Print: HERE I AM
COMMENT
go run main.go version
<<'COMMENT'
Chen Jian Version: v1.0 -- HEAD
COMMENT
go run main.go echo times WOW -t 3
<<'COMMENT'
Echo: WOW
Echo: WOW
Echo: WOW
COMMENT

自定義 help 和 usage

func (c *Command) initHelp() {
  if c.helpCommand == nil {
    c.helpCommand = &Command{
      Use:   "help [command]",
      Short: "Help about any command",
      Long: `Help provides help for any command in the application.
        Simply type ` + c.Name() + ` help [path to command] for full details.`,
      Run: c.HelpFunc(),
    }
  }
  c.AddCommand(c.helpCommand)
}

可以通過以下來自定義 help:

command.SetHelpCommand(cmd *Command)
command.SetHelpFunc(f func(*Command, []string))
command.SetHelpTemplate(s string)
return func(c *Command) error {
  err := tmpl(c.Out(), c.UsageTemplate(), c)
  return err
}

可以通過以下來自定義 help:

command.SetUsageFunc(f func(*Command) error)

command.SetUsageTemplate(s string)

先執行與後執行

Run 功能的執行先後順序如下:

錯誤處理函數

RunE 功能的執行先後順序如下:

對不明命令的建議

當遇到不明命令,會有提出一定的建議,其採用最小編輯距離算法 (Levenshtein distance)。例如:

hugo srever

<<'COMMENT'
Error: unknown command "srever" for "hugo"

Did you mean this?
        server

Run 'hugo --help' for usage.
COMMENT

如果你想關閉智能提示,可以:

command.DisableSuggestions = true

// 或者

command.SuggestionsMinimumDistance = 1

或者使用 SuggestFor 屬性來自定義一些建議,例如:

kubectl remove
<<'COMMENT'
Error: unknown command "remove" for "kubectl"

Did you mean this?
        delete

Run 'kubectl help' for usage.
COMMENT

轉自:陳健

鏈接:o-my-chenjian.com/2017/09/20/Using-Cobra-With-Golang/

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