Golang-cobra-要命!我篡改了系统命令惊现事故,竟要扣我年终奖

小熊 Golang评论2,646字数 6263阅读20分52秒阅读模式

Golang-cobra-要命!我篡改了系统命令惊现事故,竟要扣我年终奖

打工还是要打工的。。我最后也没发出去。

紧急处理以后,现在写复盘,大家随我看看我到底是在学习哪些内容。

(以上内容纯属虚构,如有雷同纯属巧合)

简介

之前我们讲过pflag和os.Args,现在说说cobra这个命令行框架。

KubernetesHugoetcd 这些知名项目都用cobra来做命令行程序。学起来!

关于作者spf13,这里多说两句。spf13 开源不少项目,而且他的开源项目质量都比较高。相信使用过 vim 的都知道spf13-vim,号称 vim 终极配置。可以一键配置,对于我这样的懒人来说绝对是福音。

Golang-cobra-要命!我篡改了系统命令惊现事故,竟要扣我年终奖

还有他的viper是一个完整的配置解决方案。完美支持 JSON/TOML/YAML/HCL/envfile/Java properties 配置文件等格式,还有一些比较实用的特性,如配置热更新、多查找目录、配置保存等。还有非常火的静态网站生成器hugo也是他的作品牛人就是牛人。

这个牛人 https://github.com/spf13

快速使用

第三方库都需要先安装,后使用。下面命令安装了cobra生成器程序和 cobra 库:

$ go get github.com/spf13/cobra/cobra

PS: 如果出现了golang.org/x/text库找不到之类的错误,需要手动从 GitHub 上下载该库,再执行上面的安装命令。

现在要举的例子是让我们的程序调子命令时会透传到git上,用git version举例。目录结构如下(手动建的):

get-started/
    cmd/       
      root.go        
      version.go    
    utils/
      helper.go
    main.go
  • cmd目标是子命令列表,这里有一个version命令。
  • root.go先卖个关子,大家不要理他。
  • main.go是主程序。
  • helper是这里使用到的工具类。
  • go.mod文件我省略了。

下面的代码文件我就省略import "github.com/spf13/cobra"了,大家知道就行,version.go文件:

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "version subcommand show git version info.",

    Run: func(cmd *cobra.Command, args []string) {
        output, err := utils.ExecuteCommand("git", "version", args...)
        if err != nil {
            utils.Error(cmd, args, err)
        }

        fmt.Fprint(os.Stdout, output)
    },
}

func init() {
    rootCmd.AddCommand(versionCmd)
}
  • 几个参数含义是子命令名称、子命令短提示、子命令调用的方法
  • init()里把子命令加到主命令中去。

你会有疑惑rootCmd是哪来的吗?实际上我们需要一个根节点,把其他命令加进来。如下是root.go文件。

var rootCmd = &cobra.Command {
    Use: "git",
    Short: "Git is a distributed version control system.",
    Long: `Git is a free ...省略`,
    Run: func(cmd *cobra.Command, args []string) {
        utils.Error(cmd, args, errors.New("unrecognized command"))
    },
}

func Execute() {
    rootCmd.Execute()
}

有没有发现这里不是init()而是Execute()?这里此包唯一暴露的公开函数内容,专门供命令初始化使用。如下main.go文件中的调用命令入口:

import "cmd"
func main() {
  cmd.Execute()
}

最后为了编码方便,在helpers.go中封装了调用外部程序和错误处理函数,我就不展开写了,有兴趣去看我的源码。

https://github.com/golang-minibear2333/cmd_utils

cobra 自动生成的帮助信息,very cool

$ go run . -h
Git is a free and open source distributed version control system
designed to handle everything from small to very large projects 
with speed and efficiency.

Usage:
  git [flags]
  git [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  version     version subcommand show git version info.

Flags:
  -h, --help   help for git

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

单个子命令的帮助信息:

$ go run . version -h
version subcommand show git version info.

Usage:
  git version [flags]

Flags:
  -h, --help   help for version

调用子命令:

$ go run . version
git version 2.33.0

未识别的子命令:

$ go run . xxx
Error: unknown command "xxx" for "git"
Run 'git --help' for usage.

使用 cobra 构建命令行时,程序的目录结构一般比较简单,推荐使用下面这种结构:

appName/
    cmd/
        cmd1.go
        cmd2.go
        cmd3.go
        root.go
    main.go

每个命令实现一个文件,所有命令文件存放在cmd目录下。外层的main.go仅初始化 cobra。

特性

cobra 提供非常丰富的功能:

  • 轻松支持子命令,如app serverapp fetch等;
  • 完全兼容 POSIX 选项(包括短、长选项);
  • 嵌套子命令;
  • 全局、本地层级选项。可以在多处设置选项,按照一定的顺序取用;
  • 使用脚手架轻松生成程序框架和命令。

首先需要明确 3 个基本概念:

  • 命令(Command):就是需要执行的操作;
  • 参数(Arg):命令的参数,即要操作的对象;
  • 选项(Flag):命令选项可以调整命令的行为。

比如

git clone URL --bare

clone 是一个(子)命令,URL 是参数,--bare是选项。子命令我们已经讲过了,现在讲讲参数。

参数

比如定义命令的地方。

var cloneCmd = &cobra.Command{
    Use:   "clone url [destination]",
  ...
  Run: func(cmd *cobra.Command, args []string) {
  ...

会改变帮助函数输出的内容。实际上还是传入字符串数组

go run . clone -h
Clone a repository into a new directory

Usage:
  git clone url [destination] [flags]

Flags:
  -h, --help   help for clone

选项

cobra 中选项分为两种.

  • 一种是永久选项( PersistentFlags 翻译不太标准,暂时就说永久选项),定义它的命令和其子命令都可以使用。方法是给根命令添加一个选项定义全局选项。
  • 另一种是本地选项,只能在定义它的命令中使用。

cobra 使用pflag解析命令行选项,上次讲过,实际上用法都是一样的。

设置永久选项,在root.go根命令文件中的init()函数:

var(
  Verbose bool
)
func init() {
  rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
}

设置本地选项,在子命令的init()函数:

var(
  Source bool
)
func init() {
  localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
  rootCmd.AddCommand(divideCmd)
}

两种参数都是相同的,长选项/短选项名、默认值和帮助信息。

脚手架

通过前面的介绍,我们也看到了其实 cobra 命令的框架还是比较固定的。这就有了工具的用武之地了,可极大地提高我们的开发效率。

在你的go root下安装cobra-cli,确保bin目录已经放到系统的path里,之前写的文章-运行那一节有提到过怎么操作,不记得的回去看看哈。

go install github.com/spf13/cobra-cli@latest

下面我们介绍如何使用这个生成器,先看命令帮助:

Usage:
  cobra-cli init [path] [flags]

Aliases:
  init, initialize, initialise, create

Flags:
  -h, --help   help for init

Global Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -l, --license string   name of license for the project
      --viper            use Viper for configuration
  • 根据提示子命令init,可选参数为path
  • 选项为-a指定作者,--config string指定cobra-cli自己的配置文件
  • -l指定license--viper使用viper来读取配置文件。

使用cobra init命令创建一个 cobra 应用程序:

$ mkdir appname
$ cd appname
$ cobra-cli init
Error: Please run `go mod init <MODNAME>` before `cobra-cli init`
$ go mod init
go: creating new go.mod: module github.com/golang-minibear2333/cmd_utils/git/appname
$ cobra-cli init
Your Cobra application is ready at
/Users/xxxx/Documents/code/go/src/github.com/golang-minibear2333/cmd_utils/git/appname
  • 先初始化mod 再初始化项目
  • 其中appname为应用程序名。生成的程序目录结构如下:
.
├── LICENSE
├── cmd
│   └── root.go
├── go.mod
├── go.sum
└── main.go

这个项目结构与之前介绍的完全相同,也是 cobra 推荐使用的结构。同样地,main.go也仅仅是入口。里面的英文注释非常的清晰,我一下子就看懂了用法,你也试试。

配置读取

除了命令行以外,这个库还可以用来配置读取,我们先创建项目和配置文件:

mkdir cfg_load && cd_cg_load
mkdir config && touch config/cfg.yaml
cat >config/cfg.yaml <<-EOF
people:
   name: minibear2333
   age: 18
EOF

PS: linux命令不熟的可以在Go群里问我。

现在我们尝试读取这个配置文件,直接使用命令来创建读取配置文件的代码。

$ cobra-cli init --viper
Your Cobra application is ready at
/Users/xxx/Documents/code/go/src/github.com/golang-minibear2333/cmd_utils/git/cfg_load

现在就创建了一个默认配置文件为$HOME/.cfg_load.yaml的命令行程序,而我们之前放在了另一个位置,所以启动的时候需要指定一下。

$ go run . --config==config/cfg.yaml

配置文件就成功载入了,现在你就可以用viper在需要的地方读取配置了。

为了展示一下配置是否成功读取,继续用cobra-cli来创建一个子命令。

$ cobra-cli add viperall

修改此子命令Run函数的内容为

var viperallCmd = &cobra.Command{
    Use:   "viperall",
    Short: "Show cfg all",
    Long: `Show the contents of the entire configuration file`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println(viper.AllSettings())
    },
}

运行,妥了。

$ go run . viperall --config=config/cfg.yaml 
Using config file: config/cfg.yaml
map[people:map[age:18 name:minibear2333]]

每次都要指定肯定很麻烦,你熟悉viper的话可以自己改一下默认文件,把我的项目下下来给我提交一个pr吧~!

子命令也可以嵌套,只需要在init()的时候,加到父命令里,当然也可以自动生成。

$ cobra-cli add tt -p viperallCmd
tt created at /Users/xxx/Documents/code/go/src/github.com/golang-minibear2333/cmd_utils/git/cfg_load
$ go run . viperall tt --config=config/cfg.yaml 
Using config file: config/cfg.yaml
tt called
  • 注意父命令是viperall,但是-p指定的时候要改为viperallCmd,因为如下(我觉得这个是个很好的贡献pr,你可以建议作者改一下):
var viperallCmd = &cobra.Command{

小结

  • 每个 cobra 程序都有一个根命令,可以给它添加任意多个子命令。比如我们在version.goinit函数中将子命令添加到根命令中。
  • 创建子命令时指定子命令名称、子命令短提示、子命令调用的方法。
  • 三个重要概念,子命令、参数、选项。
  • 全局选项和子命令自己使用的选项。
  • cobra-cli 自动创建项目,自动创建配置文件读取项目,自动增加子命令,自动增加嵌套子命令。

推荐目录结构

.
├── LICENSE
├── cmd
│   ├── root.go
|   ├── cmd1.go
├── go.mod
├── go.sum
└── main.go

还有更多! cobra 提供了非常丰富的特性和定制化接口,例如:

  • 设置钩子函数,在命令执行前、后执行某些操作。
  • 生成 Markdown/ReStructed Text/Man Page 格式的文档。
  • 等。自己下来学咯。

cobra 库的使用非常广泛,很多知名项目都有用到,前面也提到过这些项目。学习这些项目是如何使用 cobra 的,可以从中学习 cobra 的特性和最佳实践。这也是学习开源项目的一个很好的途径。

weinxin
公众号
扫码订阅最新深度技术文,回复【资源】获取技术大礼包
小熊