利用cobra库快速开发类似kubectl一样的命令行工具
# 1,基础物料
- GitHub 地址:cobra (opens new window)
- 官网:cobra.dev (opens new window)
# 2,安装工具
安装 cobra 命令行工具,我们可以通过官方提供的命令行工具对 cobra 项目进行初始化,以便快速进入开发。
$ go get -u github.com/spf13/cobra/cobra
很多地方推荐这种方式,但是安装的时候报如下错误:
$ 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)
2
3
4
5
6
如下issue (opens new window)说明了该问题。
正确姿势为:
$ go get github.com/spf13/cobra/cobra@v1.0.0
注意此处不要指定 -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
安装成功之后,会在$GOPATH/bin
路径下生成 cobra 命令的二进制文件,我们把他移动到系统环境路径下:
$ cp $GOPATH/bin/cobra /usr/local/bin
# 3,初识项目
使用如下命令可以直接初始化一个新项目:
$ cd $GOPATH/src/eryajf-cobra
$ cobra init --pkg-name eryajf-cobra -a eryajf
2
--pkg-name
:指定包名字-a
:指定作者名字
初始化出来的项目目录结构如下:
$ tree
.
├── LICENSE
├── cmd
│ └── root.go
└── main.go
1 directory, 3 files
2
3
4
5
6
7
8
主入口已经固定,我们不必过多理会,主逻辑入口在 root.go
。
开始编码之前,我们先把go mod
初始化一下。
go mod init
go mod tidy -v
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.
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) { },
}
2
3
4
5
6
7
8
9
Use:
表示当前节点使用的命令参数。Short:
短的说明信息,当用户获取当前节点父节点的帮助信息时展示。Long:
长的说明信息,当用户获取当前节点的帮助信息时展示。Run:
这里是 cobra 接受命令行参数进来之后具体执行的逻辑内容。
然后再次运行:
$ go run main.go
eryajf-cobra 这是一个测试cobra的项目.
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)
}
}
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")
},
}
2
3
4
5
6
7
8
9
10
11
然后再次运行项目查看效果:
$ go run main.go
this is test run function
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")
}
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
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())
}
}
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)
}
}
}
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
再次调整上边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"))
},
}
2
3
4
5
6
7
8
9
10
11
然后运行项目,可以看到程序能够正常读取到我们的配置文件了:
$ go run main.go
eryajf
2
当然程序默认读取的是用户家目录下的配置文件,实际使用中可按自己的需求指定配置文件路径。
# 4,例子实战
进入例子实战之前,我们先看一个简单的命令使用示例:
$ cat example.txt
这是测试内容第一行
这是测试内容第二行
$ cat -n example.txt
1 这是测试内容第一行
2 这是测试内容第二行
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 这是测试内容第二行
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)
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
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")
}
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的功能
...略...
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 显示行号
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
}
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
这是测试内容第一行
这是测试内容第二行
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
}
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 这是测试内容第二行
2
3
4
5
6
7
注意这里的-n
使用的是一个布尔参数,关于这个参数的使用方式,在这个帖子 (opens new window)有详细的方法。
以上就是 cobra 这个库在我们实际开发过程中的一些实践应用了,基本上常用的姿势我这里都涵盖到了,如果你还有其他疑问,或者更好的经验,欢迎留言区评论分享。