基于Cobra的golang命令行工具开发

基于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 fetch
  • flags兼容posix模式(包括长、短版本)
  • 支持子命令嵌套
  • 支持全局、局部以及继承falgs
  • 智能提示,如app srver,将提示srver子命令不存在,是否为app server
  • 自动生成子命令及其flags
  • 自动生成-h--help等flags提醒
  • 自动生成命令行docs和man文件
  • 支持命令行别名
  • 灵活定义help和usage信息
  • 可与viper库结合使用,方便参数、配置和环境变量的管理

相关概念


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。

点击查看更多有关Cobra Command的信息

Flags(标志)


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

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

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

安装


使用Cobra十分简单。首先通过go get安装最新版本的代码库及相关依赖,这条命令同时也会安装cobra可执行程序:

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生成器的使用,关于该工具的详细信息点击此处

使用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这个flag绑定给了viper,viper一般会按照flag、环境变量、配置文件和默认值的优先级去寻找环境变量,当用户没有通过--author给出flag值时,viper会降低优先级去别处寻找匹配的参数值。

Required flags

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

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

位置和自定义参数


验证和限制命令行程序的位置参数可以通过CommandArgs字段来实现。

Cobra内置有下列验证函数给Args字段使用:

  • NoArgs - 不允许有位置参数
  • ArbitraryArgs - 可以接受任意多个位置参数
  • OnlyValidArgs - 只允许指定CommandValidArgs字段里指定的位置参数
  • MinumumNArgs(int) - 限定最少提供多少个位置参数
  • MaximumNArgs(int) - 限定最多能提供多少个位置参数
  • ExactArgs(int) - 限定必须提供多少个对应的位置参数
  • ExcatValidArgs(int) - 限定必须提供多少个对应的位置参数,并且位置参数必须位于ValidArgs字段
  • RangeArgs(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的基本用法,本节会给出一个完整的例子来回顾前面讲过的知识。

在下面的例子中,我们定义了3个commands。两个commands为顶级命令,一个command为顶级命令的子命令。在这个例子中,由于rootCmd没有为Run字段提供方法,所以单独的root是不能运行的,必须要有子commands。

注意这里我们只为一个名为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


当你的程序有子命令时,Cobra 会自动给你程序添加help命令。当你运行app help,会调用help命令。另外,help同样支持其它输入命令。例如,你有一个没有任何其它配置的命令叫create,当你调用app help create Corbra 将会起作用。

例子

下面的输出是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


当用户错误的使用命令行程序时(如指定非法的flag和command),Cobra将会自动显示命令行程序的用法说明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


当顶级Command的Version字段被定义后,Cobra会自动为顶级command添加一个--versionflag。当命令行程序指定该flag时,Cobra会调用内置的版本函数打印命令行程序的相关版本信息。当然,你也可以使用cmd.SetVersionTemplate(s string)函数来自定义版本信息的展示内容。

预处理和后处理等Hooks函数


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

PersistentPreRunPreRun函数会在Run之前执行。

PersistentPostRunPostRun函数将会在Run之后执行。

Persistent*Run这种模式的函数会被子command继承。

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

  • PersistentPreRun

  • PreRun

  • Run

  • PostRun

  • PersistentPostRun

如下的例子使用了上面提到的所有钩子函数。需要注意的是,当子command执行时,它会执行根command的PersistentPreRun函数而不会执行根command的PersistentPostRun函数(因为子command自己定义了该函数)。

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


Cobra在unknown command错误发生时,会自动打印建议。这就让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可以基于command、flags等来自动生成文档,支持下面几种格式:

实现命令自动补全


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342