Golang 使用 Cobra 创建 CLI 应用

Stella981
• 阅读 714

虽然现在我们使用的大多数软件都是可视化的,很容易上手,但是这并不代表 CLI(命令行)应用就没有用武之地了,特别是对于开发人员来说,还是会经常和 CLI 应用打交道。而 Golang 就非常适合用来构建 CLI 应用,下面我们就将来介绍如何在 Golang 中构建一个 CLI 应用。Golang 使用 Cobra 创建 CLI 应用

对于开发人员来说平时可能就需要使用到很多 CLI 工具,比如 npm、node、go、python、docker、kubectl 等等,因为这些工具非常小巧、没有依赖性、非常适合系统管理或者一些自动化任务等等。

我们这里选择使用 Golang 里面非常有名的 Cobra 库来进行 CLI 工具的开发。Cobra 是一个功能强大的现代化 CLI 应用程序库,有很多知名的 Go 项目使用 Cobra 进行构建,比如:Kubernetes、Docker、Hugo 等等

概念

Cobra 是构建在命令、参数和标识符之上的:

  • Commands 表示执行动作

  • Args 就是执行参数

  • Flags 是这些动作的标识符

基本的执行命令如下所示:

  1. $ APPNAME Command Args --Flags

  2. # 或者

  3. $ APPNAME Command --Flags Args

比如我们平时使用的一些命令行工具:

  • git clone URL -bare

  • go get -u URL

  • npm install package --save

  • kubectl get pods -n kube-system -l app=cobra

示例

下面我们来看下 Cobra 的使用,这里我们使用的 go1.13.3 版本,使用 Go Modules 来进行包管理,如果对这部分知识点不熟悉的,可以查看前面我们的文章 Go Modules 基本使用(视频) 了解。

新建一个名为 my-calc 的目录作为项目目录,然后初始化 modules:

  1. $ mkdir my-calc && cd my-calc

  2. # 如果 go modules 默认没有开启,需要执行 export GO111MODULE=on 开启

  3. $ go mod init my-calc

  4. go: creating new go.mod: module my-calc

初始化完成后可以看到项目根目录下面多了一个 go.mod 的文件,现在我们还没有安装 cobra 库,执行下面的命令进行安装:

  1. # 强烈推荐配置该环境变量

  2. $ export GOPROXY=https://goproxy.cn

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

安装成功后,现在我们可以使用 cobra init 命令来初始化 CLI 应用的脚手架:

  1. $ cobra init --pkg-name my-calc

  2. Your Cobra applicaton is ready at

  3. /Users/ych/devs/workspace/youdianzhishi/course/my-calc

需要注意的是新版本的 cobra 库需要提供一个 --pkg-name 参数来进行初始化,也就是指定上面我们初始化的模块名称即可。上面的 init 命令就会创建出一个最基本的 CLI 应用项目:

  1. $ tree .

  2. .

  3. ├── LICENSE

  4. ├── cmd

  5. │ └── root.go

  6. ├── go.mod

  7. ├── go.sum

  8. └── main.go

  9. 1 directory, 5 files

其中 main.go 是 CLI 应用的入口,在 main.go 里面调用好了 cmd/root.go 下面的 Execute 函数:

  1. // main.go

  2. package main

  3. import "my-calc/cmd"

  4. func main() {

  5. cmd.Execute()

  6. }

然后我们再来看下 cmd/root.go 文件。

rootCmd

root(根)命令是 CLI 工具的最基本的命令,比如对于我们前面使用的 gogetURL,其中 go 就是 root 命令,而 get 就是 go 这个根命令的子命令,而在 root.go 中就直接使用了 cobra 命令来初始化 rootCmd 结构,CLI 中的其他所有命令都将是 rootCmd 这个根命令的子命令了。

这里我们将 cmd/root.go 里面的 rootCmd 变量内部的注释去掉,并在 Run 函数里面加上一句 fmt.Println("Hello Cobra CLI")

  1. var rootCmd = &cobra.Command{

  2. Use: "my-calc",

  3. Short: "A brief description of your application",

  4. Long: `A longer description that spans multiple lines and likely contains

  5. examples and usage of using your application. For example:

  6. Cobra is a CLI library for Go that empowers applications.

  7. This application is a tool to generate the needed files

  8. to quickly create a Cobra application.`,

  9. Run: func(cmd *cobra.Command, args []string) {

  10. fmt.Println("Hello Cobra CLI")

  11. },

  12. }

这个时候我们在项目根目录下面执行如下命令进行构建:

  1. $ go build -o my-calc

该命令会在项目根目录下生成一个名为 my-calc 的二进制文件,直接执行这个二进制文件可以看到如下所示的输出信息:

  1. $ ./my-calc

  2. Hello Cobra CLI

init

我们知道 init 函数是 Golang 中初始化包的时候第一个调用的函数。在 cmd/root.go 中我们可以看到 init 函数中调用了 cobra.OnInitialize(initConfig),也就是每当执行或者调用命令的时候,它都会先执行 init 函数中的所有函数,然后再执行 execute 方法。该初始化可用于加载配置文件或用于构造函数等等,这完全依赖于我们应用的实际情况。

在初始化函数里面 cobra.OnInitialize(initConfig) 调用了 initConfig 这个函数,所有,当 rootCmd 的执行方法 RUN:func 运行的时候, rootCmd 根命令就会首先运行 initConfig 函数,当所有的初始化函数执行完成后,才会执行 rootCmdRUN:func 执行函数。

我们可以在 initConfig 函数里面添加一些 Debug 信息:

  1. func initConfig() {

  2. fmt.Println("I'm inside initConfig function in cmd/root.go")

  3. ...

  4. }

然后同样重新构建一次再执行:

  1. $ go build -o my-calc

  2. $ ./my-calc

  3. I'm inside initConfig function in cmd/root.go

  4. Hello Cobra CLI

可以看到是首先运行的是 initConfig 函数里面的信息,然后才是真正的执行函数里面的内容。

为了搞清楚整个 CLI 执行的流程,我们在 main.go 里面也添加一些 Debug 信息:

  1. // cmd/root.go

  2. func init() {

  3. fmt.Println("I'm inside init function in cmd/root.go")

  4. cobra.OnInitialize(initConfig)

  5. ...

  6. }

  7. func initConfig() {

  8. fmt.Println("I'm inside initConfig function in cmd/root.go")

  9. ...

  10. }

  11. // main.go

  12. func main() {

  13. fmt.Println("I'm inside main function in main.go")

  14. cmd.Execute()

  15. }

然后同样重新构建一次再执行:

  1. $ go build -o my-calc

  2. $ ./my-calc

  3. I'm inside init function in cmd/root.go

  4. I'm inside main function in main.go

  5. I'm inside initConfig function in cmd/root.go

  6. Hello Cobra CLI

根据上面的日志信息我们就可以了解到 CLI 命令的流程了。

init 函数最后处理的就是 flags 了, Flags 就类似于命令的标识符,我们可以把他们看成是某种条件操作,在 Cobra 中提供了两种类型的标识符:PersistentFlagsLocalFlags

  • PersistentFlags: 该标志可用于为其分配的命令以及该命令的所有子命令。

  • LocalFlags: 该标志只能用于分配给它的命令。

initConfig

该函数主要用于在 home 目录下面设置一个名为 .my-calc 的配置文件,如果该文件存在则会使用这个配置文件。

  1. // cmd/root.go

  2. // initConfig 读取配置文件和环境变量

  3. func initConfig() {

  4. if cfgFile != "" {

  5. // 使用 flag 标志中传递的配置文件

  6. viper.SetConfigFile(cfgFile)

  7. } else {

  8. // 获取 Home 目录

  9. home, err := homedir.Dir()

  10. if err != nil {

  11. fmt.Println(err)

  12. os.Exit(1)

  13. }

  14. // 在 Home 目录下面查找名为 ".my-calc" 的配置文件

  15. viper.AddConfigPath(home)

  16. viper.SetConfigName(".my-calc")

  17. }

  18. // 读取匹配的环境变量

  19. viper.AutomaticEnv()

  20. // 如果有配置文件,则读取它

  21. if err := viper.ReadInConfig(); err == nil {

  22. fmt.Println("Using config file:", viper.ConfigFileUsed())

  23. }

  24. }

viper 是一个非常优秀的用于解决配置文件的 Golang 库,它可以从 JSON、TOML、YAML、HCL、envfile 以及 Java properties 配置文件中读取信息,功能非常强大,而且不仅仅是读取配置这么简单,了解更多相关信息可以查看 Git 仓库相关介绍:https://github.com/spf13/viper。

现在我们可以去掉前面我们添加的一些打印语句,我们已经创建了一个 my-calc 命令作为 rootCmd 命令,执行该根命令会打印 HelloCobraCLI 信息,接下来为我们的 CLI 应用添加一些其他的命令。

添加数据

在项目根目录下面创建一个名为 add 的命令, Cobra 添加一个新的命令的方式为:cobra add<commandName>,所以我们这里直接这样执行:

  1. $ cobra add add

  2. add created at /Users/ych/devs/workspace/youdianzhishi/course/my-calc

  3. $ tree .

  4. .

  5. ├── LICENSE

  6. ├── cmd

  7. │ ├── add.go

  8. │ └── root.go

  9. ├── go.mod

  10. ├── go.sum

  11. ├── main.go

  12. └── my-calc

  13. 1 directory, 7 files

现在我们可以看到 cmd/root.go 文件中新增了一个 add.go 的文件,我们仔细观察可以发现该文件和 cmd/root.go 比较类似。首先是声明了一个名为 addCmd 的结构体变量,类型为 *cobra.Command 指针类型, *cobra.Command 有一个 RUN 函数,带有 *cobra.Command 指针和一个字符串切片参数。

然后在 init 函数中进行初始化,初始化后,将其添加到 rootCmd 根命令中 rootCmd.AddCommand(addCmd),所以我们可以把 addCmd 看成是 rootCmd 的子命令。

同样现在重新构建应用再执行:

  1. $ go build -o my-calc

  2. $ ./my-calc

  3. Hello Cobra CLI

  4. $ ./my-calc add

  5. add called

可以看到 add 命令可以正常运行了,接下来我们来让改命令支持添加一些数字,我们知道在 RUN 函数中是用户字符串 slice 来作为参数的,所以要支持添加数字,我们首先需要将字符串转换为 int 类型,返回返回计算结果。

cmd/add.go 文件中添加一个名为 intAdd 的函数,定义如下所示:

  1. // cmd/add.go

  2. func intAdd(args []string) {

  3. var sum int

  4. // 循环 args 参数,循环的第一个值为 args 的索引,这里我们不需要,所以用 _ 忽略掉

  5. for _, ival := range args {

  6. // 将 string 转换成 int 类型

  7. temp, err := strconv.Atoi(ival)

  8. if err != nil {

  9. panic(err)

  10. }

  11. sum = sum + temp

  12. }

  13. fmt.Printf("Addition of numbers %s is %d\n", args, sum)

  14. }

然后在 addCmd 变量中,更新 RUN 函数,移除默认的打印信息,调用上面声明的 addInt 函数:

  1. // addCmd

  2. Run: func(cmd *cobra.Command, args []string) {

  3. intAdd(args)

  4. },

然后重新构建应用执行如下所示的命令:

  1. $ go build -o my-calc

  2. $ ./my-calc

  3. Hello Cobra CLI

  4. # 注意参数之间的空格

  5. $ ./my-calc add 1 2 3

  6. Addition of numbers [1 2 3] is 6

由于 RUN 函数中的 args 参数是一个字符串切片,所以我们可以传递任意数量的参数,但是却有一个缺陷,就是只能进行整数计算,不能计算小数,比如我们执行如下的计算就会直接 panic 了:

  1. $ ./my-calc add 1 2 3.5

  2. panic: strconv.Atoi: parsing "3.5": invalid syntax

  3. goroutine 1 [running]:

  4. my-calc/cmd.intAdd(0xc0000a5890, 0x3, 0x3)

  5. ......

因为在 intAdd 函数里面,我们只是将字符串转换成了 int,而不是 float32/64 类型,所以我们可以为 addCmd 命令添加一个 flag 标识符,通过该标识符来帮助 CLI 确定它是 int 计算还是 float 计算。

cmd/add.go 文件的 init 函数内部,我们创建一个 Bool 类型的本地标识符,命名成 float,简写成 f,默认值为 false。这个默认值是非常重要的,意思就是即使没有在命令行中调用 flag 标识符,该标识符的值就将为 false。

  1. // cmd/add.go

  2. func init() {

  3. rootCmd.AddCommand(addCmd)

  4. addCmd.Flags().BoolP("float", "f", false, "Add Floating Numbers")

  5. }

然后创建一个 floatAdd 的函数:

  1. func floatAdd(args []string) {

  2. var sum float64

  3. for _, fval := range args {

  4. // 将字符串转换成 float64 类型

  5. temp, err := strconv.ParseFloat(fval, 64)

  6. if err != nil {

  7. panic(err)

  8. }

  9. sum = sum + temp

  10. }

  11. fmt.Printf("Sum of floating numbers %s is %f\n", args, sum)

  12. }

该函数和上面的 intAdd 函数几乎是相同的,除了是将字符串转换成 float64 类型。然后在 addCmdRUN 函数中,我们根据传入的标识符来判断到底应该是调用 intAdd 还是 floatAdd,如果传递了 --float 或者 -f 标志,就将会调用 floatAdd 函数。

  1. // cmd/add.go

  2. // addCmd

  3. Run: func(cmd *cobra.Command, args []string) {

  4. // 获取 float 标识符的值,默认为 false

  5. fstatus, _ := cmd.Flags().GetBool("float")

  6. if fstatus { // 如果为 true,则调用 floatAdd 函数

  7. floatAdd(args)

  8. } else {

  9. intAdd(args)

  10. }

  11. },

现在重新编译构建 CLI 应用,按照如下方式执行:

  1. $ go build -o my-calc

  2. $ ./my-calc add 1 2 3

  3. Addition of numbers [1 2 3] is 6

  4. $ ./my-calc add 1 2 3.5 -f

  5. Sum of floating numbers [1 2 3.5] is 6.500000

  6. $./my-calc add 1 2 3.5 --float

  7. Sum of floating numbers [1 2 3.5] is 6.500000

然后接下来我们在给 addCmd 添加一些子命令来扩展它。

添加偶数

同样在项目根目录下执行如下命令添加一个名为 even 的命令:

  1. $ cobra add even

  2. even created at /Users/ych/devs/workspace/youdianzhishi/course/my-calc

和上面一样会在 root 目录下面新增一个名为 even.go 的文件,修改该文件中的 init 函数,将 rootCmd 修改为 addCmd,因为我们是为 addCmd 添加子命令:

  1. // cmd/even.go

  2. func init() {

  3. addCmd.AddCommand(evenCmd)

  4. }

然后更新 evenCmd 结构体参数的 RUN 函数:

  1. // cmd/even.go

  2. Run: func(cmd *cobra.Command, args []string) {

  3. var evenSum int

  4. for _, ival := range args {

  5. temp, _ := strconv.Atoi(ival)

  6. if temp%2 == 0 {

  7. evenSum = evenSum + temp

  8. }

  9. }

  10. fmt.Printf("The even addition of %s is %d\n", args, evenSum)

  11. },

首先将字符串转换成整数,然后判断如果是偶数才进行累加。然后重新编译构建应用:

  1. $ go build -o my-calc

  2. $ ./my-calc add even 1 2 3 4 5 6

  3. The even addition of [1 2 3 4 5 6] is 12

my-calc 是我们的根命令, addrootCmd 的子命令, even 又是 addCmd 的子命令,所以按照上面的方式调用。可以用同样的方式再去添加一个奇数相加的子命令。

到这里我们就在 Golang 里面使用 Cobra 创建了一个简单的 CLI 应用。本文的内容虽然比较简单,但是是我们了解学习 Cobra 基础的一个很好的入门方式,后续我们也可以尝试添加一些更加复杂的使用案例。

参考资料


K8S 进阶训练营,点击下方图片了解详情

Golang 使用 Cobra 创建 CLI 应用

Golang 使用 Cobra 创建 CLI 应用

本文分享自微信公众号 - DevOps云学堂(idevopsvip)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这