构建现代 CLI 工具:cobra 框架完全指南

全面介绍使用 cobra 框架构建现代命令行工具,包括命令结构、标志参数、子命令、viper 配置集成、Shell 自动补全、彩色输出和交互式提示

构建现代 CLI 工具:cobra 框架完全指南

你有没有用过 kubectlhugodocker 这些命令行工具?它们的用户体验堪称一流:清晰的帮助信息、自动补全、子命令嵌套、丰富的标志参数……每次使用都感觉丝滑得不可思议。你有没有想过,这些工具是怎么做到的?

答案就藏在 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 commitgit push)、自动生成帮助文档能力弱、没有 Shell 自动补全、代码组织不够清晰。

而 Cobra 提供的是一套完整的 CLI 框架:

  • 命令和子命令的层次结构app subcmd --flag value
  • POSIX 兼容的标志:支持短标志 -v 和长标志 --verbose
  • 自动帮助生成app helpapp --helpapp -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 自动生成了漂亮的帮助信息,连 completionhelp 命令都内置好了。

添加子命令

现在,让我们添加一些实际的子命令。假设我们要构建一个类似 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 podskubectl 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 的配置优先级从高到低是:

  1. 命令行标志 --verbose
  2. 环境变量 MYCLI_VERBOSE=true
  3. 配置文件 .mycli.yaml 中的 verbose: true
  4. 默认值 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 之旅!

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页