总览

Cobra是spf13写的一个编写/生成交互式命令程序的框架,github地址是:https://github.com/spf13/cobra。有很多知名的开源项目都使用了这个框架:

  • Kubernetes
  • Hugo
  • rkt
  • etcd
  • Docker (distribution)
  • OpenShift
  • Delve
  • GopherJS
  • CockroachDB
  • Bleve
  • ProjectAtomic (enterprise)
  • Parse (CLI)
  • GiantSwarm's swarm
  • Nanobox/Nanopack

了解了这个框架,再去看这些Kubernetes、etcd、Registry等开源项目的代码时,也就大概知道如何去看了,这也是我学习cobra的目的。spf13也写了一个viper框架,主要是用于处理配置文件的,功能非常强大,结合cobra使用时,就更加的niubility了,有兴趣的也可以了解下。cobra的README.md文件非常详细的介绍了该框架的使用方法,本文也基本是精简翻译了这个文档。

使用cobra我们可以很容器的开发类似git、go这样的交互式命令行工具。它提供了以下特性(这个涉及一些术语,翻译比较别扭,就直接使用原文了):

  • Easy subcommand-based CLIs: app server, app fetch, etc.
  • Fully POSIX-compliant flags (including short & long versions)
  • Nested subcommands
  • Global, local and cascading flags
  • Easy generation of applications & commands with cobra create appname & cobra add cmdname
  • Intelligent suggestions (app srver... did you mean app server?)
  • Automatic help generation for commands and flags
  • Automatic detailed help for app help [command]
  • Automatic help flag recognition of -h, --help, etc.
  • Automatically generated bash autocomplete for your application
  • Automatically generated man pages for your application
  • Command aliases so you can change things without breaking them
  • The flexibilty to define your own help, usage, etc.
  • Optional tight integration with viper for 12-factor apps

总之,cobra设计的非常强大和人性,不过在具体上手之前,我们先来看一些概念和结构。cobra开发的应用结构由三部分组成:命令(Command)参数(Args)标志(Flag)

一个命令的结构如下:

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.
}

注释里面对于各个字段的说明已经非常清楚了,前三个字段是不同场景下打印的对于命令的说明,由简到详,最后一个是这个命令要执行的动作。

cobra的标志完全兼容POSIX格式的参数,即有长格式的命令和短格式的命令。而且标志可以只对该命令有效,也可以对该命令的子命令有效。

cobra按照树状来组织它生成的代码,这个树定义了程序的结构,当一个命令执行的时候,就从该树中找到最终需要执行的命令。

OK,现在我们对于cobra已经有了一个大概的认识了,下面我们通过具体的例子来看如何使用这个强大的库吧。

入门

cobra的安装与使用和其他的go库相同:

# 安装
go get -v github.com/spf13/cobra/cobra

# 在代码中引入cobra就可以使用了
import "github.com/spf13/cobra"

一般基于cobra的程序的代码结构如下:

▾ appName/
    ▾ 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提供了一个cobra工具,我们安装完以后,如果没有什么问题,在$GOBIN目录下应该会有一个可执行的cobra文件,我们既可以自己手动写应用,也可以使用该工具来自动生成应用框架以及一些命令支持。如果你使用过beego框架的话,对这个应该不陌生,它就类似于beego的bee命令。下面我们演示使用cobra工具生成应用,当然当你对cobra的机理熟悉了以后,完全可以手写应用框架。

cobra init [yourApp]

这个命令可以帮我们生成一个代码框架。在GOPATH/src目录下执行cobra init cobra_exp1便可生成一个机遇cobra的工程:

➜ src cobra init cobra_exp1
Your Cobra application is ready at
/Users/Allan/workspace/gopath/src/cobra_exp1
Give it a try by going there and running `go run main.go`
Add commands to it by running `cobra add [cmdname]`

生成的代码结构如下:

➜  cobra_exp1 ll -R
.:
total 16K
-rw-r--r-- 1 Allan 12K  1 15 13:01 LICENSE
drwxr-xr-x 3 Allan 102  1 15 13:01 cmd
-rw-r--r-- 1 Allan 676  1 15 13:01 main.go

./cmd:
total 4.0K
-rw-r--r-- 1 Allan 2.7K  1 15 13:01 root.go

我们看到生成的代码中包含一个main.go、LICENSE和一个cmd目录,cmd目录里面只有一个root.go文件。需要注意的是,cobra也帮我们生成了LICENSE,这个LICENSE是根据~/.cobra.yaml文件生成的,比如我的文件是:

➜  cobra_exp1 cat ~/.cobra.yaml
author: Allan Ni <allan_ni@163.com>
license: MIT

# 生成的LICENSE
➜  cobra_exp1 cat LICENSE
The MIT License (MIT)

Copyright © 2017 Allan Ni

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

当然,源代码里面也是有生成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. 

我们看下cmd/root.go文件(为了节省空间,删掉了版权信息,内容和LICENSE内容是一样的。后续的文件也这样处理):

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var cfgFile string

// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
    Use:   "cobra_exp1",
    Short: "A brief description of your application",
    Long: `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.`,
// Uncomment the following line if your bare application
// has an action associated with it:
//    Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
    if err := RootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(-1)
    }
}

func init() {
    cobra.OnInitialize(initConfig)

    // Here you will define your flags and configuration settings.
    // Cobra supports Persistent Flags, which, if defined here,
    // will be global for your application.

    RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra_exp1.yaml)")
    // Cobra also supports local flags, which will only run
    // when this action is called directly.
    RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
    if cfgFile != "" { // enable ability to specify config file via flag
        viper.SetConfigFile(cfgFile)
    }

    viper.SetConfigName(".cobra_exp1") // name of config file (without extension)
    viper.AddConfigPath("$HOME")  // adding home directory as first search path
    viper.AutomaticEnv()          // read in environment variables that match

    // If a config file is found, read it in.
    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
    }
}

文件内容比较多,这里我们先只关注一下定义RootCmd这个变量的结构体内容以及Execute函数。再看main.go内容:

package main

import "cobra_exp1/cmd"

func main() {
    cmd.Execute()
}

和前面介绍的一样,main中就只是执行了初始化的语句。我们运行一下程序:

➜ cobra_exp1 go run main.go
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.

现在我们只有一个空框架,什么子命令和标志也没有。下面我们来添加命令。

cobra add

使用cobra add可以增加我们自己的命令,比如我们需要添加如下三条命令:

  • cobra_exp1 serve
  • cobra_exp1 config
  • cobra_exp1 config create

只需要在工程目录下执行如下三条命令:

➜  cobra_exp1 cobra add serve
Using config file: /Users/Allan/.cobra.yaml
serve created at /Users/Allan/workspace/gopath/src/cobra_exp1/cmd/serve.go
➜  cobra_exp1 cobra add config
Using config file: /Users/Allan/.cobra.yaml
config created at /Users/Allan/workspace/gopath/src/cobra_exp1/cmd/config.go
➜  cobra_exp1 cobra add create -p 'configCmd'
Using config file: /Users/Allan/.cobra.yaml
create created at /Users/Allan/workspace/gopath/src/cobra_exp1/cmd/create.go

# 增加完后的文件结构
➜  cobra_exp1 ll -R
.:
total 9.2M
-rw-r--r-- 1 Allan 1.1K  1 15 13:04 LICENSE
drwxr-xr-x 6 Allan  204  1 15 13:32 cmd
-rwxr-xr-x 1 Allan 9.2M  1 15 13:17 cobra_exp1
-rw-r--r-- 1 Allan 1.2K  1 15 13:04 main.go

./cmd:
total 16K
-rw-r--r-- 1 Allan 2.2K  1 15 13:32 config.go
-rw-r--r-- 1 Allan 2.2K  1 15 13:32 create.go
-rw-r--r-- 1 Allan 3.2K  1 15 13:31 root.go
-rw-r--r-- 1 Allan 2.2K  1 15 13:32 serve.go

然后再运行程序:

➜  cobra_exp1 go build
➜  cobra_exp1 ./cobra_exp1
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:
  cobra_exp1 [command]

Available Commands:
  config      A brief description of your command
  serve       A brief description of your command

Flags:
      --config string   config file (default is $HOME/.cobra_exp1.yaml)

Use "cobra_exp1 [command] --help" for more information about a command.
➜  cobra_exp1 ./cobra_exp1 serve
serve called
➜  cobra_exp1 ./cobra_exp1 config
config called
➜  cobra_exp1 ./cobra_exp1 config create
create called

我们添加的命令已经可以正常使用了,后续我们需要做的就是添加命令的动作了。是不是很强大!OK,我们接着看如何使用标志(Flag)。

cobra提供了两种flag,一种是全局的,一种是局部的。所谓全局的就是如果A命令定义了一个flag,那A命令下的所有命令都可以使用这个flag。局部的flag当然就是只能被某个特定的命令使用了。想一下我们平时使用的那些命令,是不是很多命令都支持-v这个标志来打印详情?其实就是这里的全局flag的意思。比如对于全局的命令,我们可以直接加到root下面:

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

下面我们看个例子:

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.
            `,
        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.
            `,
        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.`,
        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()
}

这个例子中,我们定义了两个顶级命令echoprint和一个echo的子命令timesechoprint功能相同,但是多了一个子命令times可以控制回显的次数,而且这个times命令不是全局的,而只是注册到echo这个命令下面了。下面我们看一下如何使用:

# 编译
➜  cobra_exp1 go build -o app main.go

# 打印应用帮助信息
➜  cobra_exp1 ./app
Usage:
  app [command]

Available Commands:
  echo        Echo anything to the screen
  print       Print anything to the screen

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

# 打印print命令帮助信息
➜  cobra_exp1 ./app print -h
print is for printing anything back to the screen.
            For many years people have printed back to the screen.

Usage:
  app print [string to print] [flags]

# 打印echo命令帮助信息
➜  cobra_exp1 ./app echo --help
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

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

# 打印echo的子命令times的帮助信息
➜  cobra_exp1 ./app echo times -h
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:
  -t, --times int   times to echo the input (default 1)

# 使用
➜  cobra_exp1 ./app print just a test
Print: just a test
➜  cobra_exp1 ./app echo just a test
Print: just a test
➜  cobra_exp1 ./app echo times just a test -t 5
Echo: just a test
Echo: just a test
Echo: just a test
Echo: just a test
Echo: just a test
➜  cobra_exp1 ./app echo times just a test --times=5
Echo: just a test
Echo: just a test
Echo: just a test
Echo: just a test
Echo: just a test
➜  cobra_exp1 ./app echo times just a test --times 5
Echo: just a test
Echo: just a test
Echo: just a test
Echo: just a test
Echo: just a test

可以看到命令使用起来语法也非常的灵活。

其他特性

当然cobra还有非常多的其他特性,这里我们只简单说明一下,就不详细介绍了。

  • 自定义帮助。之前我们已经看到了,我们新增的命令都有help功能,这个是cobra内置的,当然我们可以自定义成我们自己想要的样子。
  • 钩子函数。前面我们介绍了命令执行时会去执行命令Run字段定义的回调函数,cobra还提供了四个函数:PersistentPreRunPreRunPostRunPersistentPostRun,可以在执行这个回调函数之前和之后执行。它们的执行顺序依次是:PersistentPreRunPreRunRunPostRunPersistentPostRun。而且对于PersistentPreRunPersistentPostRun,子命令是继承的,也就是说子命令如果没有自定义自己的PersistentPreRunPersistentPostRun,那它就会执行父命令的这两个函数。
  • 可选的错误处理函数。
  • 智能提示。比如下面的:

    ➜  cobra_exp1 ./app eoch
    Error: unknown command "eoch" for "app"
    
    Did you mean this?
     echo
    
    Run 'app --help' for usage.    
  • ...

本文非常简略的介绍了cobra,主要目的是为了后续阅读Registry等使用cobra框架的开源代码,而非使用cobra去开发。如果你需要使用cobra去开发或者想更深入的了解,可以去仔细阅读一下它的README.md和源代码。