Cobra 的使用Cobra 是一个用于创建 cli 命令行程序的库。它可以帮助我们组织根命令、子命令、flag、参数校验以及命令执行前后的生命周期逻辑。相关代码test-cli参考资料Cobra 官方仓库Cobra CLI 官方说明1. 使用 Cobra 创建 CLI 程序1.1 安装 cobra-clicobra-cli 是 Cobra 提供的代码生成工具可以自动生成项目和命令文件。go install github.com/spf13/cobra-clilatest如果终端找不到 cobra-cli可以使用完整路径$(go env GOPATH)/bin/cobra-cli --help如果 Go 的可执行文件目录已经加入 PATH则可以直接使用cobra-cli --help需要区分github.com/spf13/cobra 是项目运行时使用的 Go 库。cobra-cli 是生成项目骨架和子命令文件的工具。1.2 初始化 Go Module进入准备创建 CLI 的目录mkdir my-cli cd my-cli go mod init github.com/你的用户名/my-cli如果目录中已经存在 go.mod不要重复执行 go mod init。1.3 初始化 Cobra在 Go Module 根目录执行cobra-cli init --author 你的名字 --license apache这条命令会生成基本项目结构my-cli/ ├── main.go ├── go.mod ├── go.sum ├── LICENSE └── cmd/ └── root.go参数说明–author生成代码中的作者或版权归属信息。–license apache使用 Apache License 2.0 许可证模板。如果暂时不需要作者和许可证信息可以直接执行 cobra-cli init。初始化后整理依赖go mod tidy1.4 main.go 和 cmd.Execute()Cobra 项目的 main.go 通常很简单package main import github.com/你的用户名/my-cli/cmd func main() { cmd.Execute() }cmd.Execute() 通常定义在 cmd/root.go 中func Execute() { err : rootCmd.Execute() if err ! nil { os.Exit(1) } }它会启动整个 Cobra 命令树main() - cmd.Execute() - rootCmd.Execute() - 解析命令、flag 和参数 - 执行对应命令1.5 root.go 根命令根命令是整个 CLI 的入口通常定义在 cmd/root.govar rootCmd cobra.Command{ Use: my-cli, Short: my-cli 的简短描述, Long: my-cli 的详细描述, Run: func(cmd *cobra.Command, args []string) { fmt.Println(my-cli started) }, }cobra.Command 中几个常用字段Use命令名称。Short命令的简短描述。Long命令的详细描述。Run命令的核心执行逻辑。RunE可以返回错误的执行逻辑。当前项目的根命令使用了var rootCmd cobra.Command{ Use: test-cli, Short: test-cli is a test cli tool ,this is short description, Long: test-cli is a test cli tool, this is long description, Run: func(cmd *cobra.Command, args []string) { fmt.Println(测试test-cli) }, }运行根命令go run .查看帮助go run . --help go run . -h–help 和 -h 是 Cobra 自动添加的帮助 flag。它们会显示命令的描述、用法、子命令和 flag。2. 创建子命令2.1 使用 cobra-cli add在项目根目录执行cobra-cli add run cobra-cli add exec执行后会生成cmd/run.go cmd/exec.go项目命令结构变成test-cli ├── run └── exec2.2 注册子命令生成的命令文件会在 init() 中注册到根命令func init() { rootCmd.AddCommand(runCmd) }exec 命令也是相同的方式func init() { rootCmd.AddCommand(execCmd) }AddCommand 的作用是建立父子命令关系。没有调用 AddCommand 时即使定义了一个 cobra.Command用户也不能通过根命令找到它。2.3 定义子命令一个基本的子命令如下var runCmd cobra.Command{ Use: run, Short: 执行 run 命令, Long: run 命令的详细描述, Run: func(cmd *cobra.Command, args []string) { fmt.Println(run 子命令执行成功) }, }运行子命令go run . run查看子命令帮助go run . run --help go run . run -h完整的命令调用形式是根命令 子命令 flag 参数例如test-cli run test-cli exec --info abc2.4 子命令的执行流程执行go run . runCobra 会启动 main.go。调用 cmd.Execute()。找到 runCmd。解析 run 的 flag 和参数。执行 runCmd 的执行逻辑。3. 创建 flagflag 用于修改命令的执行行为例如test-cli exec --info abc test-cli run --verboseflag 通常分为字符串 flag需要接收一个字符串值。布尔 flag表示打开或关闭一个选项。数字 flag接收整数或浮点数。本地 flag只属于某一个命令。持久化 flag当前命令和所有子命令都可以使用。3.1 子命令 flagFlags()当前项目在 exec.go 中定义了 infovar info string func init() { rootCmd.AddCommand(execCmd) execCmd.Flags().StringVarP( info, info, i, , info 的相关信息, ) }StringVarP 的参数依次是StringVarP( p *string, name string, shorthand string, value string, usage string, )对应关系info将用户输入的值保存到 info。“info”完整名称使用 --info。“i”简写名称使用 -i。“”默认值为空字符串。最后的字符串帮助信息中的说明。因此下面两种写法等价go run . exec --info abc go run . exec -i abc在 Run 中读取变量Run: func(cmd *cobra.Command, args []string) { if info { fmt.Println(info 为空) cmd.Help() return } fmt.Println(info:, info) },执行go run . exec -i abc输出info: abc3.2 字符串 flag 必须有值因为 info 是字符串 flag下面的命令会报错go run . exec -i错误发生在 Cobra 解析 flag 的阶段Run 函数还没有执行Error: flag needs an argument: i in -i正确用法go run . exec -i abc go run . exec --info abc如果想显式传入空字符串go run . exec -i 此时才会进入if info { // ... }3.3 布尔 flag如果一个 flag 只是表示“是否开启”不需要额外的值应使用布尔 flagvar force bool func init() { execCmd.Flags().BoolVarP( force, force, f, false, 是否强制执行, ) }使用go run . exec --force go run . exec -f布尔 flag 的特点不需要写额外的值。不传入时通常是 false。传入后变为 true。3.4 全局 flagPersistentFlags()当前项目在 root.go 中定义了全局 verbosevar verbose bool func init() { rootCmd.PersistentFlags().BoolVarP( verbose, verbose, v, false, 是否显示详细信息, ) }PersistentFlags() 表示这个 flag 不只属于根命令也可以被所有子命令使用go run . -v go run . run -v go run . exec -v与之相对的是execCmd.Flags()它只会给 exec 命令增加 flaggo run . exec -i abcrun 不能使用 --infogo run . run --info abc两者的区别Flags() 只对当前命令生效 PersistentFlags() 对当前命令和所有子命令生效需要注意PersistentFlags() 只负责让子命令能够接收这个 flag不会自动改变子命令的业务逻辑。当前项目的 run.go 中主动读取了 verboseRun: func(cmd *cobra.Command, args []string) { if verbose { fmt.Println(执行成功,参数为:verbose) } else { fmt.Println(run 子命令执行成功) } },因此go run . run输出普通结果而go run . run -v会输出详细模式的结果。4. 参数校验flag 和参数不是一回事。flag 带有 - 或 – 前缀test-cli exec --info abc这里的 --info 是 flagabc 是 flag 的值。普通参数直接写在命令后面test-cli run file.txt这里的 file.txt 是普通参数不是 flag。Cobra 可以通过 Args 字段校验普通参数的数量和内容。4.1 不允许参数NoArgs如果命令不接受普通参数var runCmd cobra.Command{ Use: run, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Println(run) }, }下面的命令可以执行go run . run下面的命令会校验失败go run . run abc4.2 必须有一个参数ExactArgs如果命令必须接收一个参数var runCmd cobra.Command{ Use: run [name], Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { fmt.Println(name:, args[0]) }, }正确go run . run alice错误go run . run go run . run alice bob4.3 至少一个参数MinimumNArgs如果命令至少需要一个参数var execCmd cobra.Command{ Use: exec [command], Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { fmt.Println(要执行的内容:, args) }, }正确go run . exec ls go run . exec -- ls -l错误go run . exec常用内置校验器cobra.NoArgs cobra.ExactArgs(n) cobra.MinimumNArgs(n) cobra.MaximumNArgs(n) cobra.RangeArgs(min, max)4.4 自定义参数校验如果内置校验器不能满足需求可以自己写函数Args: func(cmd *cobra.Command, args []string) error { if len(args) ! 1 { return fmt.Errorf(必须提供一个参数) } if args[0] { return fmt.Errorf(参数不能为空) } return nil },参数校验失败时Cobra 不会执行 Run。这和字符串 flag 缺少值的情况类似参数解析或校验失败 - 输出错误 - 不执行 Run5. Hooks 生命周期钩子Hooks 用于在命令执行前后插入公共逻辑。Cobra 常用的 Hook 有PersistentPreRun PreRun Run PostRun PersistentPostRun执行顺序PersistentPreRun ↓ PreRun ↓ Run ↓ PostRun ↓ PersistentPostRun5.1 PersistentPreRunPersistentPreRun 在 Run 前执行并且可以被子命令继承。当前项目的 root.go 中PersistentPreRun: func(cmd *cobra.Command, args []string) { if verbose { fmt.Println(verbose is true,显示详细信息) } },执行go run . run -v会先执行根命令的 PersistentPreRun然后执行 run 命令的逻辑。适合放读取配置文件。初始化日志。初始化数据库连接。检查全局 flag。统一处理 --verbose。5.2 PreRunPreRun 只针对当前命令执行不会被子命令继承。当前项目的 run.go 中PreRun: func(cmd *cobra.Command, args []string) { fmt.Println(run 的 PreRun) },5.3 RunRun 是当前命令的核心业务逻辑。当前项目的 run.go 中Run: func(cmd *cobra.Command, args []string) { if verbose { fmt.Println(执行成功,参数为:verbose) } else { fmt.Println(run 子命令执行成功) } },5.4 PostRunPostRun 在当前命令的 Run 执行完成后执行不会被子命令继承。当前项目的 run.go 中PostRun: func(cmd *cobra.Command, args []string) { fmt.Println(run 的 PostRun) },适合放当前命令的收尾逻辑例如输出执行结果。记录耗时。释放当前命令创建的资源。5.5 PersistentPostRunPersistentPostRun 在 Run 和 PostRun 之后执行并且可以被子命令继承。当前项目的 root.go 中PersistentPostRun: func(cmd *cobra.Command, args []string) { fmt.Println(全局清理逻辑) },因为 run 没有自己的 PersistentPostRun所以执行go run . run默认会执行 root 的 PersistentPostRun。5.6 当前项目的完整执行顺序当前项目执行go run . run -v大致会输出verbose is true,显示详细信息 run 的 PreRun 执行成功,参数为:verbose run 的 PostRun 全局清理逻辑对应关系root PersistentPreRun ↓ run PreRun ↓ run Run ↓ run PostRun ↓ root PersistentPostRun5.7 root 和子命令都有 Persistent Hook 时听谁的默认情况下子命令优先。例如 root 和 run 都定义了 PersistentPreRunroot PersistentPreRun run PersistentPreRun执行go run . run默认只会执行run PersistentPreRun原因是子命令自己的 Hook 会覆盖父命令同名 Hook。所谓“继承”可以理解为子命令没有自己的 PersistentPreRun - 使用父命令的 子命令有自己的 PersistentPreRun - 使用子命令的PersistentPostRun 也是同样的规则默认优先使用距离当前命令最近的 Hook。如果希望父命令和子命令的持久化 Hook 都执行可以设置cobra.EnableTraverseRunHooks true开启后执行子命令时的顺序是父命令 PersistentPreRun ↓ 子命令 PersistentPreRun ↓ 子命令 Run ↓ 子命令 PersistentPostRun ↓ 父命令 PersistentPostRun5.8 使用 RunE 返回错误如果命令执行过程可能失败可以使用 RunERunE: func(cmd *cobra.Command, args []string) error { if info { return cmd.Help() } fmt.Println(info:, info) return nil },Hook 也有对应的错误版本PersistentPreRunE PreRunE RunE PostRunE PersistentPostRunE例如PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if verbose { fmt.Println(详细模式已开启) } return nil },如果返回非 nil 错误后续的命令逻辑通常不会继续执行。常用验证命令格式化 Go 代码gofmt -w ./cmd/root.go gofmt -w ./cmd/run.go gofmt -w ./cmd/exec.go运行帮助go run . --help go run . run --help go run . exec --help运行命令go run . go run . run go run . run -v go run . exec -i abc运行测试和编译go test ./... go build -o test-cli . ./test-cli --help