cobra包的使用

hubble的命令行利用cobra包编写。

cobra作为go语言开发命令行程序最流行的包,应用于多个知名项目,包括:docker,Kubernetes,Hugo,rkt,etcd,Moby,OpenShift等。

Cobra是一个库,提供了一个简单的接口来创建类似于git&go工具的强大的现代CLI接口。Cobra是一个应用程序,它将生成应用程序脚手架以快速开发基于Cobra的应用程序。

Cobra提供:

  • 基于子命令的简单CLIs:app server、app fetch等。
  • 完全符合POSIX的标志(包括短版本和长版本)
  • 嵌套子命令
  • 全局、本地和级联标志
  • 使用cobra init appnamecobra add cmdname轻松生成应用程序和命令
  • 智能建议(app srver...拼写错误时会提示为app server
  • 命令和标志的自动帮助生成
  • 自动识别-h-help等帮助标志。
  • 为应用程序生成bash自动完成
  • 为应用程序自动生成的手册页
  • 命令别名,以便您可以更改而不破坏它们
  • 定义自己的帮助、用法等的灵活性。
  • 可选的与viper结合,来开发12-factor程序。

如果参考代码学习cobra可以参考码云上的简单例子cobra-example

概念

Cobra是建立在命令(Commands)、参数(Args)和标志(Flags)之上的。命令表示动作,参数是事物,标志是这些动作的修饰符。最好的命令行应用程序在使用时读起来像句子。用户将知道如何使用该应用程序,因为他们将了解如何使用它。

模式遵从APPNAME-动词-名词-形容词,或者APPNAME-命令-ARG-标志。

例如,hugo中这样使用,server是命令,port是标志:

hugo server --port=1313

git中bare的方式进行克隆,形如“APPNAME-动词-名词-形容词”:

git clone URL --bare

Commands

命令是应用程序入口。应用程序支持的每个交互都将包含在命令中。命令可以有子命令,也可以运行操作。

例如,cobra-example下的root.go代码下的RootCmd

Flags

标志是修改命令行为的一种方式。Cobra支持完全符合POSIX的标志以及Go标志包。Cobra命令可以定义持续到子命令的标志,以及仅对该命令可用的标志。

在上面的示例中,port就是标志。

标志功能是由pflag库提供的,pflag库是标志标准库的一个分支,在添加POSIX兼容性的同时维护相同的接口。

安装

如果需要支持cobra命令自动生成代码,可以使用

go get github.com/spf13/cobra/cobra

cobra添加到$GOROOT/bin下作为命令使用,这时也可以在程序中引入使用

import "github.com/spf13/cobra"

如果不需要代码生成只需要在项目中使用,比如hubble使用的cobra库版本是v0.0.5,可以在go.mod下增加

github.com/spf13/cobra v0.0.5

引入同上边描述。

命令的使用方式请参考官方文档,可以试着按官方文档使用命令生成脚手架程序。

使用

参考cobra-example程序,入口在main.go中。

func main() {
    Execute()
}

Execute()在root.go中定义,通过这个方法调用主命令RootCmdExecute()来启动命令行。

RootCmd的定义:

var RootCmd = &cobra.Command{
    Use:   "cobra-example",
    Short: "cobra例子",
    Long: `cobra-example是一个使用cobra工具包的例子程序。

Cobra是一个开发Go语言CLI端程序的库,现已应用于docker,Kubernetes,Hugo,etcd等多个知名的大型Go语言项目中。`,
    // Uncomment the following line if your bare application
    // has an action associated with it:
    // Run: func(cmd *cobra.Command, args []string) { },
}

一般在init()中绑定flags,flags分为两类,一种LocalFlag,一种PersistentFlag。

PersistentFlag相当于全局flag,LocalFlag一般只应用于本命令。

例如:

RootCmd.PersistentFlags().StringVar(&cfgFile, "config",
    "", "config file (default is $HOME/.cobra-example.yaml)")
RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

这样定义的flag在程序的所有命令中都可用。

在运行其他命令时会输出:

Global Flags:
      --config string   config file (default is $HOME/.cobra-example.yaml)

toogle这时看不到。

标志的使用可以参考start.go,

startCmd.Flags().BoolVarP(&backend, "backend", "b", false, "后端启动")

这样在命令行输入--backend=true时,定义的backend就能获取值了。

命令嵌套使用AddCommand,例如hubble中:

hubbleCmd.AddCommand(
        startCmd,
        startSingleNodeCmd,
        initCmd,
        certCmd,
        quitCmd,

        sqlShellCmd,
        userCmd,
        nodeCmd,
        dumpCmd,

        demoCmd,
        genCmd,
        versionCmd,
        DebugCmd,
        sqlfmtCmd,
        workloadcli.WorkloadCmd(true /* userFacing */),
        systemBenchCmd,
    )

这段代码将start等子命令添加到hubble命令下。

每个命令都是先执行init进行flag绑定,run进行实际操作的。其他详细使用方式请参考例子。

后续

例子中包含了一个标准的使用配置文件的方法。更详细的viper包使用请参考官方文档

由init中定义cobra.OnInitialize来加载配置文件,在使用时将其与flags进行绑定。

但实际开发中遇到了其中存在的问题。cobra.OnInitialize虽然在代码之初定义,但实际运行是在所有init之后。看代码的话发现AddCommand和OnInitialize虽然都是调用append,但实际上

OnInitialize源码定义

initializers = append(initializers, y...)

AddCommand中定义

c.commands = append(c.commands, x)

可能规则是按字母顺序进行加载(存疑)。

为什么OnInitialize后加载呢,OnInitialize相当于添加额外的初始化,而这部分应该在绑定flag之后,否则也无法在OnInitialize中读取到flag的参数。

./cobra-example start --config .cobra-example.yaml

先获取config的参数文件名,然后在读取文件中的配置。

逻辑上是正确的,但如果想在init中使用文件中的配置这种方式就会失败。而hubble中这种方式就存在问题,原因是所有参数都是要在init时获取的。

所以,目前hubble中的读取实现是采用的直接定义配置文件的方式,但这样做不够自定义化。在后续重构代码时可以考虑将此处作为一个待修改点。