Go 中好用的命令行库: Cobra

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是应用的中心点,同样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

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

代码下载: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

代码下载:cjappmanu_cmd_main.go

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

代码下载: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有以下几类:

  • NoArgs: 如果包含任何位置参数,命令报错
  • ArbitraryArgs: 命令接受任何参数
  • OnlyValidArgs: 如果有位置参数不在ValidArgs中,命令报错
  • MinimumArgs(init): 如果参数数目少于N个后,命令行报错
  • MaximumArgs(init): 如果参数数目多余N个后,命令行报错
  • ExactArgs(init): 如果参数数目不是N个话,命令行报错
  • RangeArgs(min, max): 如果参数数目不在范围(min, max)中,命令行报错

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修改为以下:

代码下载:example_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

  • help

默认的help命令如下:

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)
  • usage

默认的help命令如下:

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功能的执行先后顺序如下:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

错误处理函数

RunE功能的执行先后顺序如下:

  • PersistentPreRunE
  • PreRunE
  • RunE
  • PostRunE
  • PersistentPostRunE

对不明命令的建议

当遇到不明命令,会有提出一定的建,其采用最小编辑距离算法(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
全部评论(0)