在 Golang 中使用 Cobra 创建 CLI 应用

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

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

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

概念

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

  • Commands表示执行动作
  • Args就是执行参数
  • Flags是这些动作的标识符

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

$ APPNAME Command Args --Flags
# 或者
$ 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:

$ mkdir my-calc && cd my-calc
# 如果 go modules 默认没有开启,需要执行 export GO111MODULE=on 开启
$ go mod init my-calc
go: creating new go.mod: module my-calc

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

# 强烈推荐配置该环境变量
$ export GOPROXY=https://goproxy.cn
$ go get -u github.com/spf13/cobra/cobra

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

$ cobra init --pkg-name my-calc
Your Cobra applicaton is ready at
/Users/ych/devs/workspace/youdianzhishi/course/my-calc

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

$ tree .
.
├── LICENSE
├── cmd
│   └── root.go
├── go.mod
├── go.sum
└── main.go

1 directory, 5 files

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

// main.go
package main

import "my-calc/cmd"

func main() {
	cmd.Execute()
}

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

rootCmd

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

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

var rootCmd = &cobra.Command{
	Use:   "my-calc",
	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.`,
	Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Hello Cobra CLI")
    },
}

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

$ go build -o my-calc

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

$ ./my-calc
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 信息:

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

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

$ go build -o my-calc
$ ./my-calc
I'm inside initConfig function in cmd/root.go
Hello Cobra CLI

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

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

// cmd/root.go
func init() {
    fmt.Println("I'm inside init function in cmd/root.go")
    cobra.OnInitialize(initConfig)
    ...
}

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

// main.go
func main() {
     fmt.Println("I'm inside main function in main.go")
     cmd.Execute()
}

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

$ go build -o my-calc
$ ./my-calc
I'm inside init function in cmd/root.go
I'm inside main function in main.go
I'm inside initConfig function in cmd/root.go
Hello Cobra CLI

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

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

  • Persistent Flags: 该标志可用于为其分配的命令以及该命令的所有子命令。
  • Local Flags: 该标志只能用于分配给它的命令。

initConfig

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

// cmd/root.go
// initConfig 读取配置文件和环境变量
func initConfig() {
	if cfgFile != "" {
        // 使用 flag 标志中传递的配置文件
		viper.SetConfigFile(cfgFile)
	} else {
		// 获取 Home 目录
		home, err := homedir.Dir()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		// 在 Home 目录下面查找名为 ".my-calc" 的配置文件
		viper.AddConfigPath(home)
		viper.SetConfigName(".my-calc")
	}
    // 读取匹配的环境变量
	viper.AutomaticEnv()
	// 如果有配置文件,则读取它
	if err := viper.ReadInConfig(); err == nil {
		fmt.Println("Using config file:", viper.ConfigFileUsed())
	}
}

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

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

添加数据

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

$ cobra add add
add created at /Users/ych/devs/workspace/youdianzhishi/course/my-calc
$ tree .
.
├── LICENSE
├── cmd
│   ├── add.go
│   └── root.go
├── go.mod
├── go.sum
├── main.go
└── my-calc

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的子命令。

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

$ go build -o my-calc
$ ./my-calc
Hello Cobra CLI
$ ./my-calc add
add called

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

// cmd/add.go
func intAdd(args []string) {
	var sum int
	// 循环 args 参数,循环的第一个值为 args 的索引,这里我们不需要,所以用 _ 忽略掉
	for _, ival := range args {
		// 将 string 转换成 int 类型
		temp, err := strconv.Atoi(ival)
		if err != nil {
			panic(err)
		}
		sum = sum + temp
	}
	fmt.Printf("Addition of numbers %s is %d\n", args, sum)
}

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

// addCmd
Run: func(cmd *cobra.Command, args []string) {
    intAdd(args)
},

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

$ go build -o my-calc
$ ./my-calc
Hello Cobra CLI
# 注意参数之间的空格
$ ./my-calc add 1 2 3
Addition of numbers [1 2 3] is 6

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

$ ./my-calc add 1 2 3.5
panic: strconv.Atoi: parsing "3.5": invalid syntax

goroutine 1 [running]:
my-calc/cmd.intAdd(0xc0000a5890, 0x3, 0x3)
......

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

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

// cmd/add.go
func init() {
	rootCmd.AddCommand(addCmd)
	addCmd.Flags().BoolP("float", "f", false, "Add Floating Numbers")
}

然后创建一个floatAdd的函数:

func floatAdd(args []string) {
	var sum float64
	for _, fval := range args {
		// 将字符串转换成 float64 类型
		temp, err := strconv.ParseFloat(fval, 64)
		if err != nil {
			panic(err)
		}
		sum = sum + temp
	}
	fmt.Printf("Sum of floating numbers %s is %f\n", args, sum)
}

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

// cmd/add.go
// addCmd
Run: func(cmd *cobra.Command, args []string) {
    // 获取 float 标识符的值,默认为 false
    fstatus, _ := cmd.Flags().GetBool("float")
    if fstatus { // 如果为 true,则调用 floatAdd 函数
        floatAdd(args)
    } else {
        intAdd(args)
    }
},

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

$ go build -o my-calc
$ ./my-calc add 1 2 3
Addition of numbers [1 2 3] is 6
$ ./my-calc add 1 2 3.5 -f
Sum of floating numbers [1 2 3.5] is 6.500000
$./my-calc add 1 2 3.5 --float
Sum of floating numbers [1 2 3.5] is 6.500000

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

添加偶数

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

$ cobra add even
even created at /Users/ych/devs/workspace/youdianzhishi/course/my-calc

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

// cmd/even.go
func init() {
	addCmd.AddCommand(evenCmd)
}

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

// cmd/even.go
Run: func(cmd *cobra.Command, args []string) {
    var evenSum int
    for _, ival := range args {
        temp, _ := strconv.Atoi(ival)
        if temp%2 == 0 {
            evenSum = evenSum + temp
        }
    }
    fmt.Printf("The even addition of %s is %d\n", args, evenSum)
},

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

$ go build -o my-calc
$ ./my-calc add even 1 2 3 4 5 6
The even addition of [1 2 3 4 5 6] is 12

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

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

Reference:

https://www.qikqiak.com/post/create-cli-app-with-cobra在 Golang 中使用 Cobra 创建 CLI 应用

https://cobra.dev cobra docs

到此这篇关于在 Golang 中使用 Cobra 创建 CLI 应用的文章就介绍到这了,更多相关Golang创建 CLI 应用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang简单实现http的server端和client端

    介绍 HTTPS (Secure Hypertext Transfer Protocol)安全超文本传输协议,是一个安全通信通道,它基于HTTP开发用于在客户计算机和服务器之间交换信息.它使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版,是使用TLS/SSL加密的HTTP协议. HTTP和HTTPS的区别 HTTPS是加密传输协议,HTTP是名文传输协议 HTTPS需要用到SSL证书,而HTTP不用 HTTPS比HTTP更加安全,对搜索引擎更友好,利于SEO HTTPS标准端

  • Golang中基础的命令行模块urfave/cli的用法说明

    前言 相信只要部署过线上服务,都知道启动参数一定是必不可少的,当你在不同的网络.硬件.软件环境下去启动一个服务的时候,总会有一些启动参数是不确定的,这时候就需要通过命令行模块去解析这些参数,urfave/cli是Golang中一个简单实用的命令行工具. 安装 通过 go get github.com/urfave/cli 命令即可完成安装. 正文 使用了urfave/cli之后,你的程序就会变成一个命令行程序,以下就是通过urfave/cli创建的一个最简单的命令行程序,它设定了一些基础的信息,

  • golang在GRPC中设置client的超时时间

    超时 建立连接 主要就2函数Dail和DialContext. // Dial creates a client connection to the given target. func Dial(target string, opts ...DialOption) (*ClientConn, error) { return DialContext(context.Background(), target, opts...) } func DialContext(ctx context.Cont

  • golang elasticsearch Client的使用详解

    elasticsearch 的client ,通过 NewClient 建立连接,通过 NewClient 中的 Set.URL设置访问的地址,SetSniff设置集群 获得连接 后,通过 Index 方法插入数据,插入后可以通过 Get 方法获得数据(最后的测试用例中会使用 elasticsearch client 的Get 方法) func Save(item interface{}) { client, err := elastic.NewClient( elastic.SetURL("h

  • 在 Golang 中使用 Cobra 创建 CLI 应用

    虽然现在我们使用的大多数软件都是可视化的,很容易上手,但是这并不代表 CLI(命令行)应用就没有用武之地了,特别是对于开发人员来说,还是会经常和 CLI 应用打交道.而 Golang 就非常适合用来构建 CLI 应用,下面我们就将来介绍如何在 Golang 中构建一个 CLI 应用. 对于开发人员来说平时可能就需要使用到很多 CLI 工具,比如 npm.node.go.python.docker.kubectl 等等,因为这些工具非常小巧.没有依赖性.非常适合系统管理或者一些自动化任务等等. 我

  • 浅谈Golang中创建一个简单的服务器的方法

    我们知道,golang中的net/http包对网络的支持非常好,这样会让我们比较容易的建立起一个相对简单的服务器,我们来看一段代码 func sayHi(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w,"Hi") } func main() { http.HandleFunc("/sayHi", sayHi) log.Fatal(http.ListenAndServe("localhost:80

  • golang中make和new的区别示例详解

    前言 本文主要给大家介绍了关于golang中make和new区别的相关内容,分享出来供大家参考学习,话不多说了,来一起看看详细的介绍: new 和 make 都可以用来分配空间,初始化类型,但是它们确有不同. new(T) 返回的是 T 的指针 new(T) 为一个 T 类型新值分配空间并将此空间初始化为 T 的零值,返回的是新值的地址,也就是 T 类型的指针 *T,该指针指向 T 的新分配的零值. p1 := new(int) fmt.Printf("p1 --> %#v \n &quo

  • 在Golang中使用C语言代码实例

    cgo 使得在 Golang 中可以使用 C 代码. Hello World 为了有一个较为直观的了解,我们来看一个简单的例子,创建文件 main.go: 复制代码 代码如下: package main   /* #include <stdio.h>   void sayHi() {     printf("Hi"); } */ import "C"   func main() {     C.sayHi() } 执行程序: 复制代码 代码如下: go

  • 详解golang中bufio包的实现原理

    最近用golang写了一个处理文件的脚本,由于其中涉及到了文件读写,开始使用golang中的 io 包,后来发现golang 中提供了一个bufio的包,使用这个包可以大幅提高文件读写的效率,于是在网上搜索同样的文件读写为什么bufio 要比io的读写更快速呢?根据网上的资料和阅读源码,以下来详细解释下bufio的高效如何实现的. bufio 包介绍  bufio包实现了有缓冲的I/O.它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文

  • golang中struct和interface的基础使用教程

    前言 本文主要给大家介绍了关于golang中struct和interface的相关内容,是属于golang的基本知识,下面话不多说了,来一起看看详细的介绍吧. struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套:go中的struct类型理解为类,可以定义方法,和函数定义有些许区别:struct类型是值类型. struct定义 type User struct { Name string Age int32 mess string } var user User

  • golang中range在slice和map遍历中的注意事项

    golang中range在slice和map遍历中的注意事项 package main import ( "fmt" ) func main() { slice := []int{0, 1, 2, 3} myMap := make(map[int]*int) for _,v :=range slice{ if v==1 { v=100 } } for k,v :=range slice{ fmt.Println("k:",k,"v:",v) }

  • Golang中的Slice与数组及区别详解

    在golang中有数组和Slice两种数据结构,Slice是基于数组的实现,是长度动态不固定的数据结构,本质上是一个对数组字序列的引用,提供了对数组的轻量级访问.那么我们今天就给大家详细介绍下Golang中的Slice与数组, 1.Golang中的数组 数组是一种具有固定长度的基本数据结构,在golang中与C语言一样数组一旦创建了它的长度就不允许改变,数组的空余位置用0填补,不允许数组越界. 数组的一些基本操作:      1.创建数组: func main() { var arr1 = [.

  • Golang中使用Date进行日期格式化(沿用Java风格)

    本文介绍了Golang中使用Date进行日期格式化,分享给大家,具体如下: Github https://github.com/noogo/date Date Date是一个基于time包装的一个日期包,通过此包可以快速创建日期.获取时间戳.毫秒数及最重要的日期格式化,另外你还可以继续使用time包下的所有函数(除time.Foramt(string)外)你可以通过以下方法快速创建一个Date对象: Now() WithTime(t time.Time) WithTimestamp(timest

随机推荐