从本章节开始,我们将开始学习如何在cobra的脚手架工程中进行命令行的开发。我们首先清理上文cobra脚手架代码,使用如下命令生成一个崭新的脚手架工程。
cobra-cli init --viper
cobra-cli add cmd1
cobra-cli add cmd2 -p "cmd1Cmd"
脚手架代码生成完毕后的目录结构如下:
├── cmd
│ ├── cmd1.go
│ ├── cmd2.go
│ └── root.go
├── go.mod
├── go.sum
├── LICENSE
└── main.g
还记得我们在上文提及到框架代码入口吗?整个cobra框架的核心入口函数就是main.go函数,我们来看这个函数做了些什么事情。函数方法体内仅执行了Execute()函数。
package main
import "cli-demo/cmd"
func main() {
cmd.Execute()// cobra框架函数函数执行入口函数
}
跳转到Execute函数所在的文件/cmd/root.go,其中rootCmd就是根指令,该指令是cobra命令行默认执行的命令逻辑,不需要格外传递任何自定义的命令标识。笔者修改了Run方法的内容,并且只通过go run main.go 执行了root指令的代码逻辑,输出了helloworld字符。
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "cli-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) {
//在这里编写代码逻辑
fmt.Println("helloworld")
},
}
....后续代码省略
上述的&cobra.Command结构体中的各个字段的含义如下:
上文中,我们添加了一个自定义指令cmd1,我们修改cmd1中Command字段,可以清晰的观察到程序输出结果的变化。
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// cmd1Cmd represents the cmd1 command
var cmd1Cmd = &cobra.Command{
Use: "cmd1",
Short: "cmd1简短描述",
Long: `cmd1详细描述`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("cmd1 执行了")
},
}
func init() {
rootCmd.AddCommand(cmd1Cmd)
}
我们来观察一下修改命令后的效果,简短的short描述一般是在父级命令的帮助信息中使用,复杂的long描述一般是命令自身的帮助信息中展示。
roottest:~/go/src/cli-demo# go run main.go -h
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.
Usage:
cli-demo [flags]
cli-demo [command]
Available Commands:
cmd1 cmd1简短描述 //在父级命令的帮助文档中展示了简短描述信息
completion Generate the autocompletion script for the specified shell
help Help about any command
Flags:
--config string config file (default is $HOME/.cli-demo.yaml)
-h, --help help for cli-demo
-t, --toggle Help message for toggle
Use "cli-demo [command] --help" for more information about a command.
---------
roottest:~/go/src/cli-demo# go run main.go cmd1 -h
cmd1详细描述 //在自己的帮助文档中展示详细描述信息
Usage:
cli-demo cmd1 [flags]
cli-demo cmd1 [command]
Available Commands:
cmd2 A brief description of your command
Flags:
-h, --help help for cmd1
Global Flags:
--config string config file (default is $HOME/.cli-demo.yaml)
Use "cli-demo cmd1 [command] --help" for more information about a command.
------
roottest:~/go/src/cli-demo# go run main.go cmd1
cmd1 执行了
接下来我们观察一下父子指令之间的拓扑关系建立的代码逻辑是怎么实现的,还记得我们刚刚创建的cmd2指令吗,我们进入到cmd2.go,观察cmd1和cmd2之间建立映射关系的核心代码逻辑:
/cmd/cmd2.go
func init() {
// 在这里建立了cmd1和cmd2之间的函数映射关系
cmd1Cmd.AddCommand(cmd2Cmd)
}
golang代码在编译时会默认首先加载引用的包中的init()函数,我们首先回到程序入口,main方法的入口我们可以看到该入口函数import了cli-demo/cmd这个包,
import "cli-demo/cmd"
我们观察一下这个cmd包里的全部.go代码,程序运行时会依次执行这些代码中的init函数,从而逐个建立指令之间的拓扑关系。而这三者均在init函数中建立了彼此间的拓扑映射关系。
├── cmd1.go
├── cmd2.go
└── root.go
所以这三个命令cmd1,cmd2,root的父子依赖关系是:
上述目录结构其实是将所有的命令都放到同一级的目录结构下,当命令过多的时候,会大大增加代码阅读成本,也会增加循环依赖的风险,一般在业务开发时候采取如下目录结构会比较方便:
├── cmd1.go
├── root.go
└── sub
└── cmd2.go
其中,在cmd1.go的init中,将cmd1Cmd1添加cmd2Cmd作为子命令。
func init() {
rootCmd.AddCommand(cmd1Cmd)
cmd1Cmd.AddCommand(sub.Cmd2Cmd)
}
行文到此处,我们已经介绍了命令行指令的两个组成部分:命令和子命令,接下来我们介绍如何给命令行添加flag标识。
首先,需要定义flag绑定的变量名称。上文中已经提及了flag的解析方式,其中对于–flag1 value1的这种形式的flag命令组成成分,可以将value1绑定到某个变量上用以执行后续代码逻辑
var Verbose bool
var Source string
cobra中对于flag的声明方式有两种:
cmd1Cmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
cmd1Cmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
我们分别执行-h指令来看看上述定义实现的效果:
roottest:~/go/src/cli-demo# go run main.go cmd1 -h
cmd1详细描述
Usage:
cli-demo cmd1 [flags]
cli-demo cmd1 [command]
Available Commands:
cmd2 A brief description of your command
Flags:
-h, --help help for cmd1
-s, --source string Source directory to read from
-v, --verbose verbose output
Global Flags:
--config string config file (default is $HOME/.cli-demo.yaml)
------
roottest:~/go/src/cli-demo# go run main.go cmd1 cmd2 -h
A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.
Usage:
cli-demo cmd1 cmd2 [flags]
Flags:
-h, --help help for cmd2
Global Flags:
--config string config file (default is $HOME/.cli-demo.yaml)
-v, --verbose verbose output
对于子命令cmd2而言,它继承了父级命令的持久标识-v,但是没有继承父级命令的-s标识。现在我们来验证flag的数值是否绑定到了刚刚定义的两个变量上。
我们修改一下cmd1Cmd,并执行指令go run main.go cmd1 -v 1 -s zf,可以打印flag绑定的变量的数值:
// cmd1Cmd represents the cmd1 command
var cmd1Cmd = &cobra.Command{
Use: "cmd1",
Short: "cmd1简短描述",
Long: `cmd1详细描述`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("cmd1 执行了")
fmt.Println("Verbose:", Verbose)
fmt.Println("Source:", Source)
},
}
默认情况下,标志是可选的。相反,如果希望命令在未设置标志时报告错误,可以添加以下的代码配置:
cmd1Cmd.MarkFlagRequired("source") #本地标识的缺省异常定义
cmd1Cmd.MarkPersistentFlagRequired("xxx") #持久标识的缺省异常定义
现在如果不输入-s flag配置,程序会报错。ps:这个配置似乎对于bool类型的flag不生效。如果期望对一组flag设置规则可以参考下面几组例子,
案例一:对于这么一组flag,如果期望username和password必须一起键入,使用MarkFlagsRequiredTogether
rootCmd.Flags().StringVarP(&u, "username", "u", "", "Username (required if password is set)")
rootCmd.Flags().StringVarP(&pw, "password", "p", "", "Password (required if username is set)")
rootCmd.MarkFlagsRequiredTogether("username", "password")
案例二:对于这么一组flag,如果期望json和yaml两个flag只能两者选其一,使用MarkFlagsMutuallyExclusive
rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")
案例三:对于这么一组flag,如果期望json和yaml两个flag至少选择一个flag键入,使用MarkFlagsOneRequired和MarkFlagsMutuallyExclusive的组合
rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
rootCmd.MarkFlagsOneRequired("json", "yaml")
rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")
Command的结构体中可以指定args参数的校验逻辑,检验逻辑一般分为以下三种:
func(cmd *cobra.Command, args []string) error {}
下面给出一个示例,还是在cmd1上进行代码的修改:
var cmd1Cmd = &cobra.Command{
Use: "cmd1",
Short: "cmd1简短描述",
Long: `cmd1详细描述`,
ValidArgs: []string{"a", "b"},
Args: cobra.MatchAll(cobra.ExactArgs(2), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("cmd1 执行了")
fmt.Println("Verbose:", Verbose)
fmt.Println("Source:", Source)
fmt.Println("first arg is:", args[0])
fmt.Println("second arg is:", args[1])
},
}
下面看一下实现的结果:
roottest:~/go/src/cli-demo# go run main.go cmd1 -s 1 a
Error: accepts 2 arg(s), received 1
-----
roottest:~/go/src/cli-demo# go run main.go cmd1 -s 1 a c
Error: invalid argument "c" for "cli-demo cmd1"
-----
roottest:~/go/src/cli-demo# go run main.go cmd1 -s 1 a b
cmd1 执行了
Verbose: false
Source: 1
first arg is: a
second arg is: b
这个是cobra的更高级别的使用方式,前文所述,cobra执行的代码逻辑主要是在Command结构体的Run字段定义的,其实在执行Run函数前后,还定义定义了一些钩子函数。这些字段中定义的代码块的执行顺序如下,其中Persistent开头的方法会被子命令继承:
// cmd1Cmd represents the cmd1 command
var cmd1Cmd = &cobra.Command{
Use: "cmd1",
Short: "cmd1简短描述",
Long: `cmd1详细描述`,
ValidArgs: []string{"a", "b"},
Args: cobra.MatchAll(cobra.ExactArgs(2), cobra.OnlyValidArgs),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside cmd1Cmd PersistentPreRun with args: %v\n", args)
},
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside cmd1Cmd PreRun with args: %v\n", args)
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("cmd1 执行了")
fmt.Println("Verbose:", Verbose)
fmt.Println("Source:", Source)
fmt.Println("first arg is:", args[0])
fmt.Println("second arg is:", args[1])
},
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside cmd1Cmd PostRun with args: %v\n", args)
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside cmd1Cmd PersistentPostRun with args: %v\n", args)
},
}
执行结果如下所示,这里子命令继承了父命令中的persistent开头的两个钩子:
roottest:~/go/src/cli-demo# go run main.go cmd1 -s 1 a b
Inside cmd1Cmd PersistentPreRun with args: [a b]
Inside cmd1Cmd PreRun with args: [a b]
cmd1 执行了
Verbose: false
Source: 1
first arg is: a
second arg is: b
Inside cmd1Cmd PostRun with args: [a b]
Inside cmd1Cmd PersistentPostRun with args: [a b]
roottest:~/go/src/cli-demo# go run main.go cmd1 cmd2
Inside cmd1Cmd PersistentPreRun with args: []
cmd2 called
Inside cmd1Cmd PersistentPostRun with args: []
这个钩子可以用来进行统一的session转换或者登录校验,可以在rootCmd设置以达到类似于spring的aop的效果
可以为命令提供自己的帮助命令或自己的模板,具体使用方式如下
cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)
至此,cobra的基础用法介绍完毕,后续有机会的话笔者会更新一些在官方文档中未能介绍的巧妙用法,期望能够帮助大家快速入门cobra。