Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:04:14 +08:00
commit 70c36b5eff
248 changed files with 47482 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
# Kubectl-Style CLI Example
This example demonstrates how to build a kubectl-style CLI with nested resource commands and consistent flag handling.
## Structure
```
myctl/
├── cmd/
│ ├── root.go # Root command with global flags
│ ├── get/
│ │ ├── get.go # Parent "get" command
│ │ ├── pods.go # Get pods subcommand
│ │ ├── services.go # Get services subcommand
│ │ └── deployments.go # Get deployments subcommand
│ ├── create/
│ │ ├── create.go # Parent "create" command
│ │ ├── deployment.go # Create deployment subcommand
│ │ └── service.go # Create service subcommand
│ ├── delete/
│ │ └── delete.go # Delete command (accepts any resource)
│ ├── apply.go # Apply from file/stdin
│ └── completion.go # Shell completion
└── main.go
```
## Usage Pattern
```bash
# Get resources
myctl get pods
myctl get pods my-pod
myctl get pods --namespace production
myctl get services --all-namespaces
# Create resources
myctl create deployment my-app --image nginx:latest --replicas 3
myctl create service my-svc --port 80 --target-port 8080
# Delete resources
myctl delete pod my-pod
myctl delete deployment my-app --force
# Apply configuration
myctl apply -f deployment.yaml
myctl apply -f config.yaml --dry-run
```
## Key Features
### 1. Resource-Based Organization
Commands are organized by resource type:
- `get <resource>` - Retrieve resources
- `create <resource>` - Create resources
- `delete <resource>` - Delete resources
### 2. Consistent Flag Handling
Global flags available to all commands:
- `--namespace, -n` - Target namespace
- `--all-namespaces, -A` - Query all namespaces
- `--output, -o` - Output format (json|yaml|text)
- `--verbose, -v` - Verbose logging
### 3. Command Groups
Organized help output:
```
Basic Commands:
get Display resources
describe Show detailed information
Management Commands:
create Create resources
delete Delete resources
apply Apply configuration
```
## Implementation Example
### Root Command (cmd/root.go)
```go
package cmd
import (
"github.com/spf13/cobra"
"myctl/cmd/get"
"myctl/cmd/create"
"myctl/cmd/delete"
)
var (
namespace string
allNamespaces bool
output string
)
var rootCmd = &cobra.Command{
Use: "myctl",
Short: "Kubernetes-style resource management CLI",
}
func Execute() error {
return rootCmd.Execute()
}
func init() {
// Global flags
rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "target namespace")
rootCmd.PersistentFlags().BoolVarP(&allNamespaces, "all-namespaces", "A", false, "query all namespaces")
rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "text", "output format (text|json|yaml)")
// Command groups
rootCmd.AddGroup(&cobra.Group{ID: "basic", Title: "Basic Commands:"})
rootCmd.AddGroup(&cobra.Group{ID: "management", Title: "Management Commands:"})
// Register commands
rootCmd.AddCommand(get.GetCmd)
rootCmd.AddCommand(create.CreateCmd)
rootCmd.AddCommand(delete.DeleteCmd)
}
// Helper to get global flags
func GetNamespace() string {
return namespace
}
func GetAllNamespaces() bool {
return allNamespaces
}
func GetOutput() string {
return output
}
```
### Get Command Parent (cmd/get/get.go)
```go
package get
import (
"github.com/spf13/cobra"
)
var GetCmd = &cobra.Command{
Use: "get",
Short: "Display resources",
Long: `Display one or many resources`,
GroupID: "basic",
}
func init() {
// Add resource subcommands
GetCmd.AddCommand(podsCmd)
GetCmd.AddCommand(servicesCmd)
GetCmd.AddCommand(deploymentsCmd)
}
```
### Get Pods Subcommand (cmd/get/pods.go)
```go
package get
import (
"fmt"
"github.com/spf13/cobra"
"myctl/cmd"
"myctl/internal/client"
)
var (
selector string
watch bool
)
var podsCmd = &cobra.Command{
Use: "pods [NAME]",
Short: "Display pods",
Long: `Display one or many pods`,
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Dynamic completion: fetch pod names
return client.ListPodNames(cmd.GetNamespace()), cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
namespace := cmd.GetNamespace()
allNamespaces := cmd.GetAllNamespaces()
output := cmd.GetOutput()
if len(args) == 0 {
// List pods
return listPods(namespace, allNamespaces, selector, output)
}
// Get specific pod
podName := args[0]
return getPod(namespace, podName, output)
},
}
func init() {
// Command-specific flags
podsCmd.Flags().StringVarP(&selector, "selector", "l", "", "label selector")
podsCmd.Flags().BoolVarP(&watch, "watch", "w", false, "watch for changes")
}
func listPods(namespace string, allNamespaces bool, selector string, output string) error {
// Implementation
fmt.Printf("Listing pods (namespace: %s, all: %v, selector: %s, format: %s)\n",
namespace, allNamespaces, selector, output)
return nil
}
func getPod(namespace, name, output string) error {
// Implementation
fmt.Printf("Getting pod: %s (namespace: %s, format: %s)\n", name, namespace, output)
return nil
}
```
### Create Deployment (cmd/create/deployment.go)
```go
package create
import (
"fmt"
"github.com/spf13/cobra"
)
var (
image string
replicas int
port int
)
var deploymentCmd = &cobra.Command{
Use: "deployment NAME",
Short: "Create a deployment",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
if image == "" {
return fmt.Errorf("--image is required")
}
fmt.Printf("Creating deployment: %s\n", name)
fmt.Printf(" Image: %s\n", image)
fmt.Printf(" Replicas: %d\n", replicas)
if port > 0 {
fmt.Printf(" Container Port: %d\n", port)
}
return createDeployment(name, image, replicas, port)
},
}
func init() {
deploymentCmd.Flags().StringVar(&image, "image", "", "container image (required)")
deploymentCmd.Flags().IntVar(&replicas, "replicas", 1, "number of replicas")
deploymentCmd.Flags().IntVar(&port, "port", 0, "container port")
deploymentCmd.MarkFlagRequired("image")
}
func createDeployment(name, image string, replicas, port int) error {
// Implementation
return nil
}
```
## Best Practices
### 1. Consistent Flag Naming
- Use single-letter shortcuts for common flags (`-n`, `-o`, `-v`)
- Use descriptive long names (`--namespace`, `--output`, `--verbose`)
- Keep flag behavior consistent across commands
### 2. Dynamic Completion
Provide shell completion for resource names:
```go
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return client.ListResourceNames(), cobra.ShellCompDirectiveNoFileComp
}
```
### 3. Error Messages
Provide helpful error messages with suggestions:
```go
if image == "" {
return fmt.Errorf("--image is required. Example: --image nginx:latest")
}
```
### 4. Dry Run Support
Support `--dry-run` for preview:
```go
if dryRun {
fmt.Printf("Would create deployment: %s\n", name)
return nil
}
```
### 5. Output Formats
Support multiple output formats:
```go
switch output {
case "json":
return printJSON(pods)
case "yaml":
return printYAML(pods)
default:
return printTable(pods)
}
```
## Testing
```go
func TestGetPodsCommand(t *testing.T) {
cmd := get.GetCmd
cmd.SetArgs([]string{"pods", "--namespace", "production"})
err := cmd.Execute()
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
```
## Advanced Features
### 1. Watch Mode
```go
if watch {
return watchPods(namespace, selector)
}
```
### 2. Label Selectors
```go
podsCmd.Flags().StringVarP(&selector, "selector", "l", "", "label selector (e.g., app=nginx)")
```
### 3. Field Selectors
```go
podsCmd.Flags().StringVar(&fieldSelector, "field-selector", "", "field selector (e.g., status.phase=Running)")
```
### 4. Multiple Output Formats
```go
podsCmd.Flags().StringVarP(&output, "output", "o", "text", "output format (text|json|yaml|wide)")
```
This example provides a complete kubectl-style CLI structure that you can adapt for your resource management needs.

View File

@@ -0,0 +1,538 @@
# Complete Production CLI Example
A complete example demonstrating all production features: configuration management, error handling, logging, context support, and testing.
## Features
- ✅ Viper configuration management
- ✅ Structured logging (with levels)
- ✅ Context-aware commands (cancellation support)
- ✅ Proper error handling with wrapped errors
- ✅ Shell completion
- ✅ Unit and integration tests
- ✅ Dry-run support
- ✅ Multiple output formats
- ✅ Version information
- ✅ Configuration file support
## Complete Implementation
### main.go
```go
package main
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/example/myapp/cmd"
)
func main() {
// Setup context with cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle interrupt signals gracefully
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
cancel()
}()
// Execute with context
if err := cmd.ExecuteContext(ctx); err != nil {
os.Exit(1)
}
}
```
### cmd/root.go
```go
package cmd
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var (
cfgFile string
verbose bool
logLevel string
logger *zap.Logger
)
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A production-grade CLI application",
Long: `A complete production CLI with proper error handling,
configuration management, logging, and context support.`,
Version: "1.0.0",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Initialize logger based on flags
return initLogger()
},
}
func ExecuteContext(ctx context.Context) error {
rootCmd.SetContext(ctx)
return rootCmd.Execute()
}
func Execute() error {
return rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
// Global flags
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.myapp.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "log level (debug|info|warn|error)")
// Bind to viper
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level"))
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName(".myapp")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil && verbose {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
func initLogger() error {
// Parse log level
level := zapcore.InfoLevel
if err := level.UnmarshalText([]byte(logLevel)); err != nil {
return fmt.Errorf("invalid log level: %w", err)
}
// Create logger config
config := zap.NewProductionConfig()
config.Level = zap.NewAtomicLevelAt(level)
if verbose {
config = zap.NewDevelopmentConfig()
}
// Build logger
var err error
logger, err = config.Build()
if err != nil {
return fmt.Errorf("failed to initialize logger: %w", err)
}
return nil
}
func GetLogger() *zap.Logger {
if logger == nil {
// Fallback logger
logger, _ = zap.NewProduction()
}
return logger
}
```
### cmd/process.go (Context-Aware Command)
```go
package cmd
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
)
var (
processTimeout time.Duration
processDryRun bool
processWorkers int
)
var processCmd = &cobra.Command{
Use: "process [files...]",
Short: "Process files with context support",
Long: `Process files with proper context handling,
graceful cancellation, and timeout support.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
logger := GetLogger()
// Apply timeout if specified
if processTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, processTimeout)
defer cancel()
}
logger.Info("Starting process",
zap.Strings("files", args),
zap.Int("workers", processWorkers),
zap.Bool("dry-run", processDryRun))
if processDryRun {
logger.Info("Dry run mode - no changes will be made")
return nil
}
// Process with context
if err := processFiles(ctx, args, processWorkers); err != nil {
logger.Error("Processing failed", zap.Error(err))
return fmt.Errorf("process failed: %w", err)
}
logger.Info("Processing completed successfully")
return nil
},
}
func init() {
rootCmd.AddCommand(processCmd)
processCmd.Flags().DurationVar(&processTimeout, "timeout", 0, "processing timeout")
processCmd.Flags().BoolVar(&processDryRun, "dry-run", false, "simulate without changes")
processCmd.Flags().IntVarP(&processWorkers, "workers", "w", 4, "number of workers")
}
func processFiles(ctx context.Context, files []string, workers int) error {
logger := GetLogger()
for _, file := range files {
// Check context cancellation
select {
case <-ctx.Done():
return ctx.Err()
default:
}
logger.Debug("Processing file", zap.String("file", file))
// Simulate work
if err := processFile(ctx, file); err != nil {
return fmt.Errorf("failed to process %s: %w", file, err)
}
}
return nil
}
func processFile(ctx context.Context, file string) error {
// Simulate processing with context awareness
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
// Do work
}
}
return nil
}
```
### cmd/config.go (Configuration Management)
```go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "Manage configuration",
}
var configViewCmd = &cobra.Command{
Use: "view",
Short: "View current configuration",
RunE: func(cmd *cobra.Command, args []string) error {
settings := viper.AllSettings()
fmt.Println("Current Configuration:")
fmt.Println("=====================")
for key, value := range settings {
fmt.Printf("%s: %v\n", key, value)
}
return nil
},
}
var configSetCmd = &cobra.Command{
Use: "set KEY VALUE",
Short: "Set configuration value",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
key := args[0]
value := args[1]
viper.Set(key, value)
if err := viper.WriteConfig(); err != nil {
if err := viper.SafeWriteConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
}
fmt.Printf("Set %s = %s\n", key, value)
return nil
},
}
func init() {
rootCmd.AddCommand(configCmd)
configCmd.AddCommand(configViewCmd)
configCmd.AddCommand(configSetCmd)
}
```
### cmd/version.go
```go
package cmd
import (
"fmt"
"runtime"
"github.com/spf13/cobra"
)
var (
Version = "dev"
Commit = "none"
BuildTime = "unknown"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("myapp version %s\n", Version)
fmt.Printf(" Commit: %s\n", Commit)
fmt.Printf(" Built: %s\n", BuildTime)
fmt.Printf(" Go version: %s\n", runtime.Version())
fmt.Printf(" OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
```
### Testing (cmd/root_test.go)
```go
package cmd
import (
"bytes"
"context"
"testing"
"time"
)
func TestProcessCommand(t *testing.T) {
// Reset command for testing
processCmd.SetArgs([]string{"file1.txt", "file2.txt"})
// Capture output
buf := new(bytes.Buffer)
processCmd.SetOut(buf)
processCmd.SetErr(buf)
// Execute
err := processCmd.Execute()
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
func TestProcessCommandWithContext(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
processCmd.SetContext(ctx)
processCmd.SetArgs([]string{"file1.txt"})
err := processCmd.Execute()
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
func TestProcessCommandCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
processCmd.SetContext(ctx)
processCmd.SetArgs([]string{"file1.txt", "file2.txt"})
// Cancel context immediately
cancel()
err := processCmd.Execute()
if err == nil {
t.Error("Expected context cancellation error")
}
}
func TestConfigViewCommand(t *testing.T) {
configViewCmd.SetArgs([]string{})
buf := new(bytes.Buffer)
configViewCmd.SetOut(buf)
err := configViewCmd.Execute()
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
output := buf.String()
if output == "" {
t.Error("Expected output, got empty string")
}
}
```
### Configuration File (.myapp.yaml)
```yaml
# Application configuration
verbose: false
log-level: info
timeout: 30s
# Custom settings
api:
endpoint: https://api.example.com
timeout: 10s
retries: 3
database:
host: localhost
port: 5432
name: myapp
features:
experimental: false
beta: true
```
### Makefile
```makefile
VERSION := $(shell git describe --tags --always --dirty)
COMMIT := $(shell git rev-parse HEAD)
BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
LDFLAGS := -X github.com/example/myapp/cmd.Version=$(VERSION) \
-X github.com/example/myapp/cmd.Commit=$(COMMIT) \
-X github.com/example/myapp/cmd.BuildTime=$(BUILD_TIME)
.PHONY: build
build:
go build -ldflags "$(LDFLAGS)" -o myapp
.PHONY: test
test:
go test -v ./...
.PHONY: coverage
coverage:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
.PHONY: lint
lint:
golangci-lint run
.PHONY: install
install:
go install -ldflags "$(LDFLAGS)"
.PHONY: clean
clean:
rm -f myapp coverage.out
```
## Usage Examples
```bash
# Basic usage with verbose logging
myapp process file.txt -v
# With timeout and workers
myapp process *.txt --timeout 30s --workers 8
# Dry run
myapp process file.txt --dry-run
# Custom config file
myapp --config prod.yaml process file.txt
# View configuration
myapp config view
# Set configuration
myapp config set api.timeout 15s
# Version information
myapp version
# Shell completion
myapp completion bash > /etc/bash_completion.d/myapp
```
## Key Patterns
1. **Context Awareness**: All long-running operations respect context cancellation
2. **Structured Logging**: Use zap for performance and structure
3. **Configuration Management**: Viper for flexible config handling
4. **Error Wrapping**: Use fmt.Errorf with %w for error chains
5. **Testing**: Comprehensive unit and integration tests
6. **Build Info**: Version, commit, and build time injection
This example provides a complete production-ready CLI that you can use as a foundation for your own applications.

View File

@@ -0,0 +1,381 @@
# Simple CLI - Basic Example
A minimal example for building a simple single-command CLI with Cobra.
## Use Case
Perfect for:
- Quick utility tools
- Single-purpose commands
- Personal automation scripts
- Simple wrappers around existing tools
## Complete Example
### main.go
```go
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var (
// Flags
input string
output string
verbose bool
force bool
)
var rootCmd = &cobra.Command{
Use: "mytool [file]",
Short: "A simple utility tool",
Long: `A simple command-line utility that processes files.
This tool demonstrates a basic Cobra CLI with:
- Flag management
- Argument validation
- Error handling
- Help generation`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
filename := args[0]
if verbose {
fmt.Printf("Processing file: %s\n", filename)
fmt.Printf(" Input format: %s\n", input)
fmt.Printf(" Output format: %s\n", output)
fmt.Printf(" Force mode: %v\n", force)
}
// Process the file
if err := processFile(filename, input, output, force); err != nil {
return fmt.Errorf("failed to process file: %w", err)
}
fmt.Printf("Successfully processed: %s\n", filename)
return nil
},
}
func init() {
// Define flags
rootCmd.Flags().StringVarP(&input, "input", "i", "text", "input format (text|json|yaml)")
rootCmd.Flags().StringVarP(&output, "output", "o", "text", "output format (text|json|yaml)")
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
rootCmd.Flags().BoolVarP(&force, "force", "f", false, "force overwrite")
// Set version
rootCmd.Version = "1.0.0"
}
func processFile(filename, input, output string, force bool) error {
// Your processing logic here
if verbose {
fmt.Printf("Processing %s: %s -> %s\n", filename, input, output)
}
return nil
}
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
```
## Usage
```bash
# Build
go build -o mytool
# Show help
./mytool --help
# Process file
./mytool data.txt
# With options
./mytool data.txt --input json --output yaml --verbose
# Force mode
./mytool data.txt --force
# Show version
./mytool --version
```
## Key Features
### 1. Single Command Structure
Everything in one file - perfect for simple tools:
- Command definition
- Flag management
- Business logic
- Main function
### 2. Flag Types
```go
// String flags with shorthand
rootCmd.Flags().StringVarP(&input, "input", "i", "text", "input format")
// Boolean flags
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
// Integer flags
var count int
rootCmd.Flags().IntVar(&count, "count", 1, "number of iterations")
// String slice flags
var tags []string
rootCmd.Flags().StringSliceVar(&tags, "tags", []string{}, "list of tags")
```
### 3. Argument Validation
```go
// Exactly one argument
Args: cobra.ExactArgs(1)
// No arguments
Args: cobra.NoArgs
// At least one argument
Args: cobra.MinimumNArgs(1)
// Between 1 and 3 arguments
Args: cobra.RangeArgs(1, 3)
// Any number of arguments
Args: cobra.ArbitraryArgs
```
### 4. Error Handling
```go
RunE: func(cmd *cobra.Command, args []string) error {
// Return errors instead of os.Exit
if err := validate(args); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
if err := process(); err != nil {
return fmt.Errorf("processing failed: %w", err)
}
return nil
}
```
### 5. Auto-Generated Help
Cobra automatically generates help from your command definition:
```bash
$ ./mytool --help
A simple command-line utility that processes files.
This tool demonstrates a basic Cobra CLI with:
- Flag management
- Argument validation
- Error handling
- Help generation
Usage:
mytool [file] [flags]
Flags:
-f, --force force overwrite
-h, --help help for mytool
-i, --input string input format (text|json|yaml) (default "text")
-o, --output string output format (text|json|yaml) (default "text")
-v, --verbose verbose output
--version version for mytool
```
## Enhancements
### Add Configuration File Support
```go
import "github.com/spf13/viper"
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, _ := os.UserHomeDir()
viper.AddConfigPath(home)
viper.SetConfigName(".mytool")
viper.SetConfigType("yaml")
}
viper.AutomaticEnv()
viper.ReadInConfig()
}
```
### Add Dry Run Mode
```go
var dryRun bool
func init() {
rootCmd.Flags().BoolVar(&dryRun, "dry-run", false, "simulate without making changes")
}
func processFile(filename string) error {
if dryRun {
fmt.Printf("DRY RUN: Would process %s\n", filename)
return nil
}
// Actual processing
return nil
}
```
### Add Progress Indication
```go
import "github.com/schollz/progressbar/v3"
func processFile(filename string) error {
bar := progressbar.Default(100)
for i := 0; i < 100; i++ {
// Do work
bar.Add(1)
time.Sleep(10 * time.Millisecond)
}
return nil
}
```
## Testing
```go
package main
import (
"bytes"
"testing"
)
func TestRootCommand(t *testing.T) {
// Reset command for testing
rootCmd.SetArgs([]string{"test.txt", "--verbose"})
// Capture output
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetErr(buf)
// Execute
err := rootCmd.Execute()
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
// Check output
output := buf.String()
if !bytes.Contains([]byte(output), []byte("Processing file")) {
t.Errorf("Expected verbose output, got: %s", output)
}
}
func TestRootCommandRequiresArgument(t *testing.T) {
rootCmd.SetArgs([]string{})
err := rootCmd.Execute()
if err == nil {
t.Error("Expected error when no argument provided")
}
}
func TestFlagParsing(t *testing.T) {
rootCmd.SetArgs([]string{"test.txt", "--input", "json", "--output", "yaml"})
err := rootCmd.Execute()
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
// Verify flags were parsed
if input != "json" {
t.Errorf("Expected input=json, got %s", input)
}
if output != "yaml" {
t.Errorf("Expected output=yaml, got %s", output)
}
}
```
## go.mod
```go
module github.com/example/mytool
go 1.21
require github.com/spf13/cobra v1.8.0
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
```
## Build and Distribution
### Simple Build
```bash
go build -o mytool
```
### Cross-Platform Build
```bash
# Linux
GOOS=linux GOARCH=amd64 go build -o mytool-linux
# macOS
GOOS=darwin GOARCH=amd64 go build -o mytool-macos
# Windows
GOOS=windows GOARCH=amd64 go build -o mytool.exe
```
### With Version Info
```bash
VERSION=$(git describe --tags --always)
go build -ldflags "-X main.version=$VERSION" -o mytool
```
## Best Practices
1. **Keep It Simple**: Single file is fine for simple tools
2. **Use RunE**: Always return errors instead of os.Exit
3. **Provide Defaults**: Set sensible default flag values
4. **Add Examples**: Include usage examples in Long description
5. **Version Info**: Always set a version
6. **Test Thoroughly**: Write tests for command execution and flags
7. **Document Flags**: Provide clear flag descriptions
This example provides a solid foundation for building simple, production-ready CLI tools with Cobra.