构建现代 CLI 工具:cobra 框架完全指南
你有没有用过 kubectl、hugo、docker 这些命令行工具?它们的用户体验堪称一流:清晰的帮助信息、自动补全、子命令嵌套、丰富的标志参数……每次使用都感觉丝滑得不可思议。你有没有想过,这些工具是怎么做到的?
答案就藏在 Go 语言生态中最闪耀的 CLI 框架——Cobra。它由 Spencer Kimball 和 Steve Francia 创造,如今已成为 Go 社区构建命令行工具的事实标准。今天,我们从零开始,手把手打造一个功能完备的现代 CLI 工具。
为什么选择 Cobra?
在开始写代码之前,我们先回答一个根本问题:为什么不用标准库的 flag 包,而非要用 Cobra?
看看这个对比就明白了。用标准库 flag 写出来的 CLI 工具长这样:
// 使用标准库 flag 的方式
package main
import (
"flag"
"fmt"
)
func main() {
name := flag.String("name", "", "your name")
age := flag.Int("age", 0, "your age")
flag.Parse()
fmt.Printf("Hello %s, age %d\n", *name, *age)
}
这段代码能工作,但它有几个致命的缺陷:不支持子命令(比如 git commit、git push)、自动生成帮助文档能力弱、没有 Shell 自动补全、代码组织不够清晰。
而 Cobra 提供的是一套完整的 CLI 框架:
- 命令和子命令的层次结构:
app subcmd --flag value - POSIX 兼容的标志:支持短标志
-v和长标志--verbose - 自动帮助生成:
app help、app --help、app -h - Shell 自动补全:Bash、Zsh、Fish、PowerShell
- 配置管理:与 Viper 无缝集成
- 脚手架工具:
cobra-cli一键生成命令代码
安装与项目初始化
先安装 Cobra 和它的脚手架工具:
# 安装 cobra 库
go get -u github.com/spf13/cobra@latest
# 安装 cobra-cli 脚手架(可选但强烈推荐)
go install github.com/spf13/cobra-cli@latest
创建项目目录并初始化:
mkdir mycli && cd mycli
go mod init github.com/yourname/mycli
从零开始:第一个 Cobra 命令
最基本的命令结构
Cobra 的核心思想是:一切皆命令。根命令(root command)是所有子命令的父节点,形成一棵命令树。
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// rootCmd 根命令
var rootCmd = &cobra.Command{
Use: "mycli",
Short: "mycli 是一个现代 CLI 工具示例",
Long: `mycli 是一个功能丰富的命令行工具示例,
用来演示 Cobra 框架的各种特性。
它可以完成文件管理、系统监控、代码生成等任务。`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Welcome to mycli! Use --help to see available commands.")
},
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
运行看看效果:
$ go run main.go
Welcome to mycli! Use --help to see available commands.
$ go run main.go --help
mycli 是一个现代 CLI 工具示例
mycli 是一个功能丰富的命令行工具示例,
用来演示 Cobra 框架的各种特性。
它可以完成文件管理、系统监控、代码生成等任务。
Usage:
mycli [flags]
mycli [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
Flags:
-h, --help help for mycli
Use "mycli [command] --help" for more information about a command.
看到了吗?Cobra 自动生成了漂亮的帮助信息,连 completion 和 help 命令都内置好了。
添加子命令
现在,让我们添加一些实际的子命令。假设我们要构建一个类似 kubectl 的开发运维工具:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// versionCmd 版本信息命令
var versionCmd = &cobra.Command{
Use: "version",
Short: "显示版本信息",
Long: "显示 mycli 的当前版本号和构建信息",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("mycli version 1.0.0")
fmt.Println("Build date: 2024-02-28")
fmt.Println("Go version: go1.22")
},
}
func init() {
// 在 init 中将子命令注册到根命令
rootCmd.AddCommand(versionCmd)
}
使用 cobra-cli 快速生成命令
手写 init() 函数太累了。用 cobra-cli 可以一键生成:
$ cobra-cli add version
$ cobra-cli add deploy
$ cobra-cli add config
它会自动在项目中创建对应的 .go 文件,并把 AddCommand 的注册代码都写好。生成的目录结构类似:
mycli/
├── cmd/
│ ├── root.go
│ ├── version.go
│ ├── deploy.go
│ └── config.go
├── main.go
├── go.mod
└── go.sum
标志与参数:让命令灵活起来
CLI 工具的精髓在于标志(flags)和参数(arguments)。Cobra 提供了两套标志系统:持久标志和本地标志。
持久标志 vs 本地标志
func init() {
// 持久标志:对所有子命令生效
rootCmd.PersistentFlags().StringVarP(
&cfgFile, "config", "c", "", "配置文件路径(默认 $HOME/.mycli.yaml)")
rootCmd.PersistentFlags().BoolVarP(
&verbose, "verbose", "v", false, "启用详细输出")
// 本地标志:只对当前命令生效
deployCmd.Flags().StringVarP(
&environment, "env", "e", "dev", "部署环境(dev/staging/prod)")
deployCmd.Flags().IntVarP(
&replicas, "replicas", "r", 1, "副本数量")
}
使用时的体验是这样的:
# 持久标志可以放在任意位置
$ mycli --verbose deploy --env prod --replicas 3
$ mycli deploy --verbose --env prod --replicas 3
# 短标志组合使用
$ mycli -v deploy -e prod -r 3
自定义参数验证
Cobra 支持对参数进行严格验证,保证用户输入合法:
var deployCmd = &cobra.Command{
Use: "deploy [service-name]",
Short: "部署服务到指定环境",
Long: "将指定的服务部署到目标环境,支持 dev、staging、prod 三种环境",
Args: cobra.ExactArgs(1), // 严格要求恰好 1 个参数
RunE: runDeploy, // 使用 RunE 可以返回错误
}
func runDeploy(cmd *cobra.Command, args []string) error {
serviceName := args[0]
env, _ := cmd.Flags().GetString("env")
replicas, _ := cmd.Flags().GetInt("replicas")
// 验证环境参数
validEnvs := map[string]bool{"dev": true, "staging": true, "prod": true}
if !validEnvs[env] {
return fmt.Errorf("invalid environment %q: must be dev, staging, or prod", env)
}
fmt.Printf("Deploying %s to %s with %d replicas...\n", serviceName, env, replicas)
return nil
}
Cobra 内置了多种验证器:
cobra.NoArgs // 不接受任何参数
cobra.ArbitraryArgs // 接受任意数量参数
cobra.MinimumNArgs(n) // 至少 n 个参数
cobra.MaximumNArgs(n) // 最多 n 个参数
cobra.ExactArgs(n) // 恰好 n 个参数
cobra.OnlyValidArgs // 只接受 ValidArgs 中列出的参数
你也可以写自定义验证函数:
var scaleCmd = &cobra.Command{
Use: "scale [replicas]",
Short: "调整服务副本数",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("requires exactly 1 argument: replicas count")
}
n, err := strconv.Atoi(args[0])
if err != nil || n < 1 || n > 100 {
return fmt.Errorf("replicas must be a number between 1 and 100")
}
return nil
},
RunE: runScale,
}
嵌套子命令:构建 kubectl 风格的命令树
kubectl 之所以好用,很大程度上归功于它清晰的命令层次结构:kubectl get pods、kubectl describe service nginx。Cobra 天然支持这种嵌套。
实现 config 命令组
// cmd/config.go
package cmd
import "github.com/spf13/cobra"
// configCmd 配置管理命令组(本身不执行任何操作)
var configCmd = &cobra.Command{
Use: "config",
Short: "管理 mycli 配置",
Long: "查看和修改 mycli 的全局配置项",
}
func init() {
rootCmd.AddCommand(configCmd)
}
// cmd/config_set.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var configSetCmd = &cobra.Command{
Use: "set [key] [value]",
Short: "设置配置项",
Long: "设置一个全局配置项的值",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
key, value := args[0], args[1]
fmt.Printf("Setting %s = %s\n", key, value)
// 实际保存到配置文件中
return setConfig(key, value)
},
}
func init() {
configCmd.AddCommand(configSetCmd)
}
// cmd/config_get.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var configGetCmd = &cobra.Command{
Use: "get [key]",
Short: "获取配置项",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
key := args[0]
value, err := getConfig(key)
if err != nil {
return err
}
fmt.Println(value)
return nil
},
}
func init() {
configCmd.AddCommand(configGetCmd)
}
// cmd/config_list.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var configListCmd = &cobra.Command{
Use: "list",
Short: "列出所有配置项",
Aliases: []string{"ls"}, // 设置别名
RunE: func(cmd *cobra.Command, args []string) error {
configs, err := listConfigs()
if err != nil {
return err
}
for k, v := range configs {
fmt.Printf("%s = %s\n", k, v)
}
return nil
},
}
func init() {
configCmd.AddCommand(configListCmd)
}
现在用户可以这样使用:
$ mycli config set editor vim
$ mycli config get editor
vim
$ mycli config list
editor = vim
theme = dark
output = json
$ mycli config ls # 别名也能用
PreRun 和 PostRun 钩子
Cobra 提供了强大的生命周期钩子,让你在命令执行前后做预处理和清理工作:
var deployCmd = &cobra.Command{
Use: "deploy [service]",
Short: "部署服务",
// 在所有标志解析完成后、Run 之前执行
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// 适用于所有子命令的预处理
if verbose {
log.SetLevel(log.DebugLevel)
}
},
// Run 之前执行(仅当前命令)
PreRunE: func(cmd *cobra.Command, args []string) error {
// 检查前置条件:用户是否已登录
if !isLoggedIn() {
return fmt.Errorf("please run 'mycli login' first")
}
// 检查配置文件是否存在
if !configExists() {
return fmt.Errorf("config file not found, run 'mycli init' first")
}
return nil
},
RunE: runDeploy,
// Run 之后执行(无论成功失败)
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("Deployment completed. Cleaning up temporary files...")
cleanup()
},
}
Viper 集成:配置管理一把抓
Cobra 的好搭档是 Viper——一个功能强大的 Go 配置库。它们两个配合起来,可以实现环境变量、配置文件、命令行标志三级覆盖。
安装和基本集成
go get -u github.com/spf13/viper
// cmd/root.go
package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
verbose bool
)
var rootCmd = &cobra.Command{
Use: "mycli",
Short: "现代 CLI 工具示例",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
initConfig()
},
}
func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "详细输出")
// 将 flag 绑定到 viper
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
}
func initConfig() {
if cfgFile != "" {
// 使用指定的配置文件
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// 搜索配置文件的路径
viper.AddConfigPath(home)
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName(".mycli")
}
// 自动读取环境变量(MYCLI_ 前缀)
viper.SetEnvPrefix("MYCLI")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
if verbose {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
}
三级配置覆盖
Viper 的配置优先级从高到低是:
- 命令行标志
--verbose - 环境变量
MYCLI_VERBOSE=true - 配置文件
.mycli.yaml中的verbose: true - 默认值
false
# ~/.mycli.yaml
verbose: false
output: table
server:
host: localhost
port: 8080
database:
driver: postgres
dsn: "postgres://user:pass@localhost:5432/mydb"
// 在任何地方读取配置
func getServerConfig() ServerConfig {
return ServerConfig{
Host: viper.GetString("server.host"), // 默认 "localhost"
Port: viper.GetInt("server.port"), // 默认 8080
}
}
Shell 自动补全
这是让 CLI 工具从"能用"变成"好用"的关键一步。Cobra 内置了对 Bash、Zsh、Fish、PowerShell 的补全支持。
生成补全脚本
# Bash
$ mycli completion bash > /etc/bash_completion.d/mycli
# Zsh
$ mycli completion zsh > "${fpath[1]}/_mycli"
# Fish
$ mycli completion fish > ~/.config/fish/completions/mycli.fish
# PowerShell
$ mycli completion powershell | Out-String | Invoke-Expression
自定义补全提示
默认的补全只会提示命令名。你可以为参数提供更智能的补全:
var deployCmd = &cobra.Command{
Use: "deploy [service]",
Short: "部署服务",
ValidArgs: []string{"api", "web", "worker", "scheduler"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: runDeploy,
}
// 或者用动态补全函数
func init() {
deployCmd.RegisterFlagCompletionFunc("env", func(
cmd *cobra.Command, args []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
return []string{"dev", "staging", "prod"}, cobra.ShellCompDirectiveNoFileComp
})
// 文件名补全
deployCmd.RegisterFlagCompletionFunc("config", func(
cmd *cobra.Command, args []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
return []string{"yaml", "json", "toml"}, cobra.ShellCompDirectiveFilterFileExt
})
}
彩色输出:让终端亮起来
CLI 工具不能只有黑白两色。合理使用颜色可以大幅提升可读性。github.com/fatih/color 是最流行的 Go 彩色输出库。
go get -u github.com/fatih/color
package cmd
import (
"fmt"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var statusCmd = &cobra.Command{
Use: "status",
Short: "显示服务状态",
Run: func(cmd *cobra.Command, args []string) {
// 定义颜色
green := color.New(color.FgGreen).SprintFunc()
red := color.New(color.FgRed).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
bold := color.New(color.Bold).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
fmt.Println(bold("=== Service Status ==="))
fmt.Println()
services := []struct {
Name string
Status string
}{
{"api-gateway", "running"},
{"user-service", "running"},
{"order-service", "degraded"},
{"payment-service", "stopped"},
}
for _, svc := range services {
var statusStr string
switch svc.Status {
case "running":
statusStr = green("● RUNNING")
case "degraded":
statusStr = yellow("● DEGRADED")
case "stopped":
statusStr = red("● STOPPED")
}
fmt.Printf(" %-25s %s\n", cyan(svc.Name), statusStr)
}
fmt.Println()
},
}
输出效果:
=== Service Status ===
api-gateway ● RUNNING
user-service ● RUNNING
order-service ● DEGRADED
payment-service ● STOPPED
表格输出
用 github.com/olekukonern/tablewriter 可以输出对齐的表格:
package cmd
import (
"os"
"github.com/spf13/cobra"
"github.com/olekukonko/tablewriter"
)
var listCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "列出所有服务",
Run: func(cmd *cobra.Command, args []string) {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"NAME", "VERSION", "STATUS", "REPLICAS", "UPTIME"})
table.SetBorder(false)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
data := [][]string{
{"api-gateway", "v2.1.0", "Running", "3/3", "5d 12h"},
{"user-service", "v1.8.2", "Running", "2/2", "3d 8h"},
{"order-service", "v3.0.1", "Degraded", "1/3", "1h 23m"},
{"payment-service", "v1.2.0", "Stopped", "0/2", "N/A"},
}
for _, row := range data {
table.Append(row)
}
table.Render()
},
}
进度条与 Spinner
当命令执行时间较长时,给用户一个进度反馈非常重要。github.com/schollz/progressbar/v3 是一个优秀的进度条库。
package cmd
import (
"fmt"
"time"
"github.com/schollz/progressbar/v3"
"github.com/spf13/cobra"
)
var downloadCmd = &cobra.Command{
Use: "download [url]",
Short: "下载文件",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
url := args[0]
totalBytes := 1024 * 1024 * 50 // 模拟 50MB
bar := progressbar.NewOptions(totalBytes,
progressbar.OptionSetDescription("Downloading"),
progressbar.OptionSetWriter(os.Stdout),
progressbar.OptionShowBytes(true),
progressbar.OptionSetWidth(30),
progressbar.OptionThrottle(65 * time.Millisecond),
progressbar.OptionShowCount(),
progressbar.OptionOnCompletion(func() {
fmt.Fprint(os.Stdout, "\n")
}),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
)
// 模拟下载过程
chunkSize := 1024 * 64 // 64KB per chunk
for i := 0; i < totalBytes; i += chunkSize {
bar.Add(chunkSize)
time.Sleep(10 * time.Millisecond) // 模拟网络延迟
}
fmt.Printf("\n✅ Downloaded %s successfully!\n", url)
return nil
},
}
对于不确定进度的操作,使用 Spinner:
package cmd
import (
"fmt"
"time"
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
)
var buildCmd = &cobra.Command{
Use: "build",
Short: "构建项目",
RunE: func(cmd *cobra.Command, args []string) error {
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
// Step 1: 编译
s.Suffix = " Compiling source files..."
s.Start()
time.Sleep(2 * time.Second) // 模拟编译
s.Stop()
fmt.Println("✅ Compilation successful")
// Step 2: 运行测试
s.Suffix = " Running tests..."
s.Start()
time.Sleep(1 * time.Second)
s.Stop()
fmt.Println("✅ All 42 tests passed")
// Step 3: 打包
s.Suffix = " Building Docker image..."
s.Start()
time.Sleep(3 * time.Second)
s.Stop()
fmt.Println("✅ Docker image built: myapp:latest")
fmt.Println("\n🎉 Build completed successfully!")
return nil
},
}
交互式提示
有时候你需要在命令行中向用户提问。github.com/AlecAivazis/survey/v2 提供了丰富的交互式输入方式。
package cmd
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
)
var initCmd = &cobra.Command{
Use: "init",
Short: "初始化新项目",
RunE: func(cmd *cobra.Command, args []string) error {
answers := struct {
ProjectName string
Framework string
Database string
Features []string
License string
}{}
questions := []*survey.Question{
{
Name: "ProjectName",
Prompt: &survey.Input{
Message: "项目名称:",
Default: "my-project",
},
Validate: survey.Required,
},
{
Name: "Framework",
Prompt: &survey.Select{
Message: "选择 Web 框架:",
Options: []string{"Gin", "Echo", "Fiber", "Chi", "Standard library"},
Default: "Gin",
},
},
{
Name: "Database",
Prompt: &survey.Select{
Message: "选择数据库:",
Options: []string{"PostgreSQL", "MySQL", "SQLite", "MongoDB", "None"},
},
},
{
Name: "Features",
Prompt: &survey.MultiSelect{
Message: "选择要启用的功能:",
Options: []string{
"JWT Authentication",
"Rate Limiting",
"CORS",
"Request Logging",
"Swagger Docs",
"Graceful Shutdown",
},
Default: []string{"CORS", "Request Logging"},
},
},
{
Name: "License",
Prompt: &survey.Select{
Message: "选择许可证:",
Options: []string{"MIT", "Apache 2.0", "GPL 3.0", "BSD 3-Clause", "None"},
},
},
}
if err := survey.Ask(questions, &answers); err != nil {
return err
}
fmt.Printf("\n📦 Initializing project: %s\n", answers.ProjectName)
fmt.Printf(" Framework: %s\n", answers.Framework)
fmt.Printf(" Database: %s\n", answers.Database)
fmt.Printf(" Features: %v\n", answers.Features)
fmt.Printf(" License: %s\n", answers.License)
// 确认
confirm := false
prompt := &survey.Confirm{
Message: "确认创建项目?",
Default: true,
}
survey.AskOne(prompt, &confirm)
if confirm {
fmt.Println("\n🚀 Project created successfully!")
} else {
fmt.Println("\n❌ Project creation cancelled.")
}
return nil
},
}
实战:构建一个完整的 CLI 工具
让我们把所有学到的知识整合起来,构建一个名为 devctl 的开发者工具。
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
noColor bool
verbose bool
)
var rootCmd = &cobra.Command{
Use: "devctl",
Short: "devctl - 开发者的瑞士军刀",
Long: `
____ _ _
| _ \ _____ _| |_| |
| | | |/ _ \ \ / / __| |
| |_| | __/\ V /| |_| |
|____/ \___| \_/ \__|_|
devctl 是一个多功能的开发者工具,
集成了项目管理、代码生成、环境配置等常用功能。`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if noColor {
color.NoColor = true
}
if verbose {
fmt.Fprintln(os.Stderr, color.CyanString("[DEBUG] verbose mode enabled"))
}
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径")
rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "禁用彩色输出")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "详细输出")
// 注册所有子命令
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(newCmd)
rootCmd.AddCommand(generateCmd)
rootCmd.AddCommand(doctorCmd)
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, _ := os.UserHomeDir()
viper.AddConfigPath(home)
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName(".devctl")
}
viper.AutomaticEnv()
viper.ReadInConfig()
}
// cmd/new.go
package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/fatih/color"
"github.com/schollz/progressbar/v3"
"github.com/spf13/cobra"
)
var newCmd = &cobra.Command{
Use: "new [project-name]",
Short: "创建新项目",
Long: "使用模板创建一个新的 Go 项目,包含完整的目录结构和配置",
Args: cobra.ExactArgs(1),
RunE: runNew,
}
var (
template string
module string
)
func init() {
newCmd.Flags().StringVarP(&template, "template", "t", "api", "项目模板 (api/cli/worker)")
newCmd.Flags().StringVarP(&module, "module", "m", "", "Go module 路径")
}
func runNew(cmd *cobra.Command, args []string) error {
projectName := args[0]
if module == "" {
module = "github.com/user/" + projectName
}
// 显示创建计划
bold := color.New(color.Bold)
bold.Println("\n📋 Project Plan:")
fmt.Printf(" Name: %s\n", projectName)
fmt.Printf(" Module: %s\n", module)
fmt.Printf(" Template: %s\n", template)
fmt.Println()
// 创建目录结构
dirs := getTemplateDirs(template)
bar := progressbar.NewOptions(len(dirs),
progressbar.OptionSetDescription("Creating directories"),
progressbar.OptionSetWidth(30),
)
for _, dir := range dirs {
path := filepath.Join(projectName, dir)
os.MkdirAll(path, 0755)
bar.Add(1)
}
fmt.Println()
// 生成文件
files := getTemplateFiles(template)
bar2 := progressbar.NewOptions(len(files),
progressbar.OptionSetDescription("Generating files"),
progressbar.OptionSetWidth(30),
)
for _, f := range files {
path := filepath.Join(projectName, f.Name)
os.WriteFile(path, []byte(f.Content), 0644)
bar2.Add(1)
}
fmt.Println()
color.Green("\n✅ Project %s created successfully!", projectName)
fmt.Printf("\nNext steps:\n")
fmt.Printf(" cd %s\n", projectName)
fmt.Printf(" go mod tidy\n")
fmt.Printf(" make run\n")
return nil
}
type templateFile struct {
Name string
Content string
}
func getTemplateDirs(tmpl string) []string {
base := []string{
"cmd", "internal", "pkg", "configs", "scripts", "docs", "test",
}
switch tmpl {
case "api":
return append(base,
"internal/handler",
"internal/service",
"internal/repository",
"internal/middleware",
"internal/model",
"api/proto",
)
case "cli":
return append(base, "cmd/root", "cmd/version")
case "worker":
return append(base, "internal/job", "internal/queue")
default:
return base
}
}
func getTemplateFiles(tmpl string) []templateFile {
return []templateFile{
{Name: "go.mod", Content: fmt.Sprintf("module %s\n\ngo 1.22\n", module)},
{Name: "Makefile", Content: makefileContent()},
{Name: ".gitignore", Content: gitignoreContent()},
{Name: "README.md", Content: "# " + projectName + "\n"},
}
}
// cmd/doctor.go
package cmd
import (
"fmt"
"os/exec"
"runtime"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var doctorCmd = &cobra.Command{
Use: "doctor",
Short: "检查开发环境是否就绪",
Long: "检查 Go、Git、Docker 等开发工具是否已安装并配置正确",
Run: func(cmd *cobra.Command, args []string) {
green := color.New(color.FgGreen).SprintFunc()
red := color.New(color.FgRed).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
bold := color.New(color.Bold).SprintFunc()
fmt.Println(bold("\n🔍 Development Environment Check"))
fmt.Println(strings.Repeat("─", 40))
checks := []struct {
Name string
Command string
Args []string
}{
{"Go", "go", []string{"version"}},
{"Git", "git", []string{"--version"}},
{"Docker", "docker", []string{"--version"}},
{"Make", "make", []string{"--version"}},
{"protoc", "protoc", []string{"--version"}},
}
passed := 0
for _, check := range checks {
out, err := exec.Command(check.Command, check.Args...).CombinedOutput()
if err != nil {
fmt.Printf(" %s %-12s not found\n", red("✗"), check.Name)
} else {
version := strings.TrimSpace(strings.Split(string(out), "\n")[0])
fmt.Printf(" %s %-12s %s\n", green("✓"), check.Name, version)
passed++
}
}
// 检查 GOPATH 和 GOROOT
fmt.Println()
fmt.Printf(" OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)
fmt.Printf(" Go Root: %s\n", runtime.GOROOT())
fmt.Println()
if passed == len(checks) {
color.Green(" ✅ All %d checks passed! You're ready to code.\n", passed)
} else {
color.Yellow(" ⚠️ %d/%d checks passed. Some tools are missing.\n", passed, len(checks))
}
},
}
最佳实践总结
经过这么多代码示例,让我们来总结一些 Cobra CLI 开发的最佳实践:
1. 代码组织
mycli/
├── cmd/ # 所有命令定义
│ ├── root.go # 根命令 + 初始化
│ ├── deploy.go # 业务命令
│ └── ...
├── internal/ # 私有业务逻辑
│ ├── deployer/
│ └── builder/
├── pkg/ # 可复用的公共库
├── main.go # 只做一件事:调用 cmd.Execute()
└── Makefile
2. 错误处理
永远使用 RunE 而非 Run,让错误能被上层捕获和处理:
// ❌ 不好
Run: func(cmd *cobra.Command, args []string) {
if err := doSomething(); err != nil {
fmt.Println(err) // 错误被吞掉了
}
}
// ✅ 好
RunE: func(cmd *cobra.Command, args []string) error {
return doSomething()
}
3. 标志命名规范
// ✅ 长标志用全小写 + 短横线
cmd.Flags().StringVar(&outDir, "output-dir", "./out", "")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "")
// ❌ 不要用驼峰或下划线
cmd.Flags().StringVar(&outDir, "outputDir", "./out", "")
cmd.Flags().StringVar(&outDir, "output_dir", "./out", "")
4. 提供友好的退出码
const (
ExitCodeSuccess = 0
ExitCodeGeneralErr = 1
ExitCodeUsageErr = 2
ExitCodeNetworkErr = 3
)
func main() {
if err := rootCmd.Execute(); err != nil {
var exitErr *ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.Code)
}
os.Exit(ExitCodeGeneralErr)
}
}
总结
今天我们完整学习了使用 Cobra 框架构建现代 CLI 工具的方方面面:
- 命令结构:根命令、子命令、嵌套子命令,形成清晰的命令树
- 标志参数:持久标志、本地标志、短标志、参数验证
- Viper 集成:命令行标志、环境变量、配置文件的三级覆盖
- Shell 补全:Bash、Zsh、Fish、PowerShell 的自动补全支持
- 用户体验:彩色输出、表格展示、进度条、Spinner、交互式提示
- 实战项目:构建了一个功能完整的
devctl开发工具 - 最佳实践:代码组织、错误处理、标志命名、退出码
现在你可以信心十足地开始构建自己的 CLI 工具了。无论是团队内部的效率工具,还是面向开源社区的开发者工具,Cobra 都能帮你打造出用户体验一流的产品。
快去试试吧,cobra-cli add 一下,开启你的 CLI 之旅!
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。