Cobra:命令行框架
Cobra 就像 gotools 和 git,在命令行输入命令即可完成任务,如:
- git clone URL --bare
- go mod tidy
Cobra 能让我们轻松的实现命令行。
背景介绍
Kubernetes, Hugo, and GitHub CLI 这样的项目,都使用了 Cobra
了解 Cobra 命令结构
Cobra 建立在 commands、arguments 和 flags 结构之上
- commands 代表命令
- arguments 代表非选项参数(必需参数)
- flags 代表选项参数(也叫标志),标志是通过 --xxx 或者 -x 来使用的
在用户视角,命令就像下面这样:
# 'server' is a command, and 'port' is a flag
hugo server --port=1313
# 类似 git
# 'URL' is an argument
git clone URL --bare
命令的组成部分
在开发者视角,一个命令如下所示:
var greetCmd = &cobra.Command{
Use: "greet [name]",
Short: "Greet a person by name",
Long: `This command greets a person by the provided name.
For example, if you run 'myapp greet John', it will print 'Hello, John!'.`,
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
fmt.Printf("Hello, %s!\n", name)
},
}
1、Use:命令的使用格式
可以包含命令名、参数占位符和标志等信息,主要用于向用户展示命令的基本调用方式。
// greet 是命令名,[name] 是一个参数占位符
var greetCmd = &cobra.Command{
Use: "greet [name]",
// 其他字段...
}
2、short & long:描述
描述命令的主要功能,short 简短描述,long 详细描述
3、run:命令处理函数
当用户在命令行中调用该命令时,这个函数会被执行。
PreRun and PostRun Hooks
同时,run 函数支持前置与后置处理的钩子函数
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A simple application with hooks",
// 根命令的前置钩子,仅对根命令生效
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("PreRun of root command is executing...")
},
// 根命令的持久化前置钩子,对根命令及其所有子命令生效
PersistentPreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("PersistentPreRun of root command is executing...")
},
// 根命令的执行逻辑
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Root command is running...")
},
// 根命令的后置钩子,仅对根命令生效
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("PostRun of root command is executing...")
},
// 根命令的持久化后置钩子,对根命令及其所有子命令生效
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("PersistentPostRun of root command is executing...")
},
}
这些函数的运行顺序如下:
注意事项
- PersistentPreRun 在根命令及其所有子命令执行之前都会被调用
- PreRun 只会在该命令运行时调用,子命令是不会调用的
4、Args:参数验证
cmd 中还有个 Args 部分,用于参数验证,避免传递了非选项参数
内置验证函数
Cobra内置了一些验证函数:
- NoArgs:如果存在任何非选项参数,该命令将报错。
- ArbitraryArgs:该命令将接受任何非选项参数。
- OnlyValidArgs:如果有任何非选项参数不在Command的ValidArgs字段中,该命令将报错。
- MinimumNArgs(int):如果没有至少N个非选项参数,该命令将报错。
- MaximumNArgs(int):如果有多于N个非选项参数,该命令将报错。
- ExactArgs(int):如果非选项参数个数不为N,该命令将报错。
- ExactValidArgs(int):如果非选项参数的个数不为N,或者非选项参数不在Command的ValidArgs字段中,该命令将报错。
- RangeArgs(min, max):如果非选项参数的个数不在min和max之间,该命令将报错。
使用预定义验证函数,示例如下:
var cmd = &cobra.Command{
Short: "hello",
Args: cobra.MinimumNArgs(1), // 使用内置的验证函数
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello, World!")
},
}
自定义验证函数
var cmd = &cobra.Command{
Short: "hello",
// Args: cobra.MinimumNArgs(10), // 使用内置的验证函数
Args: func(cmd *cobra.Command, args []string) error { // 自定义验证函数
if len(args) < 1 {
return errors.New("requires at least one arg")
}
if myapp.IsValidColor(args[0]) {
return nil
}
return fmt.Errorf("invalid color specified: %s", args[0])
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello, World!")
},
}
5、TraverseChildren
默认情况下,Cobra只解析目标命令上的本地标志,父命令上的任何本地标志都会被忽略。通过启用 TraverseChildren,在执行目标命令之前,Cobra 将解析每个命令上的本地标志。
command := cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
TraverseChildren: true,
}
创建命令
Cobra提供了两种方式来创建命令:Cobra命令和Cobra库。Cobra命令可以生成一个Cobra命令模板,而命令模板也是通过引用Cobra库来构建命令的。
方法一:Cobra 库
如果要用Cobra库编码实现一个应用程序,需要首先创建一个空的main.go文件和一个rootCmd文件,之后可以根据需要添加其他命令。具体步骤如下:
1、创建 rootCmd:根命令
通常情况下,我们会将rootCmd放在文件cmd/root.go 或者 cmd/command.go中。
// rootCmd represents the base command when called without any subcommands
// 1. Hugo
var rootCmd = &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at http://hugo.spf13.com`,
Run: func(cmd *cobra.Command, args []string) {
// Do Stuff Here
},
}
// 2. go-answer
rootCmd = &cobra.Command{
Use: "answer",
Short: "Answer is a minimalist open source Q&A community.",
Long: `Answer is a minimalist open source Q&A community.
To run answer, use:
- 'answer init' to initialize the required environment.
- 'answer run' to launch application.`,
}
意义:管理子命令
rootCmd 可以理解为一个容器,管理所有子命令
例如 git 命令的根节点是 git,git commit 是其子命令
可以没有 run 方法
- 如果
rootCmd不定义Run,直接运行程序时会提示错误并要求指定子命令 - 但如果已经有子命令,可以不定义 run 方法
rootCmd 的 init 函数
1.1 全局命令参数:PersistentFlags
也叫持久化标志
通过 PersistentFlags 定义的参数可被所有子命令继承,常用于全局配置(如日志级别、配置文件路径等)
所有子命令都可以通过 --xxxFlag 标志修改该参数
func init() {
// ...
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "Author name for copyright attribution")
rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "Name of license for the project (can provide `licensetext` in config)")
rootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
// 绑定参数到 Viper(便于配置文件覆盖)
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
viper.BindPFlag("projectbase", rootCmd.PersistentFlags().Lookup("projectbase"))
viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
viper.SetDefault("license", "apache")
}
PersistentFlags returns the persistent FlagSet specifically set in the current command.
这里其实是借助 PFlag 包进行参数解析的
StringVar:字符串变量绑定
func (f *FlagSet) StringVar(p *string, name string, value string, usage string)
// p:指向要绑定的字符串变量的指针。
// name:标志的名称,用户在命令行中使用 --name 的形式来设置该标志。
// value:标志的默认值。
// usage:标志的使用说明,会在帮助信息中显示。
示例:
var (
configFilePath string
)
// 定义了一个名为 config 的标志,其默认值为 ./config.yaml,并将其绑定到 configFilePath 变量上
// 用户可以通过 --config 标志来指定不同的配置文件路径
func init() {
rootCmd.PersistentFlags().StringVar(&configFilePath, "config", "./config.yaml", "Path to the configuration file")
}
// 使用时,因为是变量,直接访问即可
rootCmd = &cobra.Command{
Use: "myapp",
Short: "A simple app",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Config file path: %s\n", configFilePath)
},
}
StringVarP:额外支持短名称
StringVar 的扩展版本,额外支持了短名称
可以看到,在 StringVar 的基础上,参数多了一个 shorthand 短名称
func (f *FlagSet) StringVarP(p *string, name, shorthand string, value string, usage string)
// p:指向要绑定的字符串变量的指针。
// name:标志的长名称,用户在命令行中使用 --name 的形式来设置该标志。
// shorthand:标志的短名称,用户可以使用 -s 的形式来设置该标志。
// value:标志的默认值。
// usage:标志的使用说明,会在帮助信息中显示。
示例:
var (
configFilePath string
)
// 可以使用 --config 或 -c 来设置配置文件的路径
rootCmd.PersistentFlags().StringVarP(&configFilePath, "config", "c", "./config.yaml", "Path to the configuration file")
StringP:动态获取
不直接绑定到变量,需要在命令执行时通过 cmd.Flag() 方法来获取标志的值,适用于需要动态获取标志值的场景。
func (f *FlagSet) StringP(name, shorthand string, value string, usage string) *string
// name:标志的长名称,用户在命令行中使用 --name 的形式来设置该标志。
// shorthand:标志的短名称,用户可以使用 -s 的形式来设置该标志。
// value:标志的默认值。
// usage:标志的使用说明,会在帮助信息中显示。
// 返回一个指向存储标志值的变量的指针
示例:
func init() {
rootCmd.PersistentFlags().StringP("config", "c", "./config.yaml", "Path to the configuration file")
}
// run 函数中动态获取参数值
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A simple app",
Run: func(cmd *cobra.Command, args []string) {
configFilePath := cmd.Flag("config").Value.String()
fmt.Printf("Config file path: %s\n", configFilePath)
},
}
其他方法
还支持 :
IntVar、IntVarP和IntPBoolVar和BoolVarP、BoolP、BoolFloat...
用法都是类似的
函数名带
P说明支持短选项,否则不支持短选项。函数名带
Var说明是将标志的值绑定到变量,否则是将标志的值存储在指针中。
BindPFlag:Viper 配置绑定到标志
这里绑定的意思是:
- Viper 中对应的 value,就是该标志的默认值
- 用户在命令行中使用
--xxx该标志时,自动修改 Viper 的值(不影响配置文件本身)
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error
// key:这是 Viper 中用于存储配置值的键名。你可以通过 viper.Get(key) 等方法来获取该键对应的值。
// flag:这是一个指向 pflag.Flag 类型的指针,代表 Cobra 定义的命令行标志。
// 通常可以通过 rootCmd.PersistentFlags().Lookup(flagName) 方法来获取指定名称的标志对象。
// 如果绑定成功,函数返回 nil;
// 如果出现错误,如标志对象为空或绑定过程中发生其他异常,函数将返回相应的错误信息。
示例:
通常搭配 rootCmd.PersistentFlags().Lookup 来获取到对应的标志对象
// 绑定参数到 Viper(便于配置文件覆盖)
// 当用户在命令行中使用 --author 标志指定作者姓名时,该值会被自动存储到 Viper 的 author 键中
// 如果 Viper 从配置文件或其他来源读取到 author 键的值,也会影响到该命令行标志的默认值。
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
通过 viper.Get() 获取该标志的值
1.2 结合 Viper 获取配置文件
func init() {
cobra.OnInitialize(initConfig)
// ...
}
func initConfig() {
// Don't forget to read config either from cfgFile or from home directory!
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 ".cobra" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
if err := viper.ReadInConfig(); err != nil {
fmt.Println("Can't read config:", err)
os.Exit(1)
}
}
1.3 预执行逻辑:PersistentPreRunE
func init() {
// ...
// 添加全局预执行钩子
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// 检查必要环境变量
if os.Getenv("API_KEY") == "" {
return fmt.Errorf("必须设置 API_KEY 环境变量")
}
// 初始化数据库连接池
db, err := initDatabase(viper.GetString("db.url"))
if err != nil {
return err
}
// 注入到全局上下文
cmd.SetContext(context.WithValue(cmd.Context(), "db", db))
return nil
}
}
2、Excecute 方法:调用 rootCmd
我们还需要一个main函数来调用rootCmd,通常我们会创建一个main.go文件,在main.go中调用rootCmd.Execute()来执行命令
main.go中不建议放很多代码,通常只需要调用cmd.Execute()即可
func Main() {
log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath)))
Execute()
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
3、添加子命令:AddCommand
除了rootCmd,我们还可以调用AddCommand添加其他命令,通常情况下,我们会把其他命令的源码文件放在cmd/目录下。
示例如下,创建cmd/version.go文件添加一个version命令
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
},
}
本地标志:子命令特有的标志 Flags
本地标志只能在它所绑定的命令上使用:
# 只能在 rootCmd 上使用 --source,子命令也不行
rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
设置标志为必选:MarkFlagRequired
默认情况下,标志是可选的,我们也可以设置标志为必选,当设置标志为必选,但是没有提供标志时,Cobra会报错。
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
标志组:约束标志的组合方法
标志组提供多个标志组合使用时的约束方法。
// MarkFlagsRequiredTogether
// 如果用户提供了 `--username` 标志,则必须同时提供 `--password` 标志
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")
// MarkFlagsMutuallyExclusive
// 二者冲突,只能提供其一
rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")
// MarkFlagsOneRequired
// 二者至少提供一个
rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
rootCmd.MarkFlagsOneRequired("json", "yaml")
// 搭配 MarkFlagsMutuallyExclusive 使用 , 即二者必须提供一个
rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")
标志小结
- 在 rootCmd 可以通过 PersistentFlags 设置全局标志,所有命令通用
- 在子Cmd 可以通过 Flags 设置特有的标志,该子命令独享该标志
- 标志可以与 Viper 进行绑定
- 可以通过 MarkFlagRequired 使标志必选
- 可以通过设置标志组,对多个标志组合进行约束
代码组织结构
1、聚合
所有子命令都放在同一个文件中,如下:
var (
// rootCmd represents the base command when called without any subcommands
rootCmd = &cobra.Command{
Use: "answer",
Short: "Answer is a minimalist open source Q&A community.",
Long: `Answer is a minimalist open source Q&A community.
To run answer, use:
- 'answer init' to initialize the required environment.
- 'answer run' to launch application.`,
}
// runCmd represents the run command
runCmd = &cobra.Command{
Use: "run",
Short: "Run the application",
Long: `Run the application`,
Run: func(_ *cobra.Command, _ []string) {
cli.FormatAllPath(dataDirPath)
fmt.Println("config file path: ", cli.GetConfigFilePath())
fmt.Println("Answer is starting..........................")
runApp()
},
}
此时,可以在 init 函数中 AddCommand
func init() {
rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time)
rootCmd.PersistentFlags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/")
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
// ...
// 统一 add
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd} {
rootCmd.AddCommand(cmd)
}
}
2、分散
每个文件对应一个子命令
// 各自在 init 里 AddCommand
func init() {
rootCmd.AddCommand(versionCmd)
}
方法二:Cobra Generator 更简单的方式
For complete details on using the Cobra generator, please refer to The Cobra-CLI Generator README
安装 cobra-cli
go install github.com/spf13/cobra-cli@latest
# Go will automatically install it in your $GOPATH/bin directory which should be in your $PATH.
Cobra Generator 只有两个操作
1、init:初始化项目骨架
创建一个新的骨架项目
cobra-cli init [app]
使用示例:
cd $HOME/code
mkdir myapp
cd myapp
go mod init github.com/spf13/myapp
cd $HOME/code/myapp
# automatically setup viper
# 还支持 author license
cobra-cli init --viper
go run main.go
2、add:添加命令
默认情况下,以 rootCmd 作为父命令
cobra-cli add greet
这将在 cmd 目录下创建一个名为 greet.go 的文件,该文件包含了 greet 命令的基本结构。
// $ greet.go
// greetCmd represents the greet command
var greetCmd = &cobra.Command{
Use: "greet",
Short: "A brief description of your command",
Long: `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.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("greet called")
},
}
func init() {
rootCmd.AddCommand(greetCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// greetCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// greetCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
添加子命令:-p
为 greet 命令创建一个名为 morning 的子命令
cobra-cli add morning -p "greetCmd"
这将在 cmd 目录下创建一个名为 morning.go 的文件,并将 morning 命令作为 greet 命令的子命令。
注意事项
- 命令命名,使用驼峰命名法,比如
cobra-cli add addUser,不能用下划线 - Generator 实现上,会将 greet 命令代码命为 greetCmd(自动加 Cmd)
配置 Cobra Generator
提供公共信息
# ~/.cobra.yaml file:
author: Steve Francia <spf@spf13.com>
year: 2020
license:
header: This file is part of CLI application foo.
text: |
{{ .copyright }}
This is my license. There are many like it, but this one is mine.
My license is my best friend. It is my life. I must master it as I must
master my life.
参考文档
Go 语言项目开发实战