写 Go 的命令行工具,标准库 flag 包够用一阵子,但只要命令稍微复杂一点------有子命令、有持久 flag、要自动补全------就得换框架。
这篇文章把日常用到的部分讲清楚:命令、子命令、flag、参数校验、hook、自动补全。
装一下
sh
go get -u github.com/spf13/cobra@latest
如果想用脚手架快速起项目,可以装 cobra-cli:
sh
go install github.com/spf13/cobra-cli@latest
cobra-cli init myapp
但手写一遍更能理解结构。下面我们手动来。
最小例子
Cobra 的核心是 Command 结构体。一个命令 = 一个 Command。
go
package main
import (
"fmt"
"github.com/spf13/cobra"
)
func main() {
rootCmd := &cobra.Command{
Use: "hello",
Short: "一个最小例子",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("hello, cobra")
},
}
rootCmd.Execute()
}
跑一下:
sh
$ go run main.go
hello, cobra
$ go run main.go --help
一个最小例子
Usage:
hello [flags]
Flags:
-h, --help help for hello
--help 是自动生成的,不用自己写。
几个关键字段:
Use:命令名 + 参数签名,比如"login <username>"Short:一句话简介,出现在父命令的子命令列表里Long:完整说明,出现在自己的--help里Run:实际执行的函数
子命令
Cobra 的子命令是挂上去的。每个 Command 都可以挂子命令。
go
func main() {
rootCmd := &cobra.Command{
Use: "git",
Short: "一个伪 git 命令",
}
addCmd := &cobra.Command{
Use: "add <files>",
Short: "添加文件到暂存区",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("add:", args)
},
}
commitCmd := &cobra.Command{
Use: "commit",
Short: "提交暂存区",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("commit")
},
}
rootCmd.AddCommand(addCmd, commitCmd)
rootCmd.Execute()
}
sh
$ go run main.go add a.go b.go
add: [a.go b.go]
$ go run main.go
一个伪 git 命令
Usage:
git [command]
Available Commands:
add 添加文件到暂存区
commit 提交暂存区
help Help about any command
不传子命令时,Cobra 默认打印 help。如果想让它干点别的(比如默认进入交互),给 rootCmd 设置 Run 就行。
Flag
Cobra 的 flag 来自 pflag,和标准库 flag 的差别主要是支持长短两种写法(-n 和 --name)。
局部 flag
只在当前命令生效。
go
commitCmd := &cobra.Command{
Use: "commit",
Short: "提交暂存区",
Run: func(cmd *cobra.Command, args []string) {
msg, _ := cmd.Flags().GetString("message")
amend, _ := cmd.Flags().GetBool("amend")
fmt.Printf("commit: msg=%s amend=%v\n", msg, amend)
},
}
commitCmd.Flags().StringP("message", "m", "", "提交信息")
commitCmd.Flags().BoolP("amend", "a", false, "修改上一次提交")
sh
$ go run main.go commit -m "fix bug" -a
commit: msg=fix bug amend=true
持久 flag
PersistentFlags 会让 flag 对当前命令以及它所有的子命令都生效。这是配置项最常用的方式。
go
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "详细输出")
rootCmd.PersistentFlags().StringP("config", "c", "config.yaml", "配置文件路径")
加完之后,任何子命令里都可以读:
go
Run: func(cmd *cobra.Command, args []string) {
verbose, _ := cmd.Flags().GetBool("verbose")
if verbose {
log.Println("doing something...")
}
},
sh
$ go run main.go --verbose commit -m "x"
注意 --verbose 写在子命令前面,因为它是 root 的 flag。Cobra 在解析时会向上找 persistent flag,所以写在后面通常也行,但前面更清晰。
必填 flag
go
commitCmd.Flags().StringP("message", "m", "", "提交信息")
commitCmd.MarkFlagRequired("message")
不传就报错:
sh
$ go run main.go commit
Error: required flag(s) "message" not set
持久必填用 MarkPersistentFlagRequired。
参数校验
Args 字段控制位置参数怎么校验。Cobra 内置了几个:
| 校验器 | 含义 |
|---|---|
cobra.NoArgs |
不允许任何参数 |
cobra.ArbitraryArgs |
任意参数(默认行为) |
cobra.MinimumNArgs(n) |
至少 n 个 |
cobra.MaximumNArgs(n) |
最多 n 个 |
cobra.ExactArgs(n) |
恰好 n 个 |
cobra.RangeArgs(min, max) |
在 min, max 之间 |
cobra.OnlyValidArgs + ValidArgs |
只允许 ValidArgs 列表里的值 |
go
colorCmd := &cobra.Command{
Use: "color <name>",
Short: "选个颜色",
Args: cobra.OnlyValidArgs,
ValidArgs: []string{"red", "green", "blue"},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("color:", args[0])
},
}
sh
$ go run main.go color yellow
Error: invalid argument "yellow" for "color"
想自定义也简单,Args 是个 func(cmd *cobra.Command, args []string) error:
go
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("需要一个参数")
}
if !strings.HasPrefix(args[0], "user:") {
return errors.New("参数必须以 user: 开头")
}
return nil
},
Hook:在 Run 前后做事
Cobra 提供了一组 hook,按顺序执行:
PersistentPreRun → PreRun → Run → PostRun → PersistentPostRun
Persistent* 版本会对子命令也生效。常见用法:
PersistentPreRun:读配置文件、初始化日志、建立数据库连接PreRun:当前命令的预处理PostRun:清理、打点PersistentPostRun:通用收尾
go
rootCmd := &cobra.Command{
Use: "myapp",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// 初始化日志
log.SetFlags(log.LstdFlags | log.Lshortfile)
},
}
runCmd := &cobra.Command{
Use: "run",
PreRun: func(cmd *cobra.Command, args []string) {
log.Println("准备启动")
},
Run: func(cmd *cobra.Command, args []string) {
log.Println("运行中")
},
PostRun: func(cmd *cobra.Command, args []string) {
log.Println("运行结束")
},
}
注意:如果子命令定义了自己的 PersistentPreRun,会覆盖父命令的,不会叠加。需要叠加的话自己显式调用父命令的 hook。
配合 viper 读配置
Cobra 和 viper 是一套组合拳,同一个作者。viper 读配置文件、环境变量、命令行 flag,并能把它们绑定到一起。
go
var cfgFile string
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径")
rootCmd.PersistentFlags().String("db-url", "", "数据库连接")
viper.BindPFlag("db.url", rootCmd.PersistentFlags().Lookup("db-url"))
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
viper.ReadInConfig()
}
// 命令行没传 --db-url 时,自动回退到配置文件里的 db.url
}
flag → 配置文件 → 默认值,优先级从前到后,调用者不用关心值从哪来。
自动补全
Cobra 内置 shell 补全,几行就生成:
go
rootCmd := &cobra.Command{
Use: "myapp",
// ...
}
rootCmd.AddCommand(&cobra.Command{
Use: "completion [shell]",
Short: "生成 shell 补全脚本",
Args: cobra.ExactValidArgs(1),
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
rootCmd.GenBashCompletion(os.Stdout)
case "zsh":
rootCmd.GenZshCompletion(os.Stdout)
case "fish":
rootCmd.GenFishCompletion(os.Stdout, true)
}
},
})
或者直接:
sh
$ myapp completion zsh > /usr/local/share/zsh/site-functions/_myapp
更猛的是动态补全------补全的候选项可以在运行时算。给 flag 注册 RegisterFlagCompletionFunc,给参数注册 ValidArgsFunction:
go
userCmd.Flags().String("user", "", "用户名")
userCmd.RegisterFlagCompletionFunc("user", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// 从数据库查用户列表
return []string{"alice", "bob", "charlie"}, cobra.ShellCompDirectiveNoFileComp
})
按 tab 时 Cobra 会调用这个函数,把结果作为补全候选项。
推荐的项目结构
工具稍微大一点就建议拆文件。一个常见布局:
csharp
myapp/
├── cmd/
│ ├── root.go # rootCmd 定义 + Execute()
│ ├── serve.go # serve 子命令
│ ├── login.go # login 子命令
│ └── user/
│ ├── list.go # user list
│ └── create.go # user create
├── internal/
│ └── ... # 业务逻辑
├── go.mod
└── main.go
main.go 只剩一行:
go
package main
import "myapp/cmd"
func main() {
cmd.Execute()
}
cmd/root.go:
go
package cmd
import "github.com/spf13/cobra"
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "我的应用",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "详细输出")
}
每个子命令一个文件,用 init() 把自己挂到 rootCmd 上:
go
// cmd/serve.go
package cmd
import "github.com/spf13/cobra"
var serveCmd = &cobra.Command{
Use: "serve",
Short: "启动服务",
Run: func(cmd *cobra.Command, args []string) {
// ...
},
}
func init() {
rootCmd.AddCommand(serveCmd)
}
init() 是 cobra-cli 默认的写法。命令多了之后如果你不喜欢隐式的 init,也可以在 Execute() 里显式 rootCmd.AddCommand(...),更直观。
一些容易踩的坑
Run 和 RunE 的取舍。 Run 没法返回 error,错误只能 os.Exit 或 panic。生产代码用 RunE:
go
RunE: func(cmd *cobra.Command, args []string) error {
if err := doStuff(); err != nil {
return err
}
return nil
},
返回的 error 会由 Cobra 统一打印 + 退出,错误信息也更一致。
flag 默认值和"未设置"无法区分。 String flag 默认是空字符串,调用者没传和传了空串分不开。需要区分时,用 *string 指针版本:Flags().StringVarP(&p, "name", "n", nil, "..."),没传时 p 是 nil。
**子命令的 Args 校验在 flag 解析之后。**所以校验函数里可以读 flag 的值。
**--help 不会触发 Run。**测试时如果发现 Run 没执行,先看看是不是不小心传了 -h。
完整示例
把上面的东西串起来:一个带子命令、持久 flag、参数校验、RunE 的最小可运行项目。
go
package main
import (
"errors"
"fmt"
"log"
"os"
"github.com/spf13/cobra"
)
var verbose bool
func main() {
rootCmd := &cobra.Command{
Use: "myapp",
Short: "一个完整的 cobra 示例",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if verbose {
log.SetFlags(log.LstdFlags | log.Lshortfile)
} else {
log.SetFlags(0)
}
},
}
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "详细输出")
greetCmd := &cobra.Command{
Use: "greet <name>",
Short: "和某人打招呼",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("需要一个名字")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
times, _ := cmd.Flags().GetInt("times")
for i := 0; i < times; i++ {
fmt.Printf("hello, %s\n", args[0])
}
if verbose {
log.Printf("打印了 %d 次", times)
}
return nil
},
}
greetCmd.Flags().IntP("times", "t", 1, "重复次数")
rootCmd.AddCommand(greetCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
sh
$ go run main.go greet Alice -t 2
hello, Alice
hello, Alice
$ go run main.go -v greet Alice -t 2
hello, Alice
hello, Alice
2026/06/29 main.go:48: 打印了 2 次
$ go run main.go greet
Error: 需要一个名字