二丫讲梵 二丫讲梵
首页
  • 最佳实践
  • 迎刃而解
  • Nginx
  • Php
  • Zabbix
  • AWS
  • Prometheus
  • Grafana
  • CentOS
  • Systemd
  • Docker
  • Rancher
  • Ansible
  • Ldap
  • Gitlab
  • GitHub
  • Etcd
  • Consul
  • RabbitMQ
  • Kafka
  • MySql
  • MongoDB
  • OpenVPN
  • KVM
  • VMware
  • Other
  • ELK
  • K8S
  • LLM
  • Nexus
  • Jenkins
  • 随写编年
  • 家人物语
  • 追忆青春
  • 父亲的朋友圈
  • 电影音乐
  • 效率工具
  • 博客相关
  • Shell
  • 前端实践
  • Vue学习笔记
  • Golang学习笔记
  • Golang编程技巧
  • 学习周刊
  • Obsidian插件周刊
关于
友链
  • 本站索引

    • 分类
    • 标签
    • 归档
  • 本站页面

    • 导航
    • 打赏
  • 我的工具

    • 备忘录清单 (opens new window)
    • json2go (opens new window)
    • gopher (opens new window)
    • 微信MD编辑 (opens new window)
    • 国内镜像 (opens new window)
    • 出口IP查询 (opens new window)
    • 代码高亮工具 (opens new window)
  • 外站页面

    • 开往 (opens new window)
    • ldapdoc (opens new window)
    • HowToStartOpenSource (opens new window)
    • vdoing-template (opens new window)
GitHub (opens new window)

二丫讲梵

行者常至,为者常成
首页
  • 最佳实践
  • 迎刃而解
  • Nginx
  • Php
  • Zabbix
  • AWS
  • Prometheus
  • Grafana
  • CentOS
  • Systemd
  • Docker
  • Rancher
  • Ansible
  • Ldap
  • Gitlab
  • GitHub
  • Etcd
  • Consul
  • RabbitMQ
  • Kafka
  • MySql
  • MongoDB
  • OpenVPN
  • KVM
  • VMware
  • Other
  • ELK
  • K8S
  • LLM
  • Nexus
  • Jenkins
  • 随写编年
  • 家人物语
  • 追忆青春
  • 父亲的朋友圈
  • 电影音乐
  • 效率工具
  • 博客相关
  • Shell
  • 前端实践
  • Vue学习笔记
  • Golang学习笔记
  • Golang编程技巧
  • 学习周刊
  • Obsidian插件周刊
关于
友链
  • 本站索引

    • 分类
    • 标签
    • 归档
  • 本站页面

    • 导航
    • 打赏
  • 我的工具

    • 备忘录清单 (opens new window)
    • json2go (opens new window)
    • gopher (opens new window)
    • 微信MD编辑 (opens new window)
    • 国内镜像 (opens new window)
    • 出口IP查询 (opens new window)
    • 代码高亮工具 (opens new window)
  • 外站页面

    • 开往 (opens new window)
    • ldapdoc (opens new window)
    • HowToStartOpenSource (opens new window)
    • vdoing-template (opens new window)
GitHub (opens new window)
  • Shell编程

  • Go编程笔记

    • 开发技巧

    • 库包研究

      • 使用gorm进行联合查询的整理总结
      • 一个ftp客户端的封装
      • 使用go-bindata将文件编译进二进制
      • go-gitlab包源码探寻与心得
      • 利用cobra库快速开发类似kubectl一样的命令行工具
        • 1,基础物料
        • 2,安装工具
        • 3,初识项目
        • 4,例子实战
      • 使用MongoDB官方go库操作MongoDB
      • 再探-利用gorm自身提供的方法实现MySQL中数据关联的能力
      • 使用retry-go给项目添加重试机制
      • go-cache包的使用简析
      • 利用gorm自身提供的方法实现存在更新不存在则创建的能力
      • 近期关于cobra库的一些实践心得总结
    • 个人项目

  • 前端编程笔记

  • Go学习笔记

  • Vue-21年学习笔记

  • Vue-22年重学笔记

  • 编程世界
  • Go编程笔记
  • 库包研究
二丫讲梵
2022-01-16
目录

利用cobra库快速开发类似kubectl一样的命令行工具

文章发布较早,内容可能过时,阅读注意甄别。

# 1,基础物料

  • GitHub 地址:cobra (opens new window)
  • 官网:cobra.dev (opens new window)

# 2,安装工具

安装 cobra 命令行工具,我们可以通过官方提供的命令行工具对 cobra 项目进行初始化,以便快速进入开发。

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

很多地方推荐这种方式,但是安装的时候报如下错误:

$ go get -u github.com/spf13/cobra/cobra
go: downloading github.com/spf13/cobra/cobra v0.0.0-20200823174541-9ed1d713d619
go: github.com/spf13/cobra/cobra upgrade => v0.0.0-20200823174541-9ed1d713d619
go get github.com/spf13/cobra/cobra: ambiguous import: found package github.com/spf13/cobra/cobra in multiple modules:
        github.com/spf13/cobra v1.1.3 (/Users/liqilong/eryajf/letsgo/project/pkg/mod/github.com/spf13/cobra@v1.1.3/cobra)
        github.com/spf13/cobra/cobra v0.0.0-20200823174541-9ed1d713d619 (/Users/liqilong/eryajf/letsgo/project/pkg/mod/github.com/spf13/cobra/cobra@v0.0.0-20200823174541-9ed1d713d619)
1
2
3
4
5
6

如下issue (opens new window)说明了该问题。

正确姿势为:

$ go get github.com/spf13/cobra/cobra@v1.0.0
1

注意此处不要指定 -u,否则可能会遇到如下包升级后的错误:

go get: upgrading github.com/hashicorp/hcl@v1.0.0: github.com/hashicorp/hcl@v2: invalid version: reading http://nexus.eryajf.net/repository/wpt-go-group/github.com/hashicorp/hcl/@v/v2.info: 404 Not Found
1

安装成功之后,会在$GOPATH/bin路径下生成 cobra 命令的二进制文件,我们把他移动到系统环境路径下:

$ cp $GOPATH/bin/cobra /usr/local/bin
1

# 3,初识项目

使用如下命令可以直接初始化一个新项目:

$ cd $GOPATH/src/eryajf-cobra
$ cobra init --pkg-name eryajf-cobra -a eryajf
1
2
  • --pkg-name:指定包名字
  • -a:指定作者名字

初始化出来的项目目录结构如下:

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

1 directory, 3 files
1
2
3
4
5
6
7
8

主入口已经固定,我们不必过多理会,主逻辑入口在 root.go。

开始编码之前,我们先把go mod初始化一下。

go mod init
go mod tidy -v
1
2

go mod 初始化完成之后,我们可以先运行一下项目,进行简单的验证:

$ 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.
1
2
3
4
5
6
7

此时来到root.go文件中,查看rootCmd变量,在 Long字段里看到了如上输出的说明信息,此时把信息改成如下内容:

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "eryajf-cobra",
	Short: "A brief description of your application",
	Long: `eryajf-cobra 这是一个测试cobra的项目.`,
	// Uncomment the following line if your bare application
	// has an action associated with it:
	//	Run: func(cmd *cobra.Command, args []string) { },
}
1
2
3
4
5
6
7
8
9
  • Use:表示当前节点使用的命令参数。
  • Short:短的说明信息,当用户获取当前节点父节点的帮助信息时展示。
  • Long:长的说明信息,当用户获取当前节点的帮助信息时展示。
  • Run:这里是 cobra 接受命令行参数进来之后具体执行的逻辑内容。

然后再次运行:

$ go run main.go
eryajf-cobra 这是一个测试cobra的项目.
1
2

可以看到输出内容正式我们更改后的内容。

再往下看,可以看到一个 Execute方法:

// 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)
	}
}
1
2
3
4
5
6
7
8

这个方法便是main.go中调用的方法。是 cobra 跟节点的执行方法。现在默认情况下运行项目就是执行的根节点的方法,在此基础之上,我们还能增加更多的命令层级,以满足不同的需求场景。

此时我们把Run字段打开,并写入如下代码:

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "eryajf-cobra",
	Short: "A brief description of your application",
	Long:  `eryajf-cobra 这是一个测试cobra的项目.`,
	// Uncomment the following line if your bare application
	// has an action associated with it:
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("this is test run function")
	},
}
1
2
3
4
5
6
7
8
9
10
11

然后再次运行项目查看效果:

$ go run main.go
this is test run function
1
2

到这里,我们基本上就能隐约体会到 cobra 这个框架的整体套路了,接下来通过两个小示例来深入体会结合 cobra 包解决我们实际生产中的一些问题。

另外再提一句,在这个文件中还有一个方法init(),我们可以看下都做了些什么:

func init() {
	cobra.OnInitialize(initConfig)

	// Here you will define your flags and configuration settings.
	// Cobra supports persistent flags, which, if defined here,
	// will be global for your application.

	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.eryajf-cobra.yaml)")

	// Cobra also supports local flags, which will only run
	// when this action is called directly.
	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
1
2
3
4
5
6
7
8
9
10
11
12
13

首先是一个初始化配置文件的操作,这个也会在我们实际开发场景中用到,如果你的交互需要存放一些配置文件,可以结合这个方法提供的能力进行开发。

注意它提供了运行时指定和内嵌到程序两种方式。

运行时指定:

$ go run main.go config -h
eryajf-cobra 这是一个测试cobra的项目.

Usage:
  eryajf-cobra [flags]

Flags:
      --config string   config file (default is $HOME/.eryajf-cobra.yaml)
  -h, --help            help for eryajf-cobra
  -t, --toggle          Help message for toggle
1
2
3
4
5
6
7
8
9
10

当我们不指定的时候,默认会走入到下边的逻辑:

// initConfig reads in config file and ENV variables if set.
func initConfig() {
	if cfgFile != "" {
		// Use config file from the flag.
		viper.SetConfigFile(cfgFile)
	} else {
		// Find home directory.
		home, err := homedir.Dir()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		// Search config in home directory with name ".eryajf-cobra" (without extension).
		viper.AddConfigPath(home)
		viper.SetConfigName(".eryajf-cobra")
	}

	viper.AutomaticEnv() // read in environment variables that match

	// If a config file is found, read it in.
	if err := viper.ReadInConfig(); err == nil {
		fmt.Println("Using config file:", viper.ConfigFileUsed())
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

程序会读取当前用户家目录下文件名为.eryajf-cobra的配置文件。

viper 是 cobra 作者的另一个优秀且强大的配置文件交互库,为了方便演示,我们将配置文件格式指定为yaml,调整代码如下:

// initConfig reads in config file and ENV variables if set.
func initConfig() {
	if cfgFile != "" {
		// Use config file from the flag.
		viper.SetConfigFile(cfgFile)
	} else {
		// Find home directory.
		home, err := homedir.Dir()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		// fmt.Println(home)
		// Search config in home directory with name ".eryajf-cobra" (without extension).
		viper.AddConfigPath(home)
		viper.SetConfigType("yaml")
		viper.SetConfigName(".eryajf-cobra")
	}

	viper.AutomaticEnv() // read in environment variables that match
	// If a config file is found, read it in.
	if err := viper.ReadInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
			fmt.Println("Config file not found")
		} else {
			fmt.Println(err)
			os.Exit(1)
		}
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

然后写一个测试配置信息:

echo 'TEST_USER: eryajf' > $HOME/.eryajf-cobra
1

再次调整上边rootCmd的方法内容:

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "eryajf-cobra",
	Short: "A brief description of your application",
	Long:  `eryajf-cobra 这是一个测试cobra的项目.`,
	// Uncomment the following line if your bare application
	// has an action associated with it:
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println(viper.GetString("TEST_USER"))
	},
}
1
2
3
4
5
6
7
8
9
10
11

然后运行项目,可以看到程序能够正常读取到我们的配置文件了:

$ go run main.go
eryajf
1
2

当然程序默认读取的是用户家目录下的配置文件,实际使用中可按自己的需求指定配置文件路径。

# 4,例子实战

进入例子实战之前,我们先看一个简单的命令使用示例:

$ cat example.txt
这是测试内容第一行
这是测试内容第二行

$ cat -n example.txt
	 1  这是测试内容第一行
     2  这是测试内容第二行
1
2
3
4
5
6
7

使用 cat 命令我们能够查看一个文件的内容,配合-n参数能够展示文件的行号,那么现在就在如上代码基础上,用 go 来实现一下这个功能。

注意:cat 命令是直接在一级跟路径上展开的方法,下边为了举例方便,会将此命令做成一级参数来实现,最终效果大概会是下边的样子:

$ go run main.go cat example.txt
这是测试内容第一行
这是测试内容第二行

$ go run main.go cat -n example.txt
	 1  这是测试内容第一行
     2  这是测试内容第二行
1
2
3
4
5
6
7

在我们一开始还没有基础代码的时候,可以通过 cobra 提供的add方法来完成第一次代码的自动生成,先来看下这个方法:

$ cobra add -h
Add (cobra add) will create a new command, with a license and
the appropriate structure for a Cobra-based CLI application,
and register it to its parent (default rootCmd).

If you want your command to be public, pass in the command name
with an initial uppercase letter.

Example: cobra add server -> resulting in a new cmd/server.go

Usage:
  cobra add [command name] [flags]

Aliases:
  add, command

Flags:
  -h, --help            help for add
  -p, --parent string   variable name of parent command for this command (default "rootCmd")

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 (default true)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

使用这个命令我们可以快速创建一个 cobra 的子项参数包,如果你想要嵌套多层子项,可以通过-p 参数指定该子项的父项,不指定情况下,默认父项为 root。

我们创建一个 cat 的子项:

$ cobra add cat
cat created at /Users/liqilong/eryajf/letsgo/project/src/eryajf-cobra
1
2

此命令运行之后,立马就可以看到在 cmd 目录下多了个cat.go的文件,这个文件就是 cobra 自动生成的一个子项文件包:

如下内容根据上边的需求,已经做了简单的调整。

package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

// catCmd represents the cat command
var catCmd = &cobra.Command{
	Use:   "cat",
	Short: "一个命令行程序,实现cat的功能",
	Long:  `这是 eryajf 测试 cobra 而生成的 cat 子项.`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("cat called")
	},
}

func init() {
	rootCmd.AddCommand(catCmd)
	cset := catCmd.Flags()
	cset.StringP("file", "f", "", "文件名称")
	cset.BoolP("num", "n", false, "显示行号")
	catCmd.MarkFlagRequired("file")
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

我们先看init方法, 可以看到 cobra 自动将catCmd挂载到了rootCmd之下。

通过 cobra 提供的 Flags 方法,我们可以很方便的给命令挂载子参数。

注意:其中的MarkFlagRequired表示此参数为必填项,而非可选。

运行一下项目,来感受一下命令层级的设计以及应用:

$ go run main.go -h
eryajf-cobra 这是一个测试cobra的项目.

Usage:
  eryajf-cobra [flags]
  eryajf-cobra [command]

Available Commands:
  cat         一个命令行程序,实现cat的功能

  ...略...
1
2
3
4
5
6
7
8
9
10
11

可以看到我查看这个程序的帮助时cat变成了他的一个参数项,而且参数后边对应的说明内容,也是其方法中对应的Short字段的值。

接着再往下看:

$ go run main.go cat -h
这是 eryajf 测试 cobra 而生成的 cat 子项.

Usage:
  eryajf-cobra cat [flags]

Flags:
  -f, --file string   文件名称
  -h, --help          help for cat
  -n, --num           显示行号
1
2
3
4
5
6
7
8
9
10

可以看到 cat 作为根路径的子项能够进入到它自己的帮助信息内,其说明信息是其方法中对应的Long字段的值。并且还自动将我们配置的该子项的子参数列了出来。熟悉 k8s 的同学可能会有点似曾相识的感觉了,没错,kubectl命令也正是基于 cobra 这个库来完成的。

接下来我们对cat.go文件进行如下编码:

package cmd

import (
	"bufio"
	"fmt"
	"io"
	"os"

	"github.com/spf13/cobra"
)

// catCmd represents the cat command
var catCmd = &cobra.Command{
	Use:   "cat",
	Short: "一个命令行程序,实现cat的功能",
	Long:  `这是 eryajf 测试 cobra 而生成的 cat 子项.`,
	Run: func(cmd *cobra.Command, args []string) {
		name, _ := cmd.Flags().GetString("file")
		err := Tcat(name)
		if err != nil {
			errmsg := fmt.Sprintf("文件读取异常: %s\n", err)
			fmt.Println(errmsg)
		}

	},
}

func init() {
	rootCmd.AddCommand(catCmd)
	cset := catCmd.Flags()
	cset.StringP("file", "f", "", "文件名称")
	cset.BoolP("num", "n", false, "显示行号")
	catCmd.MarkFlagRequired("file")
}

func Tcat(filenamt string) error {
	fi, err := os.Open(filenamt)
	if err != nil {
		return err
	}
	defer fi.Close()
	br := bufio.NewReader(fi)
	for {
		a, _, c := br.ReadLine()
		if c == io.EOF {
			break
		}
		fmt.Println(string(a))
	}
	return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

添加了个Tcat()方法,当我们执行到 cat 函数的时候,就运行这个方法,接下来看示例:

$ go run main.go cat -f example.txt
这是测试内容第一行
这是测试内容第二行
1
2
3

其中参数与内容之间用等号或者空格都是可以的。

然后调整一下代码:

package cmd

import (
	"bufio"
	"fmt"
	"io"
	"os"

	"github.com/spf13/cobra"
)

// catCmd represents the cat command
var catCmd = &cobra.Command{
	Use:   "cat",
	Short: "一个命令行程序,实现cat的功能",
	Long:  `这是 eryajf 测试 cobra 而生成的 cat 子项.`,
	Run: func(cmd *cobra.Command, args []string) {
		name, _ := cmd.Flags().GetString("file")
		num, _ := cmd.Flags().GetBool("num")
		err := Tcat(num, name)
		if err != nil {
			errmsg := fmt.Sprintf("文件读取异常: %s\n", err)
			fmt.Println(errmsg)
		}
	},
}

func init() {
	rootCmd.AddCommand(catCmd)
	cset := catCmd.Flags()
	cset.StringP("file", "f", "", "文件名称")
	cset.BoolP("num", "n", false, "显示行号")
	catCmd.MarkFlagRequired("file")
}

func Tcat(num bool, filenamt string) error {
	fi, err := os.Open(filenamt)
	if err != nil {
		return err
	}
	defer fi.Close()
	br := bufio.NewReader(fi)
	i := 0
	for {
		i++
		a, _, c := br.ReadLine()
		if c == io.EOF {
			break
		}
		if num {
			fmt.Println("\t", i, string(a))
		} else {
			fmt.Println(string(a))
		}
	}
	return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

此时这个程序也集成了查看文件行号的能力,测试如下:

$ go run main.go cat  -f example.txt
这是测试内容第一行
这是测试内容第二行

$ go run main.go cat -n  -f example.txt
         1 这是测试内容第一行
         2 这是测试内容第二行
1
2
3
4
5
6
7

注意这里的-n使用的是一个布尔参数,关于这个参数的使用方式,在这个帖子 (opens new window)有详细的方法。

以上就是 cobra 这个库在我们实际开发过程中的一些实践应用了,基本上常用的姿势我这里都涵盖到了,如果你还有其他疑问,或者更好的经验,欢迎留言区评论分享。

微信 支付宝
上次更新: 2024/07/04, 22:40:37
go-gitlab包源码探寻与心得
使用MongoDB官方go库操作MongoDB

← go-gitlab包源码探寻与心得 使用MongoDB官方go库操作MongoDB→

最近更新
01
记录二五年五一之短暂回归家庭
05-09
02
学习周刊-总第210期-2025年第19周
05-09
03
学习周刊-总第209期-2025年第18周
05-03
更多文章>
Theme by Vdoing | Copyright © 2017-2025 | 点击查看十年之约 | 浙ICP备18057030号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式