# 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.