基于golang语言开发命令行工具(command line interfaces,CLI)最常用的框架是Cobra。通过Cobra可以使用简单的接口实现一个强大现代的CLI工具,许多知名的项目比如Docker、Kubernetes等都用Cobra来开发自己的命令行工具。下面我们将对Cobra的基本用法做简要介绍。

  • 概览
  • 相关概念
    • Commands(命令)
    • Flags(标志)
  • 安装
  • 开始使用
    • 使用Cobra生成器
    • 使用Cobra库
    • 使用Flags
    • 位置和自定义参数
    • 示例
    • Help Command
    • 展示Usage
    • 预处理和后处理等Hooks函数
    • 处理 unknown command 的建议
    • 生成命令行文档
    • 实现命令自动补全
概览

Cobra提供简单的接口来创建强大的现代化CLI接口,比如git与go。Cobra同时也提供一个二进制工具,用于创建命令行程序。

Cobra提供:

app serverapp fetchapp srverapp server-h--help
相关概念

Cobra的构建基于结构化的commands,arguments和flags,即命令、参数和标志。

Commands代表命令行程序执行什么命令,Args(即arguments)和Flags即这些命令的修饰符。

最好的命令行程序应该像读句子那样去使用,用户通过命令行本身就可以自然地知道这段命令执行的是什么操作。

APPNAME COMMAND ARG --FLAGAPPNAME VERB NOUN --ADJECTIVE

例如下面的例子,‘server’是一个操作, ‘port’是一个标志:

hugo server --port=1313

下面的另一个例子告诉我们,它要通过Git来执行clone操作拷贝url对应的项目到本地的裸仓库:

git clone URL --bare

Commands(命令)

Command是一个命令行程序最重要的概念。命令行程序支持的每一个交互操作都应该被包含在一个command中。一条command可以有多条子commands并且能够可选地执行它们。

在上面的例子中, ‘server’就是一条command。

Flags(标志)

一个Flag用来控制command的行为。Cobra完全兼容POSIX的flags模式,如同Go自带的标准flag库那样。一条Cobra生成的command可以将它的flags继承给它的子commadn,也可以限定这些flags只能被该command使用。

在上面的例子中,‘port’即一个flag。

更加强大的flag功能可以使用pflag库,它是一个标准flag库的扩展。

安装
go getcobra
go get -u github.com/spf13/cobra/cobra

接下来,在golang代码中引用Cobra:

import "github.com/spf13/cobra"
开始使用

通常一个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库

使用Cobra,需要创建一个main.go文件和一个rootCmd文件。当然,你也可以选择别的地方去添加额外的commands。

创建rootCmd

Cobra没有构造函数,所以直接简单地创建一个command对象就行。

这里假设下面的代码位于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和配置。

例如 cmd/root.go:

package cmd

import (
    "fmt"

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

var (
    // Used for flags.
    cfgFile     string
    userLicense string

    rootCmd = &cobra.Command{
        Use:   "cobra",
        Short: "A generator for Cobra based Applications",
        Long: `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.`,
    }
)

// Execute executes the root command.
func Execute() error {
    return rootCmd.Execute()
}

func init() {
    cobra.OnInitialize(initConfig)

    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
    rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution")
    rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project")
    rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
    viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
    viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
    viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
    viper.SetDefault("license", "apache")

    rootCmd.AddCommand(addCmd)
    rootCmd.AddCommand(initCmd)
}

func initConfig() {
    if cfgFile != "" {
        // Use config file from the flag.
        viper.SetConfigFile(cfgFile)
    } else {
        // Find home directory.
        home, err := homedir.Dir()
        if err != nil {
            er(err)
        }

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

    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
    }
}

创建main.go

在cmd/root.go文件中我们已经定义了一个名为rootCmd的command作为根,为了执行该command,还需要将其放入main.go文件中执行。

在一个Cobra程序中,main.go文件通常只干一件事,即初始化Cobra并执行command。

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

添加其他command

不同的command通常分别在不同的go文件中定义,这里假设所有command的实现都处于cmd/目录下不同的文件中。
例如,如果你想创建名为version的command,可以创建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提供控制command行为的能力。

给command绑定flags

一般情况下,我们需要预先定义变量来存储flags的值,这样便于我们各处使用它们(Cobra也支持不显式地定义和使用flags,但这种情况我们后面再说)。

如下,我们定义了Verbose和Source两个不同类型的变量来表示和存储flags值。

var Verbose bool
var Source string

Cobra有两种不同的方式给command绑定flags,分别为Persistent Flags和Local Flags。

Persistent Flags( 持久型flags)

Persistent Flags表示flag不仅绑定在一个command上,同时也绑定在了这个command的子command上。Persistent Flags可以被一个command下的所有子command使用。

例如,我们给根command(即rootCmd)绑定了一个名为‘verbose’的Persistent Flag,那么这个flag就成了一个全局flag,可以被所有command使用(因为rootCommand为根command,显然后续所有增加的command都为子command)。

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

Local Flags(本地flags)

Local Flags表示flag仅能被其绑定的command使用。

如下我们将名为'source'的flag绑定给一个名为localCmd的command,除了localCmd外,其他command无法接收到这个flag的值,即只有loalCmd能给‘Source’变量通过指定flag来赋值。

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

command定义的Local Flags只能由绑定了这些flags的command使用,但这里还有一种方便的方式,可以使得一个command所有的子command定的flags都绑定给他们的父command,只要在创建command时,指定TraverseChildren为true即可,这样父command在真正执行前会遍历其所有的子command来绑定flags。

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

给flags绑定配置

在使用命令行程序时不总会显式地提供flags的值,有时希望程序自己去环境变量、配置文件等地方自动寻找配置参数。这时我们可以通过viper库来实现这一功能,将flags的值绑定给viper。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"))
}
--author

Required flags

默认情况下,是否指定flags是可选的,如果你希望当一个flag没有设置时,命令行报错,你可以标记它为必须的

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

位置和自定义参数

CommandArgs
Args
NoArgsArbitraryArgsOnlyValidArgsCommandValidArgsMinumumNArgs(int)MaximumNArgs(int)ExactArgs(int)ExcatValidArgs(int)ValidArgsRangeArgs(min, max)

以下为一个限制位置参数的例子:

var cmd = &cobra.Command{
  Short: "hello",
  Args: func(cmd *cobra.Command, args []string) error {
    if len(args) < 1 {
      return errors.New("requires a color argument")
    }
    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!")
  },
}

示例

目前为止我们已经介绍了很多Cobra的基本用法,本节会给出一个完整的例子来回顾前面讲过的知识。

rootCmd

注意这里我们只为一个名为echoTimes的command 设置了flag。更多flags的用法参考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("Echo: " + strings.Join(args, " "))
    },
  }

  var cmdTimes = &cobra.Command{
    Use:   "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。

Help Command

app helpcreateapp help create

例子

下面的输出是Cobra自动生成的,除了command和flag的定义,我们没有对command做任何其他定制。

    $ 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 就跟其它命令一样,并没有特殊的逻辑或行为。事实上,你也可以提供你自己定义的help。

自定义Help

你可以使用下面的函数来定义自己的help:

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

后两个函数定义的help会被command的子命令继承。

展示Usage

usage

例子

    $ 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 使用。
类似于自定义help,usage的方法和模板都会覆盖默认的公共说明。

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

版本Flag

--versioncmd.SetVersionTemplate(s string)

预处理和后处理等Hooks函数

Cobra提供了多个钩子(hooks)函数的接口,你可以很容易地去决定在command执行Run中的实际函数之前或之后,需要执行哪些方法。

PersistentPreRunPreRunRun
PersistentPostRunPostRunRun
Persistent*Run

钩子函数的执行顺序如下:

PersistentPreRunPreRunRunPostRunPersistentPostRun
PersistentPreRunPersistentPostRun
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 的建议

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可以基于command、flags等来自动生成文档,支持下面几种格式:

实现命令自动补全

Cobra还提供了自动生成bash或zsh自动补全脚本的功能,这部分参见