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