golang-cli-cobra-viper
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo CLI Development with Cobra & Viper
基于Cobra & Viper的Go CLI开发
Overview
概述
Cobra and Viper are the industry-standard libraries for building production-quality CLIs in Go. Cobra provides command structure and argument parsing, while Viper manages configuration from multiple sources with clear precedence rules.
Key Features:
- 🎯 Cobra Commands: POSIX-compliant CLI with subcommands ()
app verb noun --flag - ⚙️ Viper Config: Unified configuration from flags, env vars, and config files
- 🔄 Integration: Seamless Cobra + Viper plumbing patterns
- 🐚 Shell Completion: Auto-generated completions for bash, zsh, fish, PowerShell
- ✅ Production Ready: Battle-tested by kubectl, docker, gh, hugo
Used By: Kubernetes (kubectl), Docker CLI, GitHub CLI (gh), Hugo, Helm, and 100+ major projects
Cobra和Viper是Go语言中构建生产级CLI的行业标准库。Cobra提供命令结构和参数解析功能,而Viper则通过清晰的优先级规则管理来自多源的配置。
核心特性:
- 🎯 Cobra命令:符合POSIX标准的CLI,支持子命令()
app verb noun --flag - ⚙️ Viper配置:统一管理来自命令行标志、环境变量和配置文件的配置
- 🔄 集成性:Cobra + Viper的无缝集成模式
- 🐚 Shell补全:自动生成bash、zsh、fish、PowerShell的补全脚本
- ✅ 生产就绪:经过kubectl、docker、gh、hugo等工具的实战检验
使用案例:Kubernetes(kubectl)、Docker CLI、GitHub CLI(gh)、Hugo、Helm等100+主流项目
When to Use This Skill
何时使用该技能
Activate this skill when:
- Building multi-command CLI tools with subcommands
- Creating developer tools, project generators, or scaffolding utilities
- Implementing admin CLIs for services or infrastructure
- Requiring flexible configuration (flags > env vars > config files > defaults)
- Adding shell completion for frequently-used CLIs
- Building DevOps automation tools or deployment scripts
在以下场景中启用该技能:
- 构建支持子命令的多命令CLI工具
- 创建开发者工具、项目生成器或脚手架工具
- 为服务或基础设施实现管理CLI
- 需要灵活的配置(标志 > 环境变量 > 配置文件 > 默认值)
- 为高频使用的CLI添加Shell补全功能
- 构建DevOps自动化工具或部署脚本
Cobra Framework
Cobra框架
Command Structure Pattern
命令结构模式
Cobra follows the pattern popularized by git and kubectl.
APPNAME VERB NOUN --FLAGgo
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A powerful CLI tool for developers",
Long: `MyApp is a CLI tool that demonstrates best practices
for building production-quality command-line applications.
Complete documentation is available at https://myapp.example.com`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
// Persistent flags (available to all subcommands)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.myapp.yaml)")
rootCmd.PersistentFlags().Bool("verbose", false, "verbose output")
// Bind persistent flags to viper
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
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(".myapp")
}
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
if viper.GetBool("verbose") {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
}Cobra遵循由git和kubectl普及的模式。
APPNAME VERB NOUN --FLAGgo
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A powerful CLI tool for developers",
Long: `MyApp is a CLI tool that demonstrates best practices
for building production-quality command-line applications.
Complete documentation is available at https://myapp.example.com`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
// Persistent flags (available to all subcommands)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.myapp.yaml)")
rootCmd.PersistentFlags().Bool("verbose", false, "verbose output")
// Bind persistent flags to viper
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
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(".myapp")
}
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
if viper.GetBool("verbose") {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
}Subcommands with Arguments
带参数的子命令
go
// cmd/deploy.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy application to specified environment",
Long: `Deploy the application to the specified environment.
Supports: dev, staging, production`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{"dev", "staging", "production"},
PreRunE: func(cmd *cobra.Command, args []string) error {
// Validation logic runs before RunE
env := args[0]
if env == "production" && !viper.GetBool("force") {
return fmt.Errorf("production deploys require --force flag")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
env := args[0]
region := viper.GetString("region")
force := viper.GetBool("force")
fmt.Printf("Deploying to %s in region %s (force=%v)\n", env, region, force)
// Actual deployment logic
return deploy(env, region, force)
},
PostRunE: func(cmd *cobra.Command, args []string) error {
// Cleanup or notifications
fmt.Println("Deployment complete")
return nil
},
}
func init() {
rootCmd.AddCommand(deployCmd)
// Local flags (only for this command)
deployCmd.Flags().StringP("region", "r", "us-east-1", "AWS region")
deployCmd.Flags().BoolP("force", "f", false, "Force deployment without confirmation")
// Bind flags to viper
viper.BindPFlag("region", deployCmd.Flags().Lookup("region"))
viper.BindPFlag("force", deployCmd.Flags().Lookup("force"))
}
func deploy(env, region string, force bool) error {
// Implementation
return nil
}go
// cmd/deploy.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy application to specified environment",
Long: `Deploy the application to the specified environment.
Supports: dev, staging, production`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{"dev", "staging", "production"},
PreRunE: func(cmd *cobra.Command, args []string) error {
// Validation logic runs before RunE
env := args[0]
if env == "production" && !viper.GetBool("force") {
return fmt.Errorf("production deploys require --force flag")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
env := args[0]
region := viper.GetString("region")
force := viper.GetBool("force")
fmt.Printf("Deploying to %s in region %s (force=%v)\n", env, region, force)
// Actual deployment logic
return deploy(env, region, force)
},
PostRunE: func(cmd *cobra.Command, args []string) error {
// Cleanup or notifications
fmt.Println("Deployment complete")
return nil
},
}
func init() {
rootCmd.AddCommand(deployCmd)
// Local flags (only for this command)
deployCmd.Flags().StringP("region", "r", "us-east-1", "AWS region")
deployCmd.Flags().BoolP("force", "f", false, "Force deployment without confirmation")
// Bind flags to viper
viper.BindPFlag("region", deployCmd.Flags().Lookup("region"))
viper.BindPFlag("force", deployCmd.Flags().Lookup("force"))
}
func deploy(env, region string, force bool) error {
// Implementation
return nil
}Persistent vs. Local Flags
全局标志与局部标志
go
// Persistent flags: Available to command and all subcommands
rootCmd.PersistentFlags().String("config", "", "config file path")
rootCmd.PersistentFlags().Bool("verbose", false, "verbose output")
// Local flags: Only available to this specific command
deployCmd.Flags().String("region", "us-east-1", "deployment region")
deployCmd.Flags().Bool("force", false, "force deployment")
// Required flags
deployCmd.MarkFlagRequired("region")
// Flag dependencies
deployCmd.MarkFlagsRequiredTogether("username", "password")
deployCmd.MarkFlagsMutuallyExclusive("json", "yaml")go
// Persistent flags: Available to command and all subcommands
rootCmd.PersistentFlags().String("config", "", "config file path")
rootCmd.PersistentFlags().Bool("verbose", false, "verbose output")
// Local flags: Only available to this specific command
deployCmd.Flags().String("region", "us-east-1", "deployment region")
deployCmd.Flags().Bool("force", false, "force deployment")
// Required flags
deployCmd.MarkFlagRequired("region")
// Flag dependencies
deployCmd.MarkFlagsRequiredTogether("username", "password")
deployCmd.MarkFlagsMutuallyExclusive("json", "yaml")PreRun/PostRun Hooks
PreRun/PostRun钩子
Cobra provides execution hooks for setup and cleanup:
go
var serverCmd = &cobra.Command{
Use: "server",
Short: "Start API server",
// Execution order (all optional):
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Runs before PreRunE, inherited by subcommands
return setupLogging()
},
PreRunE: func(cmd *cobra.Command, args []string) error {
// Validation and setup before RunE
return validateConfig()
},
RunE: func(cmd *cobra.Command, args []string) error {
// Main command logic
return startServer()
},
PostRunE: func(cmd *cobra.Command, args []string) error {
// Cleanup after RunE
return cleanup()
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
// Runs after PostRunE, inherited by subcommands
return flushLogs()
},
}Important: Use , , (error-returning versions) instead of , , .
RunEPreRunEPostRunERunPreRunPostRunCobra提供用于初始化和清理的执行钩子:
go
var serverCmd = &cobra.Command{
Use: "server",
Short: "Start API server",
// Execution order (all optional):
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Runs before PreRunE, inherited by subcommands
return setupLogging()
},
PreRunE: func(cmd *cobra.Command, args []string) error {
// Validation and setup before RunE
return validateConfig()
},
RunE: func(cmd *cobra.Command, args []string) error {
// Main command logic
return startServer()
},
PostRunE: func(cmd *cobra.Command, args []string) error {
// Cleanup after RunE
return cleanup()
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
// Runs after PostRunE, inherited by subcommands
return flushLogs()
},
}重要提示:使用、、(返回错误的版本)而非、、。
RunEPreRunEPostRunERunPreRunPostRunViper Configuration Management
Viper配置管理
Configuration Priority
配置优先级
Viper follows a strict precedence order (highest to lowest):
- Explicit Set ()
viper.Set("key", value) - Command-line Flags (bound with )
viper.BindPFlag - Environment Variables ()
MYAPP_KEY=value - Config File (,
~/.myapp.yaml)./config.yaml - Key/Value Store (etcd, Consul - optional)
- Defaults ()
viper.SetDefault("key", value)
go
func initConfig() {
// 1. Set defaults (lowest priority)
viper.SetDefault("port", 8080)
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 5432)
// 2. Config file locations (checked in order)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("/etc/myapp/")
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath(".")
// 3. Environment variables (prefix + automatic mapping)
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv() // MYAPP_PORT, MYAPP_DATABASE_HOST, etc.
// 4. Read config file (optional)
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found - use defaults and env vars
} else {
// Config file found but error reading it
return err
}
}
// 5. Flags will be bound in init() functions (highest priority)
}Viper遵循严格的优先级顺序(从高到低):
- 显式设置()
viper.Set("key", value) - 命令行标志(通过绑定)
viper.BindPFlag - 环境变量()
MYAPP_KEY=value - 配置文件(、
~/.myapp.yaml)./config.yaml - 键值存储(etcd、Consul - 可选)
- 默认值()
viper.SetDefault("key", value)
go
func initConfig() {
// 1. Set defaults (lowest priority)
viper.SetDefault("port", 8080)
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 5432)
// 2. Config file locations (checked in order)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("/etc/myapp/")
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath(".")
// 3. Environment variables (prefix + automatic mapping)
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv() // MYAPP_PORT, MYAPP_DATABASE_HOST, etc.
// 4. Read config file (optional)
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found - use defaults and env vars
} else {
// Config file found but error reading it
return err
}
}
// 5. Flags will be bound in init() functions (highest priority)
}Environment Variable Mapping
环境变量映射
Viper automatically maps environment variables with prefix and dot notation:
go
viper.SetEnvPrefix("MYAPP") // Prefix for env vars
viper.AutomaticEnv() // Enable automatic mapping
// Config key → Environment variable
// "port" → MYAPP_PORT
// "database.host" → MYAPP_DATABASE_HOST
// "database.port" → MYAPP_DATABASE_PORT
// "aws.s3.region" → MYAPP_AWS_S3_REGIONManual mapping for non-standard env var names:
go
viper.BindEnv("database.host", "DB_HOST") // Custom env var name
viper.BindEnv("database.password", "DB_PASSWORD") // Different naming conventionViper会自动将带前缀的环境变量与点标记的配置键进行映射:
go
viper.SetEnvPrefix("MYAPP") // Prefix for env vars
viper.AutomaticEnv() // Enable automatic mapping
// Config key → Environment variable
// "port" → MYAPP_PORT
// "database.host" → MYAPP_DATABASE_HOST
// "database.port" → MYAPP_DATABASE_PORT
// "aws.s3.region" → MYAPP_AWS_S3_REGION手动映射非标准环境变量名:
go
viper.BindEnv("database.host", "DB_HOST") // Custom env var name
viper.BindEnv("database.password", "DB_PASSWORD") // Different naming conventionConfig File Formats
配置文件格式
Viper supports multiple formats: YAML, JSON, TOML, HCL, INI, envfile, Java properties.
config.yaml:
yaml
port: 8080
log_level: info
database:
host: localhost
port: 5432
user: postgres
ssl_mode: require
aws:
region: us-east-1
s3:
bucket: my-app-bucketAccessing config values:
go
port := viper.GetInt("port") // 8080
dbHost := viper.GetString("database.host") // "localhost"
s3Bucket := viper.GetString("aws.s3.bucket") // "my-app-bucket"
// Type-safe access
if viper.IsSet("database.ssl_mode") {
sslMode := viper.GetString("database.ssl_mode")
}
// Unmarshal into struct
type Config struct {
Port int `mapstructure:"port"`
LogLevel string `mapstructure:"log_level"`
Database struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
SSLMode string `mapstructure:"ssl_mode"`
} `mapstructure:"database"`
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return err
}Viper支持多种格式:YAML、JSON、TOML、HCL、INI、envfile、Java属性文件。
config.yaml:
yaml
port: 8080
log_level: info
database:
host: localhost
port: 5432
user: postgres
ssl_mode: require
aws:
region: us-east-1
s3:
bucket: my-app-bucket访问配置值:
go
port := viper.GetInt("port") // 8080
dbHost := viper.GetString("database.host") // "localhost"
s3Bucket := viper.GetString("aws.s3.bucket") // "my-app-bucket"
// Type-safe access
if viper.IsSet("database.ssl_mode") {
sslMode := viper.GetString("database.ssl_mode")
}
// Unmarshal into struct
type Config struct {
Port int `mapstructure:"port"`
LogLevel string `mapstructure:"log_level"`
Database struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
SSLMode string `mapstructure:"ssl_mode"`
} `mapstructure:"database"`
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return err
}Cobra + Viper Integration
Cobra + Viper集成
Critical Integration Pattern
关键集成模式
The key to Cobra + Viper integration is binding flags to Viper keys:
go
// cmd/root.go
func init() {
cobra.OnInitialize(initConfig) // Load config before command execution
// Define flags
rootCmd.PersistentFlags().String("config", "", "config file")
rootCmd.PersistentFlags().String("log-level", "info", "log level")
rootCmd.PersistentFlags().Int("port", 8080, "server port")
// Bind flags to Viper (critical step!)
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
viper.BindPFlag("log_level", rootCmd.PersistentFlags().Lookup("log-level"))
viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
}
func initConfig() {
// This runs BEFORE command execution via cobra.OnInitialize
if cfgFile := viper.GetString("config"); cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath(".")
viper.SetConfigName("config")
}
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
viper.ReadInConfig() // Ignore errors - config file is optional
}Flag binding strategies:
go
// Strategy 1: Bind each flag individually (explicit)
viper.BindPFlag("log_level", rootCmd.Flags().Lookup("log-level"))
// Strategy 2: Bind all flags automatically (convenient)
viper.BindPFlags(rootCmd.Flags())
// Strategy 3: Hybrid approach (recommended)
// - Bind persistent flags globally
// - Bind local flags in each command's init()
rootCmd.PersistentFlags().String("config", "", "config file")
viper.BindPFlags(rootCmd.PersistentFlags())
deployCmd.Flags().String("region", "us-east-1", "AWS region")
viper.BindPFlag("deploy.region", deployCmd.Flags().Lookup("region"))Cobra + Viper集成的核心是将标志绑定到Viper键:
go
// cmd/root.go
func init() {
cobra.OnInitialize(initConfig) // Load config before command execution
// Define flags
rootCmd.PersistentFlags().String("config", "", "config file")
rootCmd.PersistentFlags().String("log-level", "info", "log level")
rootCmd.PersistentFlags().Int("port", 8080, "server port")
// Bind flags to Viper (critical step!)
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
viper.BindPFlag("log_level", rootCmd.PersistentFlags().Lookup("log-level"))
viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
}
func initConfig() {
// This runs BEFORE command execution via cobra.OnInitialize
if cfgFile := viper.GetString("config"); cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath(".")
viper.SetConfigName("config")
}
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
viper.ReadInConfig() // Ignore errors - config file is optional
}标志绑定策略:
go
// Strategy 1: Bind each flag individually (explicit)
viper.BindPFlag("log_level", rootCmd.Flags().Lookup("log-level"))
// Strategy 2: Bind all flags automatically (convenient)
viper.BindPFlags(rootCmd.Flags())
// Strategy 3: Hybrid approach (recommended)
// - Bind persistent flags globally
// - Bind local flags in each command's init()
rootCmd.PersistentFlags().String("config", "", "config file")
viper.BindPFlags(rootCmd.PersistentFlags())
deployCmd.Flags().String("region", "us-east-1", "AWS region")
viper.BindPFlag("deploy.region", deployCmd.Flags().Lookup("region"))PersistentPreRun for Config Loading
使用PersistentPreRun加载配置
Use to load and validate configuration:
PersistentPreRunEgo
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Runs before ALL commands (inherited by subcommands)
// 1. Validate required config
if !viper.IsSet("api_key") {
return fmt.Errorf("API key not configured (set MYAPP_API_KEY or add to config file)")
}
// 2. Setup logging based on config
logLevel := viper.GetString("log_level")
if err := setupLogging(logLevel); err != nil {
return fmt.Errorf("invalid log level: %w", err)
}
// 3. Initialize clients/services
apiKey := viper.GetString("api_key")
if err := initAPIClient(apiKey); err != nil {
return fmt.Errorf("failed to initialize API client: %w", err)
}
return nil
},
}使用加载并验证配置:
PersistentPreRunEgo
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Runs before ALL commands (inherited by subcommands)
// 1. Validate required config
if !viper.IsSet("api_key") {
return fmt.Errorf("API key not configured (set MYAPP_API_KEY or add to config file)")
}
// 2. Setup logging based on config
logLevel := viper.GetString("log_level")
if err := setupLogging(logLevel); err != nil {
return fmt.Errorf("invalid log level: %w", err)
}
// 3. Initialize clients/services
apiKey := viper.GetString("api_key")
if err := initAPIClient(apiKey); err != nil {
return fmt.Errorf("failed to initialize API client: %w", err)
}
return nil
},
}Shell Completion
Shell补全
Cobra generates shell completion scripts for bash, zsh, fish, and PowerShell.
Cobra可为bash、zsh、fish和PowerShell生成Shell补全脚本。
Adding Completion Command
添加补全命令
go
// cmd/completion.go
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Long: `Generate shell completion script for myapp.
To load completions:
Bash:
$ source <(myapp completion bash)
# To load automatically, add to ~/.bashrc:
$ echo 'source <(myapp completion bash)' >> ~/.bashrc
Zsh:
$ source <(myapp completion zsh)
# To load automatically, add to ~/.zshrc:
$ echo 'source <(myapp completion zsh)' >> ~/.zshrc
Fish:
$ myapp completion fish | source
# To load automatically:
$ myapp completion fish > ~/.config/fish/completions/myapp.fish
PowerShell:
PS> myapp completion powershell | Out-String | Invoke-Expression
# To load automatically, add to PowerShell profile.
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
return nil
},
}
func init() {
rootCmd.AddCommand(completionCmd)
}go
// cmd/completion.go
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Long: `Generate shell completion script for myapp.
To load completions:
Bash:
$ source <(myapp completion bash)
# To load automatically, add to ~/.bashrc:
$ echo 'source <(myapp completion bash)' >> ~/.bashrc
Zsh:
$ source <(myapp completion zsh)
# To load automatically, add to ~/.zshrc:
$ echo 'source <(myapp completion zsh)' >> ~/.zshrc
Fish:
$ myapp completion fish | source
# To load automatically:
$ myapp completion fish > ~/.config/fish/completions/myapp.fish
PowerShell:
PS> myapp completion powershell | Out-String | Invoke-Expression
# To load automatically, add to PowerShell profile.
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
return nil
},
}
func init() {
rootCmd.AddCommand(completionCmd)
}Custom Completion Functions
自定义补全函数
Provide dynamic completions for command arguments:
go
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy to environment",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Return available environments
envs := []string{"dev", "staging", "production"}
return envs, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
return deploy(args[0])
},
}
// Custom flag completion
deployCmd.RegisterFlagCompletionFunc("region", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
regions := []string{"us-east-1", "us-west-2", "eu-west-1"}
return regions, cobra.ShellCompDirectiveNoFileComp
})为命令参数提供动态补全:
go
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy to environment",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Return available environments
envs := []string{"dev", "staging", "production"}
return envs, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
return deploy(args[0])
},
}
// Custom flag completion
deployCmd.RegisterFlagCompletionFunc("region", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
regions := []string{"us-east-1", "us-west-2", "eu-west-1"}
return regions, cobra.ShellCompDirectiveNoFileComp
})CLI Best Practices
CLI最佳实践
User-Friendly Error Messages
用户友好的错误消息
go
// ❌ BAD: Technical jargon
return fmt.Errorf("db connection failed: EOF")
// ✅ GOOD: Actionable error message
return fmt.Errorf("cannot connect to database at %s:%d\nPlease check:\n - Database is running\n - Credentials are correct (MYAPP_DB_PASSWORD)\n - Network connectivity", host, port)
// ✅ GOOD: Suggest remediation
if !viper.IsSet("api_key") {
return fmt.Errorf("API key not configured\nSet environment variable: export MYAPP_API_KEY=your-key\nOr add to config file: ~/.myapp.yaml")
}go
// ❌ 错误示例:技术术语过多
return fmt.Errorf("db connection failed: EOF")
// ✅ 正确示例:可执行的错误消息
return fmt.Errorf("无法连接到数据库 %s:%d\n请检查:\n - 数据库是否运行\n - 凭证是否正确(MYAPP_DB_PASSWORD)\n - 网络连通性", host, port)
// ✅ 正确示例:提供修复建议
if !viper.IsSet("api_key") {
return fmt.Errorf("未配置API密钥\n设置环境变量:export MYAPP_API_KEY=your-key\n或添加到配置文件:~/.myapp.yaml")
}Progress Indicators
进度指示器
go
import "github.com/briandowns/spinner"
func deploy(env string) error {
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
s.Suffix = " Deploying to " + env + "..."
s.Start()
defer s.Stop()
// Deployment logic
if err := performDeployment(env); err != nil {
s.Stop()
return err
}
s.Stop()
fmt.Println("✓ Deployment successful")
return nil
}go
import "github.com/briandowns/spinner"
func deploy(env string) error {
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
s.Suffix = " Deploying to " + env + "..."
s.Start()
defer s.Stop()
// Deployment logic
if err := performDeployment(env); err != nil {
s.Stop()
return err
}
s.Stop()
fmt.Println("✓ 部署成功")
return nil
}Output Formatting
输出格式化
go
import (
"encoding/json"
"github.com/olekukonko/tablewriter"
)
func displayResults(items []Item, format string) error {
switch format {
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(items)
case "table":
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Name", "Status"})
for _, item := range items {
table.Append([]string{item.ID, item.Name, item.Status})
}
table.Render()
return nil
default:
return fmt.Errorf("unknown format: %s (use json or table)", format)
}
}go
import (
"encoding/json"
"github.com/olekukonko/tablewriter"
)
func displayResults(items []Item, format string) error {
switch format {
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(items)
case "table":
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "名称", "状态"})
for _, item := range items {
table.Append([]string{item.ID, item.Name, item.Status})
}
table.Render()
return nil
default:
return fmt.Errorf("未知格式:%s(请使用json或table)", format)
}
}Logging vs. User Output
日志与用户输出分离
go
import (
"log"
"os"
)
var (
// User-facing output (stdout)
out = os.Stdout
// Logging and errors (stderr)
logger = log.New(os.Stderr, "[myapp] ", log.LstdFlags)
)
func RunCommand() error {
// User output: stdout
fmt.Fprintln(out, "Processing files...")
// Debug/verbose logging: stderr
if viper.GetBool("verbose") {
logger.Println("Reading config from", viper.ConfigFileUsed())
}
// Errors: stderr
if err := process(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return err
}
// Success message: stdout
fmt.Fprintln(out, "✓ Complete")
return nil
}go
import (
"log"
"os"
)
var (
// 用户面向的输出(stdout)
out = os.Stdout
// 日志和错误输出(stderr)
logger = log.New(os.Stderr, "[myapp] ", log.LstdFlags)
)
func RunCommand() error {
// 用户输出:stdout
fmt.Fprintln(out, "正在处理文件...")
// 调试/详细日志:stderr
if viper.GetBool("verbose") {
logger.Println("正在从", viper.ConfigFileUsed(), "读取配置")
}
// 错误输出:stderr
if err := process(); err != nil {
fmt.Fprintf(os.Stderr, "错误:%v\n", err)
return err
}
// 成功消息:stdout
fmt.Fprintln(out, "✓ 完成")
return nil
}Testing CLI Applications
测试CLI应用
Testing Command Execution
测试命令执行
go
// cmd/deploy_test.go
package cmd
import (
"bytes"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func executeCommand(root *cobra.Command, args ...string) (output string, err error) {
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs(args)
err = root.Execute()
return buf.String(), err
}
func TestDeployCommand(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
wantOut string
}{
{
name: "deploy to dev",
args: []string{"deploy", "dev"},
wantErr: false,
wantOut: "Deploying to dev",
},
{
name: "deploy to production without force",
args: []string{"deploy", "production"},
wantErr: true,
wantOut: "production deploys require --force flag",
},
{
name: "deploy to production with force",
args: []string{"deploy", "production", "--force"},
wantErr: false,
wantOut: "Deploying to production",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output, err := executeCommand(rootCmd, tt.args...)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Contains(t, output, tt.wantOut)
})
}
}go
// cmd/deploy_test.go
package cmd
import (
"bytes"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func executeCommand(root *cobra.Command, args ...string) (output string, err error) {
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs(args)
err = root.Execute()
return buf.String(), err
}
func TestDeployCommand(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
wantOut string
}{
{
name: "部署到dev环境",
args: []string{"deploy", "dev"},
wantErr: false,
wantOut: "Deploying to dev",
},
{
name: "未加force标志部署到生产环境",
args: []string{"deploy", "production"},
wantErr: true,
wantOut: "production deploys require --force flag",
},
{
name: "添加force标志部署到生产环境",
args: []string{"deploy", "production", "--force"},
wantErr: false,
wantOut: "Deploying to production",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output, err := executeCommand(rootCmd, tt.args...)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Contains(t, output, tt.wantOut)
})
}
}Testing with Viper Configuration
结合Viper配置测试
go
func TestCommandWithConfig(t *testing.T) {
// Reset viper state before each test
viper.Reset()
// Set test configuration
viper.Set("region", "eu-west-1")
viper.Set("api_key", "test-key-123")
output, err := executeCommand(rootCmd, "deploy", "staging")
require.NoError(t, err)
assert.Contains(t, output, "eu-west-1")
}go
func TestCommandWithConfig(t *testing.T) {
// 每次测试前重置Viper状态
viper.Reset()
// 设置测试配置
viper.Set("region", "eu-west-1")
viper.Set("api_key", "test-key-123")
output, err := executeCommand(rootCmd, "deploy", "staging")
require.NoError(t, err)
assert.Contains(t, output, "eu-west-1")
}Capturing Output
捕获输出
go
func TestOutputFormat(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
defer func() { os.Stdout = oldStdout }()
// Execute command
err := listCmd.RunE(listCmd, []string{})
require.NoError(t, err)
// Read output
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
assert.Contains(t, output, "ID")
assert.Contains(t, output, "Name")
}go
func TestOutputFormat(t *testing.T) {
// 捕获stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
defer func() { os.Stdout = oldStdout }()
// 执行命令
err := listCmd.RunE(listCmd, []string{})
require.NoError(t, err)
// 读取输出
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
assert.Contains(t, output, "ID")
assert.Contains(t, output, "名称")
}Decision Trees
决策树
When to Use Cobra
何时使用Cobra
Use Cobra when:
- ✅ Building multi-command CLI with subcommands (e.g., ,
git clone)docker run - ✅ Need POSIX-compliant flag parsing (,
--flag)-f - ✅ Want built-in help generation ()
--help - ✅ Require shell completion support
- ✅ Building professional CLI used by other developers
Don't use Cobra when:
- ❌ Simple single-command script (use package)
flag - ❌ Internal-only tool with 1-2 flags
- ❌ Prototyping or throwaway scripts
使用Cobra的场景:
- ✅ 构建支持子命令的多命令CLI(如、
git clone)docker run - ✅ 需要符合POSIX标准的标志解析(、
--flag)-f - ✅ 想要自动生成帮助文档()
--help - ✅ 需要Shell补全支持
- ✅ 构建供其他开发者使用的专业CLI
不使用Cobra的场景:
- ❌ 简单的单命令脚本(使用包即可)
flag - ❌ 仅含1-2个标志的内部工具
- ❌ 原型或一次性脚本
When to Use Viper
何时使用Viper
Use Viper when:
- ✅ Need configuration from multiple sources (flags, env vars, files)
- ✅ Want clear configuration precedence rules
- ✅ Support multiple config file formats (YAML, JSON, TOML)
- ✅ Require environment variable mapping with prefixes
- ✅ Need live config reloading (watch config file changes)
Don't use Viper when:
- ❌ Only using command-line flags (Cobra alone is sufficient)
- ❌ Hardcoded configuration values
- ❌ Simple scripts with no configuration
使用Viper的场景:
- ✅ 需要从多源加载配置(标志、环境变量、文件)
- ✅ 想要清晰的配置优先级规则
- ✅ 支持多种配置文件格式(YAML、JSON、TOML)
- ✅ 需要带前缀的环境变量映射
- ✅ 需要配置热重载(监听配置文件变化)
不使用Viper的场景:
- ❌ 仅使用命令行标志(单独使用Cobra即可)
- ❌ 硬编码配置值
- ❌ 无配置需求的简单脚本
When to Add Shell Completion
何时添加Shell补全
Add shell completion when:
- ✅ CLI used frequently by developers (daily/hourly)
- ✅ Many subcommands or complex flag combinations
- ✅ Arguments have known valid values (e.g., environments, regions)
- ✅ Building professional developer tools
Skip shell completion when:
- ❌ CLI used rarely (monthly or less)
- ❌ Simple commands with few options
- ❌ Internal-only tools
添加Shell补全的场景:
- ✅ CLI被开发者高频使用(每日/每小时)
- ✅ 包含多个子命令或复杂的标志组合
- ✅ 参数有已知的有效值(如环境、区域)
- ✅ 构建专业的开发者工具
跳过Shell补全的场景:
- ❌ CLI使用频率低(每月或更少)
- ❌ 命令简单、选项少
- ❌ 仅内部使用的工具
When to Use Persistent Flags
何时使用全局标志
Use persistent flags when:
- ✅ Flag applies to ALL subcommands (e.g., ,
--verbose)--config - ✅ Common configuration shared across commands
- ✅ Global behavior modifiers (e.g., ,
--dry-run)--output
Use local flags when:
- ✅ Flag only relevant to specific command
- ✅ Command-specific parameters (e.g., for deploy command)
--region
使用全局标志的场景:
- ✅ 标志适用于所有子命令(如、
--verbose)--config - ✅ 命令间共享的通用配置
- ✅ 全局行为修改器(如、
--dry-run)--output
使用局部标志的场景:
- ✅ 标志仅与特定命令相关
- ✅ 命令专属参数(如deploy命令的)
--region
Anti-Patterns
反模式
❌ Not Handling Errors in PreRunE/RunE
❌ 未在PreRunE/RunE中处理错误
Wrong:
go
var deployCmd = &cobra.Command{
Use: "deploy",
Run: func(cmd *cobra.Command, args []string) {
deploy(args[0]) // Ignores error!
},
}Correct:
go
var deployCmd = &cobra.Command{
Use: "deploy",
RunE: func(cmd *cobra.Command, args []string) error {
return deploy(args[0]) // Proper error handling
},
}错误示例:
go
var deployCmd = &cobra.Command{
Use: "deploy",
Run: func(cmd *cobra.Command, args []string) {
deploy(args[0]) // 忽略错误!
},
}正确示例:
go
var deployCmd = &cobra.Command{
Use: "deploy",
RunE: func(cmd *cobra.Command, args []string) error {
return deploy(args[0]) // 正确处理错误
},
}❌ Mixing Configuration Sources Without Clear Precedence
❌ 混合配置源且无清晰优先级
Wrong:
go
// Confusing: Which takes precedence?
config.Port = viper.GetInt("port")
if os.Getenv("PORT") != "" {
config.Port = atoi(os.Getenv("PORT"))
}
if *flagPort != 0 {
config.Port = *flagPort
}Correct:
go
// Clear: Viper handles precedence automatically
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
viper.SetDefault("port", 8080)
config.Port = viper.GetInt("port") // Respects: flag > env > config > default错误示例:
go
// 混淆:哪个优先级更高?
config.Port = viper.GetInt("port")
if os.Getenv("PORT") != "" {
config.Port = atoi(os.Getenv("PORT"))
}
if *flagPort != 0 {
config.Port = *flagPort
}正确示例:
go
// 清晰:Viper自动处理优先级
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
viper.SetDefault("port", 8080)
config.Port = viper.GetInt("port") // 遵循:标志 > 环境变量 > 配置文件 > 默认值❌ Forgetting to Bind Flags to Viper
❌ 忘记将标志绑定到Viper
Wrong:
go
rootCmd.Flags().String("region", "us-east-1", "AWS region")
// Flag not bound to Viper - won't respect precedence!
func deploy() {
region := viper.GetString("region") // Always returns config file value
}Correct:
go
rootCmd.Flags().String("region", "us-east-1", "AWS region")
viper.BindPFlag("region", rootCmd.Flags().Lookup("region")) // Bind it!
func deploy() {
region := viper.GetString("region") // Respects flag > env > config
}错误示例:
go
rootCmd.Flags().String("region", "us-east-1", "AWS region")
// 标志未绑定到Viper - 不遵循优先级!
func deploy() {
region := viper.GetString("region") // 始终返回配置文件中的值
}正确示例:
go
rootCmd.Flags().String("region", "us-east-1", "AWS region")
viper.BindPFlag("region", rootCmd.Flags().Lookup("region")) // 绑定标志!
func deploy() {
region := viper.GetString("region") // 遵循:标志 > 环境变量 > 配置文件
}❌ Not Testing CLI Commands
❌ 未测试CLI命令
Wrong:
go
// No tests for CLI commands - bugs slip throughCorrect:
go
func TestDeployCommand(t *testing.T) {
output, err := executeCommand(rootCmd, "deploy", "staging", "--region", "eu-west-1")
require.NoError(t, err)
assert.Contains(t, output, "Deploying to staging")
assert.Contains(t, output, "eu-west-1")
}错误示例:
go
// 无CLI命令测试 - 漏洞容易被忽略正确示例:
go
func TestDeployCommand(t *testing.T) {
output, err := executeCommand(rootCmd, "deploy", "staging", "--region", "eu-west-1")
require.NoError(t, err)
assert.Contains(t, output, "Deploying to staging")
assert.Contains(t, output, "eu-west-1")
}❌ Poor Error Messages
❌ 错误消息质量差
Wrong:
go
return fmt.Errorf("connection failed") // UnhelpfulCorrect:
go
return fmt.Errorf("cannot connect to database at %s:%d\nCheck:\n - Database is running\n - Credentials (MYAPP_DB_PASSWORD)\n - Firewall rules", host, port)错误示例:
go
return fmt.Errorf("连接失败") // 无帮助正确示例:
go
return fmt.Errorf("无法连接到数据库 %s:%d\n检查:\n - 数据库是否运行\n - 凭证(MYAPP_DB_PASSWORD)\n - 防火墙规则", host, port)❌ Using Run Instead of RunE
❌ 使用Run而非RunE
Wrong:
go
var rootCmd = &cobra.Command{
Use: "myapp",
Run: func(cmd *cobra.Command, args []string) {
if err := execute(); err != nil {
fmt.Println(err) // Error not propagated
}
},
}Correct:
go
var rootCmd = &cobra.Command{
Use: "myapp",
RunE: func(cmd *cobra.Command, args []string) error {
return execute() // Cobra handles error display and exit code
},
}错误示例:
go
var rootCmd = &cobra.Command{
Use: "myapp",
Run: func(cmd *cobra.Command, args []string) {
if err := execute(); err != nil {
fmt.Println(err) // 错误未被传播
}
},
}正确示例:
go
var rootCmd = &cobra.Command{
Use: "myapp",
RunE: func(cmd *cobra.Command, args []string) error {
return execute() // Cobra处理错误显示和退出码
},
}Production Example
生产示例
Minimal production-ready CLI structure:
myapp/
├── cmd/
│ ├── root.go # Root command + config loading
│ ├── deploy.go # Deploy subcommand
│ ├── status.go # Status subcommand
│ └── completion.go # Shell completion
├── main.go # Entry point
├── config.yaml # Example config file
└── go.modmain.go:
go
package main
import "myapp/cmd"
func main() {
cmd.Execute()
}cmd/root.go: See "Command Structure Pattern" section above
Building and installing:
bash
undefined最小化生产就绪CLI结构:
myapp/
├── cmd/
│ ├── root.go # 根命令 + 配置加载
│ ├── deploy.go # 部署子命令
│ ├── status.go # 状态子命令
│ └── completion.go # Shell补全
├── main.go # 入口文件
├── config.yaml # 示例配置文件
└── go.modmain.go:
go
package main
import "myapp/cmd"
func main() {
cmd.Execute()
}cmd/root.go:参见上方「命令结构模式」章节
构建与安装:
bash
undefinedDevelopment
开发模式
go run main.go deploy staging --region us-west-2
go run main.go deploy staging --region us-west-2
Production build
生产构建
go build -o myapp
go build -o myapp
Install globally
全局安装
go install
go install
Enable shell completion
启用Shell补全
myapp completion bash > /etc/bash_completion.d/myapp
undefinedmyapp completion bash > /etc/bash_completion.d/myapp
undefinedResources
资源
Official Documentation:
- Cobra User Guide - Official framework documentation
- Viper Documentation - Configuration management guide
Learning Resources:
- "Building CLI Apps in Go with Cobra & Viper" (November 2025) - Comprehensive tutorial
- "The Cobra & Viper Journey" - Learning path for CLI development
- Cobra Generator - Scaffolding tool for new CLIs
Production Examples:
Related Skills:
- - Testing CLI commands comprehensively
golang-testing-strategies - - Building API servers with configuration
golang-http-servers - - Async operations in CLI tools
golang-concurrency-patterns
官方文档:
学习资源:
- "Building CLI Apps in Go with Cobra & Viper"(2025年11月)- 全面教程
- "The Cobra & Viper Journey" - CLI开发学习路径
- Cobra生成器 - 新CLI脚手架工具
生产示例:
相关技能:
- - 全面测试CLI命令
golang-testing-strategies - - 结合配置构建API服务
golang-http-servers - - CLI工具中的异步操作
golang-concurrency-patterns
Success Criteria
成功标准
You know you've mastered Cobra + Viper when:
- ✅ Commands follow POSIX conventions ()
VERB NOUN --FLAG - ✅ Configuration precedence is clear: flags > env > config > defaults
- ✅ All flags bound to Viper for unified config access
- ✅ Shell completion generated for all major shells
- ✅ Error messages are actionable and user-friendly
- ✅ CLI commands have comprehensive tests
- ✅ Help text auto-generated and accurate
- ✅ PersistentPreRunE used for global setup/validation
- ✅ Separation of concerns: user output (stdout) vs. logging (stderr)
- ✅ Config files optional - CLI works with flags/env vars alone
当你掌握Cobra + Viper时,你能够:
- ✅ 命令遵循POSIX规范()
VERB NOUN --FLAG - ✅ 配置优先级清晰:标志 > 环境变量 > 配置文件 > 默认值
- ✅ 所有标志绑定到Viper以实现统一配置访问
- ✅ 为所有主流Shell生成补全脚本
- ✅ 错误消息可执行且用户友好
- ✅ CLI命令有全面的测试
- ✅ 帮助文本自动生成且准确
- ✅ 使用PersistentPreRunE进行全局初始化/验证
- ✅ 关注点分离:用户输出(stdout)与日志(stderr)
- ✅ 配置文件可选 - CLI仅通过标志/环境变量即可工作