Golang | 使用 Cobra 构建命令行工具

文章首发于个人公众号:「阿拉平平」

最近折腾了下命令行库 Cobra,和大家分享下。本文演示环境为 CentOS 7.5,Golang 1.11。

文章目录:

  1. Cobra 介绍
    1.1 概念
    1.2 安装
    1.3 初始化
    1.4 代码分析

  2. Cobra 实践
    2.1 子命令
    2.2 子命令嵌套
    2.3 参数
    2.4 标志
    2.5 读取配置
    2.6 编译运行

1. Cobra 介绍

Cobra 是一个用来创建命令行的 golang 库,同时也是一个用于生成应用和命令行文件的程序。

1.1 概念

Cobra 结构由三部分组成:命令 (commands)、参数 (arguments)、标志 (flags)。基本模型如下:
APPNAME VERB NOUN --ADJECTIVE 或者 APPNAME COMMAND ARG --FLAG

如果不是太理解的话,没关系,我们先看个例子:

hugo server --port=1313
  • hugo:根命令
  • server:子命令
  • --port:标志

再看个带有参数的例子:

git clone URL --bare
  • git:根命令
  • clone:子命令
  • URL:参数,即 clone 作用的对象
  • --bare:标志

总结下:

  • commands 代表行为,是应用的中心点
  • arguments 代表行为作用的对象
  • flags 是行为的修饰符

相信看了例子后,应该有个直观的认识了。接下来我们安装 Cobra。

1.2 安装

安装很简单:

go get -u github.com/spf13/cobra/cobra

但是由于网络原因,有些包会下载失败,提示 i/o timeout

package golang.org/x/sys/unix: unrecognized import path "golang.org/x/sys/unix" (https fetch: Get https://golang.org/x/sys/unix?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
package golang.org/x/text/transform: unrecognized import path "golang.org/x/text/transform" (https fetch: Get https://golang.org/x/text/transform?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
package golang.org/x/text/unicode/norm: unrecognized import path "golang.org/x/text/unicode/norm" (https fetch: Get https://golang.org/x/text/unicode/norm?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)

网上解决方法很多,这里我推荐使用 gopm 来下载:

# 下载 gopm,之后会在 $GOPATH/bin 目录下生成 gopm
go get -u github.com/gpmgo/gopm

# 使用 gopm 来下载 cobra
gopm get -u -g github.com/spf13/cobra/cobra

下载完成后安装 cobra 工具,在 $GOPATH/bin 会生成可执行文件:

go install github.com/spf13/cobra/cobra

将生成的 cobra 工具放到 $PATH 目录下,可以看到:

[root@localhost ~]# cp -a $GOPATH/bin/cobra /usr/local/bin
[root@localhost ~]# cobra
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.

接下来我们初始化一个项目。

1.3 初始化

通过 cobra init 初始化 demo 项目:

[root@localhost ~]# cd $GOPATH/src 
[root@localhost src]# cobra init demo --pkg-name=demo
Your Cobra applicaton is ready at
/root/go/src/demo

当前项目结构为:

demo
├── cmd
│   └── root.go
├── LICENSE
└── main.go

可以看到初始化后的项目非常简单,主要是 main.goroot.go 文件。在编写代码之前,我们先分析下目前代码的逻辑。

1.4 代码分析

先查看下入口文件 main.go。代码逻辑很简单,就是调用 cmd 包里 Execute()函数:

package main

import "demo/cmd"

func main() {
  cmd.Execute()
}

再看下 root.go 中 rootCmd 的字段:

...

var rootCmd = &cobra.Command{
  Use:   "demo",
  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 and 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)
  }
}

...

简单说明下:

  • Use:命令名
  • Short & Long:帮助信息的文字内容
  • Run:运行命令的逻辑

Command 结构体中的字段当然远不止这些,受限于篇幅,这里无法全部介绍。有兴趣的童鞋可以查阅下官方文档。

运行测试:

[root@localhost demo]# 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.

subcommand is required
exit status 1

如果运行的结果和我的一致,那我们就可以进入到实践环节了。

2. Cobra 实践

铺垫了这么久,终于可以开始实践了。实践环节中,我会 提一些需求,然后我们一起实现一个简单的命令行工具。

2.1 子命令

之前运行会提示 subcommand is required,是因为根命令无法直接运行。那我们就添加个子命令试试。

通过 cobra add 添加子命令 create:

[root@localhost demo]# cobra add create
create created at /root/go/src/demo

当前项目结构为:

demo
├── cmd
│   ├── create.go
│   └── root.go
├── LICENSE
└── main.go

查看下 create.goinit() 说明了命令的层级关系:

...

func init() {
       rootCmd.AddCommand(createCmd)        
}

运行测试:

# 输入正确
[root@localhost demo]# go run main.go create
create called

# 未知命令
[root@localhost demo]# go run main.go crea
Error: unknown command "crea" for "demo"

Did you mean this?
    create

Run 'demo --help' for usage.
unknown command "crea" for "demo"

Did you mean this?
    create

2.2 子命令嵌套

对于功能相对复杂的 CLI,通常会通过多级子命令,即:子命令嵌套的方式进行描述,那么该如何实现呢?

demo create rule

首先添加子命令 rule :

[root@localhost demo]# cobra add rule
rule created at /root/go/src/demo

当前目录结构如下:

demo
├── cmd
│   ├── create.go
│   ├── root.go
│   └── rule.go
├── LICENSE
└── main.go

目前createrule 是同级的,所以需要修改 rule.goinit() 来改变子命令间的层级关系:

...

func init() {
        // 修改子命令的层级关系
        //rootCmd.AddCommand(ruleCmd)
        createCmd.AddCommand(ruleCmd)
}

虽然调整了命令的层级关系,但是目前运行 demo create 会打印 create called,我希望运行时可以打印帮助提示。所以我们继续完善下代码,修改 create.go

...

var createCmd = &cobra.Command{
        Use:   "create",
        Short: "create",
        Long: "Create Command.",
        Run: func(cmd *cobra.Command, args []string) {
                // 如果 create 命令后没有参数,则提示帮助信息
                if len(args) == 0 {
                  cmd.Help()
                  return
                }
        },
}

...

运行测试:

  • 直接运行 create,打印帮助提示:
[root@localhost demo]# go run main.go create
Create Command.

Usage:
  demo create [flags]
  demo create [command]

Available Commands:
  rule        A brief description of your command

Flags:
  -h, --help   help for create

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

Use "demo create [command] --help" for more information about a command.
  • 运行 create rule,输出 rule called
[root@localhost demo]# go run main.go create rule
rule called

2.3 参数

先说说参数。现在有个需求:给 CLI 加个位置参数,要求参数有且仅有一个。这个需求我们要如何实现呢?

demo create rule foo 

实现前先说下,Command 结构体中有个 Args 的字段,接受类型为 type PositionalArgs func(cmd *Command, args []string) error

内置的验证方法如下:

  • NoArgs:如果有任何参数,命令行将会报错
  • ArbitraryArgs: 命令行将会接收任何参数
  • OnlyValidArgs: 如果有如何参数不属于 Command 的 ValidArgs 字段,命令行将会报错
  • MinimumNArgs(int): 如果参数个数少于 N 个,命令行将会报错
  • MaximumNArgs(int): 如果参数个数多于 N 个,命令行将会报错
  • ExactArgs(int): 如果参数个数不等于 N 个,命令行将会报错
  • RangeArgs(min, max): 如果参数个数不在 min 和 max 之间, 命令行将会报错

由于需求里要求参数有且仅有一个,想想应该用哪个内置验证方法呢?相信你已经找到了 ExactArgs(int)。

改写下 rule.go

...

var ruleCmd = &cobra.Command{
        Use:   "rule",
        Short: "rule",
        Long: "Rule Command.",
        
        Args: cobra.ExactArgs(1),
        Run: func(cmd *cobra.Command, args []string) {           
          fmt.Printf("Create rule %s success.\n", args[0])
        },
}

...

运行测试:

  • 不输入参数:
[root@localhost demo]# go run main.go create rule
Error: accepts 1 arg(s), received 0
  • 输入 1 个参数:
[root@localhost demo]# go run main.go create rule foo
Create rule foo success.
  • 输入 2 个参数:
[root@localhost demo]# go run main.go create rule
Error: accepts 1 arg(s), received 2

从测试的情况看,运行的结果符合我们的预期。如果需要对参数进行复杂的验证,还可以自定义 Args,这里就不多做赘述了。

2.4 标志

再说说标志。现在要求 CLI 不接受参数,而是通过标志 --namerule 进行描述。这个又该如何实现?

demo create rule --name foo

Cobra 中有两种标志:持久标志 ( Persistent Flags ) 和 本地标志 ( Local Flags ) 。

持久标志:指所有的 commands 都可以使用该标志。比如:--verbose ,--namespace
本地标志:指特定的 commands 才可以使用该标志。

这个标志的作用是修饰和描述 rule的名字,所以选用本地标志。修改 rule.go

package cmd

import (
        "fmt"        
        "github.com/spf13/cobra"
)       

// 添加变量 name
var name string

var ruleCmd = &cobra.Command{
        Use:   "rule",
        Short: "rule",
        Long: "Rule Command.",
        Run: func(cmd *cobra.Command, args []string) {
          // 如果没有输入 name
          if len(name) == 0 {
            cmd.Help()
            return
          }     
          fmt.Printf("Create rule %s success.\n", name)
        },
}

func init() {
        createCmd.AddCommand(ruleCmd)
        // 添加本地标志
        ruleCmd.Flags().StringVarP(&name, "name", "n", "", "rule name")      
}

说明:StringVarP 用来接收类型为字符串变量的标志。相较StringVarStringVarP 支持标志短写。以我们的 CLI 为例:在指定标志时可以用 --name,也可以使用短写 -n

运行测试:

# 这几种写法都可以执行
[root@localhost demo]# go run main.go create rule -n foo
Create rule foo success.
[root@localhost demo]# go run main.go create rule --name foo
Create rule foo success.
[root@localhost demo]# go run main.go create -n foo rule
Create rule foo success.

2.5 读取配置

最后说说配置。需求:要求 --name 标志存在默认值,且该值是可配置的。

如果只需要标志提供默认值,我们只需要修改 StringVarPvalue 参数就可以实现。但是这个需求关键在于标志是可配置的,所以需要借助配置文件。

很多情况下,CLI 是需要读取配置信息的,比如 kubectl 的~/.kube/config。在帮助提示里可以看到默认的配置文件为 $HOME/.demo.yaml

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

​配置库我们可以使用 Viper。Viper 是 Cobra 集成的配置文件读取库,支持 YAMLJSONTOMLHCL 等格式的配置。

添加配置文件 $HOME/.demo.yaml,增加 name 字段:

[root@localhost ~]# vim $HOME/.demo.yaml 
name: foo

修改 rule.go:

package cmd

import (
        "fmt"
         // 导入 viper 包
        "github.com/spf13/viper"
        "github.com/spf13/cobra"
)

var name string

var ruleCmd = &cobra.Command{
        Use:   "rule",
        Short: "rule",
        Long: "Rule Command.",
        Run: func(cmd *cobra.Command, args []string) {
          // 不输入 --name 从配置文件中读取 name
          if len(name) == 0 {
            name = viper.GetString("name")
            // 配置文件中未读取到 name,打印帮助提示
            if len(name) == 0 {
              cmd.Help()
              return
            }
          }
          fmt.Printf("Create rule %s success.\n", name)
        },
}

func init() {
        createCmd.AddCommand(ruleCmd)
        ruleCmd.Flags().StringVarP(&name, "name", "n", "", "rule name")
}

运行测试:

[root@localhost demo]# go run main.go create rule
Using config file: /root/.demo.yaml
Create rule foo success.

如果 CLI 没有用到配置文件,可以在初始化项目的时候关闭 Viper 的选项以减少编译后文件的体积,如下:

cobra init demo --pkg-name=demo --viper=false

2.6 编译运行

​编译生成命令行工具:

[root@localhost demo]# go build -o demo

运行测试:

[root@localhost demo]# ./demo create rule
Using config file: /root/.demo.yaml
Create rule foo success.
参考文档:
  1. Github - https://github.com/spf13/cobra
  2. Cobra 的一些笔记 - https://zhangguanzhang.github.io/2019/06/02/cobra/
  3. Golang之使用Cobra - https://o-my-chenjian.com/2017/09/20/Using-Cobra-With-Golang/
  4. golang命令行库Cobra的使用 - //www.greatytc.com/p/7abe7cff5384
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容