Initial commit
This commit is contained in:
693
skills/cobra-patterns/SKILL.md
Normal file
693
skills/cobra-patterns/SKILL.md
Normal file
@@ -0,0 +1,693 @@
|
||||
---
|
||||
name: cobra-patterns
|
||||
description: Production-ready Cobra CLI patterns including command structure, flags (local and persistent), nested commands, PreRun/PostRun hooks, argument validation, and initialization patterns used by kubectl and hugo. Use when building Go CLIs, implementing Cobra commands, creating nested command structures, managing flags, validating arguments, or when user mentions Cobra, CLI development, command-line tools, kubectl patterns, or Go CLI frameworks.
|
||||
allowed-tools: Bash, Read, Write, Edit
|
||||
---
|
||||
|
||||
# Cobra Patterns Skill
|
||||
|
||||
Production-ready patterns for building powerful CLI applications with Cobra, following best practices from kubectl, hugo, and other production CLIs.
|
||||
|
||||
## Instructions
|
||||
|
||||
### 1. Choose CLI Structure Pattern
|
||||
|
||||
Select the appropriate CLI structure based on your use case:
|
||||
|
||||
- **simple**: Single command with flags (quick utilities)
|
||||
- **flat**: Root command with subcommands at one level
|
||||
- **nested**: Hierarchical command structure (kubectl-style)
|
||||
- **plugin**: Extensible CLI with plugin support
|
||||
- **hybrid**: Mix of built-in and dynamic commands
|
||||
|
||||
### 2. Generate Cobra CLI Structure
|
||||
|
||||
Use the setup script to scaffold a new Cobra CLI:
|
||||
|
||||
```bash
|
||||
cd /home/gotime2022/.claude/plugins/repos/cli-builder/skills/cobra-patterns
|
||||
./scripts/setup-cobra-cli.sh <cli-name> <structure-type>
|
||||
```
|
||||
|
||||
**Structure types:** `simple`, `flat`, `nested`, `plugin`, `hybrid`
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
./scripts/setup-cobra-cli.sh myctl nested
|
||||
```
|
||||
|
||||
**What This Creates:**
|
||||
- Complete directory structure with cmd/ package
|
||||
- Root command with initialization
|
||||
- Example subcommands
|
||||
- Flag definitions (local and persistent)
|
||||
- Cobra initialization (cobra init pattern)
|
||||
- Go module configuration
|
||||
- Main entry point
|
||||
|
||||
### 3. Command Structure Patterns
|
||||
|
||||
#### Basic Command Structure
|
||||
|
||||
```go
|
||||
var exampleCmd = &cobra.Command{
|
||||
Use: "example [flags]",
|
||||
Short: "Brief description",
|
||||
Long: `Detailed description with examples`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Command logic
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Command with Lifecycle Hooks
|
||||
|
||||
```go
|
||||
var advancedCmd = &cobra.Command{
|
||||
Use: "advanced",
|
||||
Short: "Advanced command with hooks",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// Runs before command execution (inherited by children)
|
||||
},
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
// Runs before command execution (local only)
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Main command logic
|
||||
},
|
||||
PostRun: func(cmd *cobra.Command, args []string) {
|
||||
// Runs after command execution (local only)
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
// Runs after command execution (inherited by children)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Command with Error Handling
|
||||
|
||||
```go
|
||||
var robustCmd = &cobra.Command{
|
||||
Use: "robust",
|
||||
Short: "Command with proper error handling",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Return errors instead of os.Exit
|
||||
if err := validateInput(args); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
if err := executeOperation(); err != nil {
|
||||
return fmt.Errorf("operation failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Flag Management Patterns
|
||||
|
||||
#### Persistent Flags (Global Options)
|
||||
|
||||
```go
|
||||
func init() {
|
||||
// Available to this command and all subcommands
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "log level")
|
||||
}
|
||||
```
|
||||
|
||||
#### Local Flags (Command-Specific)
|
||||
|
||||
```go
|
||||
func init() {
|
||||
// Only available to this specific command
|
||||
createCmd.Flags().StringVarP(&name, "name", "n", "", "resource name")
|
||||
createCmd.Flags().IntVar(&replicas, "replicas", 1, "number of replicas")
|
||||
createCmd.Flags().BoolVar(&dryRun, "dry-run", false, "simulate operation")
|
||||
|
||||
// Mark required flags
|
||||
createCmd.MarkFlagRequired("name")
|
||||
}
|
||||
```
|
||||
|
||||
#### Flag Groups and Validation
|
||||
|
||||
```go
|
||||
func init() {
|
||||
// Mutually exclusive flags (only one allowed)
|
||||
createCmd.MarkFlagsMutuallyExclusive("json", "yaml", "text")
|
||||
|
||||
// Required together (all or none)
|
||||
createCmd.MarkFlagsRequiredTogether("username", "password")
|
||||
|
||||
// At least one required
|
||||
createCmd.MarkFlagsOneRequired("file", "stdin", "url")
|
||||
|
||||
// Custom flag completion
|
||||
createCmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{"json", "yaml", "text"}, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Nested Command Patterns
|
||||
|
||||
#### Root Command Setup
|
||||
|
||||
```go
|
||||
// cmd/root.go
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "myctl",
|
||||
Short: "A production-grade CLI tool",
|
||||
Long: `A complete CLI application built with Cobra.
|
||||
|
||||
This application demonstrates production patterns including
|
||||
nested commands, flag management, and proper error handling.`,
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
|
||||
// Add subcommands
|
||||
rootCmd.AddCommand(getCmd)
|
||||
rootCmd.AddCommand(createCmd)
|
||||
rootCmd.AddCommand(deleteCmd)
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
// Initialize configuration, logging, etc.
|
||||
}
|
||||
```
|
||||
|
||||
#### Subcommand with Children (kubectl-style)
|
||||
|
||||
```go
|
||||
// cmd/create/create.go
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create resources",
|
||||
Long: `Create various types of resources`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add nested subcommands
|
||||
createCmd.AddCommand(createDeploymentCmd)
|
||||
createCmd.AddCommand(createServiceCmd)
|
||||
createCmd.AddCommand(createConfigMapCmd)
|
||||
}
|
||||
|
||||
// cmd/create/deployment.go
|
||||
var createDeploymentCmd = &cobra.Command{
|
||||
Use: "deployment [name]",
|
||||
Short: "Create a deployment",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return createDeployment(args[0])
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Command Groups (Organized Help)
|
||||
|
||||
```go
|
||||
func init() {
|
||||
// Define command groups
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "basic",
|
||||
Title: "Basic Commands:",
|
||||
})
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "management",
|
||||
Title: "Management Commands:",
|
||||
})
|
||||
|
||||
// Assign commands to groups
|
||||
getCmd.GroupID = "basic"
|
||||
createCmd.GroupID = "management"
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Argument Validation Patterns
|
||||
|
||||
```go
|
||||
// No arguments allowed
|
||||
var noArgsCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return listResources()
|
||||
},
|
||||
}
|
||||
|
||||
// Exactly n arguments
|
||||
var exactArgsCmd = &cobra.Command{
|
||||
Use: "get <name>",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return getResource(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
// Range of arguments
|
||||
var rangeArgsCmd = &cobra.Command{
|
||||
Use: "delete <name> [names...]",
|
||||
Args: cobra.RangeArgs(1, 5),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return deleteResources(args)
|
||||
},
|
||||
}
|
||||
|
||||
// Custom validation
|
||||
var customValidationCmd = &cobra.Command{
|
||||
Use: "custom",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("requires at least 1 argument")
|
||||
}
|
||||
for _, arg := range args {
|
||||
if !isValid(arg) {
|
||||
return fmt.Errorf("invalid argument: %s", arg)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return processArgs(args)
|
||||
},
|
||||
}
|
||||
|
||||
// Valid args with completion
|
||||
var validArgsCmd = &cobra.Command{
|
||||
Use: "select <resource>",
|
||||
ValidArgs: []string{"pod", "service", "deployment", "configmap"},
|
||||
Args: cobra.OnlyValidArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return selectResource(args[0])
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Initialization and Configuration Patterns
|
||||
|
||||
#### cobra.OnInitialize Pattern
|
||||
|
||||
```go
|
||||
var (
|
||||
cfgFile string
|
||||
config Config
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register initialization functions
|
||||
cobra.OnInitialize(initConfig, initLogging, initClient)
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
home, err := os.UserHomeDir()
|
||||
cobra.CheckErr(err)
|
||||
|
||||
viper.AddConfigPath(home)
|
||||
viper.SetConfigType("yaml")
|
||||
viper.SetConfigName(".myctl")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
||||
|
||||
func initLogging() {
|
||||
// Setup logging based on flags
|
||||
}
|
||||
|
||||
func initClient() {
|
||||
// Initialize API clients, connections, etc.
|
||||
}
|
||||
```
|
||||
|
||||
#### Viper Integration
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Bind flags to viper
|
||||
rootCmd.PersistentFlags().String("output", "json", "output format")
|
||||
viper.BindPFlag("output", rootCmd.PersistentFlags().Lookup("output"))
|
||||
|
||||
// Set defaults
|
||||
viper.SetDefault("output", "json")
|
||||
viper.SetDefault("timeout", 30)
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
// Access config via viper
|
||||
output := viper.GetString("output")
|
||||
timeout := viper.GetInt("timeout")
|
||||
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Production Patterns
|
||||
|
||||
#### Kubectl-Style Command Structure
|
||||
|
||||
```go
|
||||
// Organize commands by resource type
|
||||
// myctl get pods
|
||||
// myctl create deployment
|
||||
// myctl delete service
|
||||
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Display resources",
|
||||
}
|
||||
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create resources",
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Resource-specific subcommands
|
||||
getCmd.AddCommand(getPodsCmd)
|
||||
getCmd.AddCommand(getServicesCmd)
|
||||
|
||||
createCmd.AddCommand(createDeploymentCmd)
|
||||
createCmd.AddCommand(createServiceCmd)
|
||||
}
|
||||
```
|
||||
|
||||
#### Hugo-Style Plugin Commands
|
||||
|
||||
```go
|
||||
// Support external commands (hugo server, hugo new, etc.)
|
||||
func init() {
|
||||
rootCmd.AddCommand(serverCmd)
|
||||
rootCmd.AddCommand(newCmd)
|
||||
|
||||
// Auto-discover plugin commands
|
||||
discoverPluginCommands(rootCmd)
|
||||
}
|
||||
|
||||
func discoverPluginCommands(root *cobra.Command) {
|
||||
// Look for executables like "myctl-plugin-*"
|
||||
// Add them as dynamic commands
|
||||
}
|
||||
```
|
||||
|
||||
#### Context and Cancellation
|
||||
|
||||
```go
|
||||
var longRunningCmd = &cobra.Command{
|
||||
Use: "process",
|
||||
Short: "Long-running operation",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Respect context cancellation (Ctrl+C)
|
||||
return processWithContext(ctx)
|
||||
},
|
||||
}
|
||||
|
||||
func processWithContext(ctx context.Context) error {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
// Do work
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Validation and Testing
|
||||
|
||||
Use validation scripts to ensure CLI compliance:
|
||||
|
||||
```bash
|
||||
# Validate command structure
|
||||
./scripts/validate-cobra-cli.sh <cli-directory>
|
||||
|
||||
# Test command execution
|
||||
./scripts/test-cobra-commands.sh <cli-binary>
|
||||
|
||||
# Generate shell completions
|
||||
./scripts/generate-completions.sh <cli-binary>
|
||||
```
|
||||
|
||||
**Validation Checks:**
|
||||
- All commands have Use, Short, and Long descriptions
|
||||
- Flags are properly defined and documented
|
||||
- Required flags are marked
|
||||
- Argument validation is implemented
|
||||
- RunE is used for error handling
|
||||
- Commands are organized in logical groups
|
||||
|
||||
### 10. Shell Completion Support
|
||||
|
||||
```go
|
||||
var completionCmd = &cobra.Command{
|
||||
Use: "completion [bash|zsh|fish|powershell]",
|
||||
Short: "Generate completion script",
|
||||
Long: `Generate shell completion script.
|
||||
|
||||
Example usage:
|
||||
# Bash
|
||||
source <(myctl completion bash)
|
||||
|
||||
# Zsh
|
||||
source <(myctl completion zsh)
|
||||
|
||||
# Fish
|
||||
myctl completion fish | source
|
||||
|
||||
# PowerShell
|
||||
myctl completion powershell | Out-String | Invoke-Expression
|
||||
`,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
||||
Args: cobra.ExactArgs(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)
|
||||
default:
|
||||
return fmt.Errorf("unsupported shell: %s", args[0])
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
|
||||
- **setup-cobra-cli.sh**: Scaffold new Cobra CLI with chosen structure
|
||||
- **validate-cobra-cli.sh**: Validate CLI structure and patterns
|
||||
- **test-cobra-commands.sh**: Test all commands and flags
|
||||
- **generate-completions.sh**: Generate shell completion scripts
|
||||
- **add-command.sh**: Add new command to existing CLI
|
||||
- **refactor-flags.sh**: Reorganize flags (local to persistent, etc.)
|
||||
|
||||
## Templates
|
||||
|
||||
### Core Templates
|
||||
- **root.go**: Root command with initialization
|
||||
- **command.go**: Basic command template
|
||||
- **nested-command.go**: Subcommand with children
|
||||
- **main.go**: CLI entry point
|
||||
- **config.go**: Configuration management with Viper
|
||||
|
||||
### Command Templates
|
||||
- **get-command.go**: Read/retrieve operation
|
||||
- **create-command.go**: Create operation with validation
|
||||
- **delete-command.go**: Delete with confirmation
|
||||
- **list-command.go**: List resources with filtering
|
||||
- **update-command.go**: Update with partial modifications
|
||||
|
||||
### Advanced Templates
|
||||
- **plugin-command.go**: Extensible plugin support
|
||||
- **completion-command.go**: Shell completion generation
|
||||
- **version-command.go**: Version information display
|
||||
- **middleware.go**: Command middleware pattern
|
||||
- **context-command.go**: Context-aware command
|
||||
|
||||
### Flag Templates
|
||||
- **persistent-flags.go**: Global flag definitions
|
||||
- **flag-groups.go**: Flag validation groups
|
||||
- **custom-flags.go**: Custom flag types
|
||||
- **viper-flags.go**: Viper-integrated flags
|
||||
|
||||
### Testing Templates
|
||||
- **command_test.go**: Command unit test
|
||||
- **integration_test.go**: CLI integration test
|
||||
- **mock_test.go**: Mock dependencies for testing
|
||||
|
||||
## Examples
|
||||
|
||||
See `examples/` directory for production patterns:
|
||||
- `kubectl-style/`: Kubectl command organization pattern
|
||||
- `hugo-style/`: Hugo plugin architecture pattern
|
||||
- `simple-cli/`: Basic single-level CLI
|
||||
- `nested-cli/`: Multi-level command hierarchy
|
||||
- `production-cli/`: Full production CLI with all features
|
||||
|
||||
Each example includes:
|
||||
- Complete working CLI
|
||||
- Command structure documentation
|
||||
- Flag management examples
|
||||
- Test suite
|
||||
- Shell completion setup
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Command Organization
|
||||
1. One command per file for maintainability
|
||||
2. Group related commands in subdirectories
|
||||
3. Use command groups for organized help output
|
||||
4. Keep root command focused on initialization
|
||||
|
||||
### Flag Management
|
||||
1. Use persistent flags for truly global options
|
||||
2. Mark required flags explicitly
|
||||
3. Provide sensible defaults
|
||||
4. Use flag groups for related options
|
||||
5. Implement custom completion for better UX
|
||||
|
||||
### Error Handling
|
||||
1. Always use RunE instead of Run
|
||||
2. Return wrapped errors with context
|
||||
3. Use cobra.CheckErr() for fatal errors
|
||||
4. Provide helpful error messages with suggestions
|
||||
|
||||
### Code Organization
|
||||
1. Separate command definition from logic
|
||||
2. Keep business logic in separate packages
|
||||
3. Use dependency injection for testability
|
||||
4. Avoid global state where possible
|
||||
|
||||
### Documentation
|
||||
1. Provide both Short and Long descriptions
|
||||
2. Include usage examples in Long description
|
||||
3. Document all flags with clear help text
|
||||
4. Generate and maintain shell completions
|
||||
|
||||
### Testing
|
||||
1. Unit test command functions separately
|
||||
2. Integration test full command execution
|
||||
3. Mock external dependencies
|
||||
4. Test flag validation and argument parsing
|
||||
5. Verify error messages and exit codes
|
||||
|
||||
### Performance
|
||||
1. Use cobra.OnInitialize for lazy loading
|
||||
2. Avoid expensive operations in init()
|
||||
3. Implement context cancellation
|
||||
4. Profile and optimize hot paths
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Creating a New Nested CLI
|
||||
|
||||
```bash
|
||||
# 1. Generate CLI structure
|
||||
./scripts/setup-cobra-cli.sh myctl nested
|
||||
|
||||
# 2. Add commands
|
||||
cd myctl
|
||||
../scripts/add-command.sh get
|
||||
../scripts/add-command.sh create --parent get
|
||||
|
||||
# 3. Validate structure
|
||||
../scripts/validate-cobra-cli.sh .
|
||||
|
||||
# 4. Build and test
|
||||
go build -o myctl
|
||||
./myctl --help
|
||||
```
|
||||
|
||||
### Adding Authentication to CLI
|
||||
|
||||
```bash
|
||||
# Use authentication template
|
||||
cp templates/auth-command.go cmd/login.go
|
||||
|
||||
# Add persistent auth flags
|
||||
cp templates/auth-flags.go cmd/root.go
|
||||
|
||||
# Implement token management
|
||||
# Edit cmd/root.go to add initAuth() to cobra.OnInitialize
|
||||
```
|
||||
|
||||
### Implementing kubectl-Style Resource Commands
|
||||
|
||||
```bash
|
||||
# Generate resource-based structure
|
||||
./scripts/setup-cobra-cli.sh myctl nested
|
||||
|
||||
# Add resource commands (get, create, delete, update)
|
||||
./scripts/add-command.sh get --style kubectl
|
||||
./scripts/add-command.sh create --style kubectl
|
||||
|
||||
# Add resource types as subcommands
|
||||
./scripts/add-command.sh pods --parent get
|
||||
./scripts/add-command.sh services --parent get
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Commands not showing in help**: Ensure AddCommand() is called in init()
|
||||
|
||||
**Flags not recognized**: Check if flag is registered before command execution
|
||||
|
||||
**PersistentFlags not inherited**: Verify parent command has PersistentFlags defined
|
||||
|
||||
**Completion not working**: Run completion command and source output, check ValidArgs
|
||||
|
||||
**Context cancellation ignored**: Ensure you're checking ctx.Done() in long-running operations
|
||||
|
||||
## Integration
|
||||
|
||||
This skill is used by:
|
||||
- CLI generation commands - Scaffolding new CLIs
|
||||
- Code generation agents - Implementing CLI patterns
|
||||
- Testing commands - Validating CLI structure
|
||||
- All Go CLI development workflows
|
||||
|
||||
---
|
||||
|
||||
**Plugin:** cli-builder
|
||||
**Version:** 1.0.0
|
||||
**Category:** Go CLI Development
|
||||
**Skill Type:** Patterns & Templates
|
||||
366
skills/cobra-patterns/examples/kubectl-style-cli.md
Normal file
366
skills/cobra-patterns/examples/kubectl-style-cli.md
Normal 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.
|
||||
538
skills/cobra-patterns/examples/production-cli-complete.md
Normal file
538
skills/cobra-patterns/examples/production-cli-complete.md
Normal 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.
|
||||
381
skills/cobra-patterns/examples/simple-cli-basic.md
Normal file
381
skills/cobra-patterns/examples/simple-cli-basic.md
Normal 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.
|
||||
113
skills/cobra-patterns/scripts/add-command.sh
Executable file
113
skills/cobra-patterns/scripts/add-command.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Add a new command to existing Cobra CLI
|
||||
# Usage: ./add-command.sh <command-name> [--parent parent-command]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
COMMAND_NAME="${1:-}"
|
||||
PARENT_CMD=""
|
||||
|
||||
# Parse arguments
|
||||
shift || true
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--parent)
|
||||
PARENT_CMD="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$COMMAND_NAME" ]; then
|
||||
echo "Error: Command name required"
|
||||
echo "Usage: $0 <command-name> [--parent parent-command]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "cmd" ]; then
|
||||
echo "Error: cmd/ directory not found. Run from CLI root directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine file location
|
||||
if [ -n "$PARENT_CMD" ]; then
|
||||
CMD_DIR="cmd/$PARENT_CMD"
|
||||
mkdir -p "$CMD_DIR"
|
||||
CMD_FILE="$CMD_DIR/$COMMAND_NAME.go"
|
||||
PACKAGE_NAME="$PARENT_CMD"
|
||||
else
|
||||
CMD_FILE="cmd/$COMMAND_NAME.go"
|
||||
PACKAGE_NAME="cmd"
|
||||
fi
|
||||
|
||||
if [ -f "$CMD_FILE" ]; then
|
||||
echo "Error: Command file already exists: $CMD_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create command file
|
||||
cat > "$CMD_FILE" << EOF
|
||||
package $PACKAGE_NAME
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// Add command-specific flags here
|
||||
${COMMAND_NAME}Example string
|
||||
)
|
||||
|
||||
var ${COMMAND_NAME}Cmd = &cobra.Command{
|
||||
Use: "$COMMAND_NAME",
|
||||
Short: "Short description of $COMMAND_NAME",
|
||||
Long: \`Detailed description of the $COMMAND_NAME command.
|
||||
|
||||
This command does something useful. Add more details here.
|
||||
|
||||
Examples:
|
||||
mycli $COMMAND_NAME --example value\`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Executing $COMMAND_NAME command\n")
|
||||
|
||||
// Add command logic here
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Define flags
|
||||
${COMMAND_NAME}Cmd.Flags().StringVar(&${COMMAND_NAME}Example, "example", "", "example flag")
|
||||
|
||||
// Register command
|
||||
EOF
|
||||
|
||||
if [ -n "$PARENT_CMD" ]; then
|
||||
cat >> "$CMD_FILE" << EOF
|
||||
${PARENT_CMD}Cmd.AddCommand(${COMMAND_NAME}Cmd)
|
||||
EOF
|
||||
else
|
||||
cat >> "$CMD_FILE" << EOF
|
||||
rootCmd.AddCommand(${COMMAND_NAME}Cmd)
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat >> "$CMD_FILE" << EOF
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "✓ Created command file: $CMD_FILE"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Update the command logic in $CMD_FILE"
|
||||
echo "2. Add any required flags"
|
||||
echo "3. Build and test: go build"
|
||||
echo ""
|
||||
82
skills/cobra-patterns/scripts/generate-completions.sh
Executable file
82
skills/cobra-patterns/scripts/generate-completions.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Generate shell completion scripts for Cobra CLI
|
||||
# Usage: ./generate-completions.sh <cli-binary> [output-dir]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CLI_BINARY="${1:-}"
|
||||
OUTPUT_DIR="${2:-./completions}"
|
||||
|
||||
if [ -z "$CLI_BINARY" ]; then
|
||||
echo "Error: CLI binary path required"
|
||||
echo "Usage: $0 <cli-binary> [output-dir]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$CLI_BINARY" ]; then
|
||||
echo "Error: Binary not found: $CLI_BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$CLI_BINARY" ]; then
|
||||
echo "Error: Binary is not executable: $CLI_BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
CLI_NAME=$(basename "$CLI_BINARY")
|
||||
|
||||
echo "Generating shell completions for $CLI_NAME..."
|
||||
echo ""
|
||||
|
||||
# Generate Bash completion
|
||||
if "$CLI_BINARY" completion bash > "$OUTPUT_DIR/$CLI_NAME.bash" 2>/dev/null; then
|
||||
echo "✓ Bash completion: $OUTPUT_DIR/$CLI_NAME.bash"
|
||||
echo " Install: source $OUTPUT_DIR/$CLI_NAME.bash"
|
||||
else
|
||||
echo "⚠ Bash completion not available"
|
||||
fi
|
||||
|
||||
# Generate Zsh completion
|
||||
if "$CLI_BINARY" completion zsh > "$OUTPUT_DIR/_$CLI_NAME" 2>/dev/null; then
|
||||
echo "✓ Zsh completion: $OUTPUT_DIR/_$CLI_NAME"
|
||||
echo " Install: Place in \$fpath directory"
|
||||
else
|
||||
echo "⚠ Zsh completion not available"
|
||||
fi
|
||||
|
||||
# Generate Fish completion
|
||||
if "$CLI_BINARY" completion fish > "$OUTPUT_DIR/$CLI_NAME.fish" 2>/dev/null; then
|
||||
echo "✓ Fish completion: $OUTPUT_DIR/$CLI_NAME.fish"
|
||||
echo " Install: source $OUTPUT_DIR/$CLI_NAME.fish"
|
||||
else
|
||||
echo "⚠ Fish completion not available"
|
||||
fi
|
||||
|
||||
# Generate PowerShell completion
|
||||
if "$CLI_BINARY" completion powershell > "$OUTPUT_DIR/$CLI_NAME.ps1" 2>/dev/null; then
|
||||
echo "✓ PowerShell completion: $OUTPUT_DIR/$CLI_NAME.ps1"
|
||||
echo " Install: & $OUTPUT_DIR/$CLI_NAME.ps1"
|
||||
else
|
||||
echo "⚠ PowerShell completion not available"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Completions generated in: $OUTPUT_DIR"
|
||||
echo ""
|
||||
echo "Installation instructions:"
|
||||
echo ""
|
||||
echo "Bash:"
|
||||
echo " echo 'source $OUTPUT_DIR/$CLI_NAME.bash' >> ~/.bashrc"
|
||||
echo ""
|
||||
echo "Zsh:"
|
||||
echo " mkdir -p ~/.zsh/completions"
|
||||
echo " cp $OUTPUT_DIR/_$CLI_NAME ~/.zsh/completions/"
|
||||
echo " Add to ~/.zshrc: fpath=(~/.zsh/completions \$fpath)"
|
||||
echo ""
|
||||
echo "Fish:"
|
||||
echo " mkdir -p ~/.config/fish/completions"
|
||||
echo " cp $OUTPUT_DIR/$CLI_NAME.fish ~/.config/fish/completions/"
|
||||
echo ""
|
||||
566
skills/cobra-patterns/scripts/setup-cobra-cli.sh
Executable file
566
skills/cobra-patterns/scripts/setup-cobra-cli.sh
Executable file
@@ -0,0 +1,566 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Setup Cobra CLI with chosen structure pattern
|
||||
# Usage: ./setup-cobra-cli.sh <cli-name> <structure-type>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CLI_NAME="${1:-}"
|
||||
STRUCTURE_TYPE="${2:-flat}"
|
||||
|
||||
if [ -z "$CLI_NAME" ]; then
|
||||
echo "Error: CLI name required"
|
||||
echo "Usage: $0 <cli-name> <structure-type>"
|
||||
echo "Structure types: simple, flat, nested, plugin, hybrid"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate structure type
|
||||
case "$STRUCTURE_TYPE" in
|
||||
simple|flat|nested|plugin|hybrid)
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid structure type: $STRUCTURE_TYPE"
|
||||
echo "Valid types: simple, flat, nested, plugin, hybrid"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Creating Cobra CLI: $CLI_NAME with $STRUCTURE_TYPE structure..."
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$CLI_NAME"
|
||||
cd "$CLI_NAME"
|
||||
|
||||
# Initialize Go module
|
||||
go mod init "$CLI_NAME" 2>/dev/null || echo "Go module already initialized"
|
||||
|
||||
# Create base directories
|
||||
mkdir -p cmd
|
||||
mkdir -p internal
|
||||
|
||||
# Install Cobra
|
||||
echo "Installing Cobra dependency..."
|
||||
go get -u github.com/spf13/cobra@latest
|
||||
|
||||
case "$STRUCTURE_TYPE" in
|
||||
simple)
|
||||
# Single command CLI
|
||||
cat > main.go << 'EOF'
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
verbose bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "CLI_NAME",
|
||||
Short: "A simple CLI tool",
|
||||
Long: `A simple command-line tool built with Cobra.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if verbose {
|
||||
fmt.Println("Running in verbose mode")
|
||||
}
|
||||
fmt.Println("Hello from CLI_NAME!")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
EOF
|
||||
sed -i "s/CLI_NAME/$CLI_NAME/g" main.go
|
||||
;;
|
||||
|
||||
flat)
|
||||
# Root with subcommands at one level
|
||||
cat > cmd/root.go << 'EOF'
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
verbose bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "CLI_NAME",
|
||||
Short: "A CLI tool with flat command structure",
|
||||
Long: `A command-line tool with subcommands at a single level.`,
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > cmd/get.go << 'EOF'
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get [resource]",
|
||||
Short: "Get resources",
|
||||
Long: `Retrieve and display resources`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Getting resource: %s\n", args[0])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(getCmd)
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > cmd/create.go << 'EOF'
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
createName string
|
||||
)
|
||||
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create resources",
|
||||
Long: `Create new resources`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if createName == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
fmt.Printf("Creating resource: %s\n", createName)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
createCmd.Flags().StringVarP(&createName, "name", "n", "", "resource name (required)")
|
||||
createCmd.MarkFlagRequired("name")
|
||||
rootCmd.AddCommand(createCmd)
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > main.go << 'EOF'
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"CLI_NAME/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
EOF
|
||||
sed -i "s/CLI_NAME/$CLI_NAME/g" main.go cmd/root.go
|
||||
;;
|
||||
|
||||
nested)
|
||||
# kubectl-style nested commands
|
||||
mkdir -p cmd/get cmd/create cmd/delete
|
||||
|
||||
cat > cmd/root.go << 'EOF'
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
verbose bool
|
||||
output string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "CLI_NAME",
|
||||
Short: "A production-grade CLI tool",
|
||||
Long: `A complete CLI application with nested command structure.
|
||||
|
||||
This CLI demonstrates kubectl-style command organization with
|
||||
hierarchical commands and consistent flag handling.`,
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
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:",
|
||||
})
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if verbose {
|
||||
fmt.Fprintln(os.Stderr, "Verbose mode enabled")
|
||||
}
|
||||
if cfgFile != "" {
|
||||
fmt.Fprintf(os.Stderr, "Using config file: %s\n", cfgFile)
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > cmd/get/get.go << 'EOF'
|
||||
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() {
|
||||
GetCmd.AddCommand(podsCmd)
|
||||
GetCmd.AddCommand(servicesCmd)
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > cmd/get/pods.go << 'EOF'
|
||||
package get
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
namespace string
|
||||
allNamespaces bool
|
||||
)
|
||||
|
||||
var podsCmd = &cobra.Command{
|
||||
Use: "pods [NAME]",
|
||||
Short: "Display pods",
|
||||
Long: `Display one or many pods`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if allNamespaces {
|
||||
fmt.Println("Listing pods in all namespaces")
|
||||
} else {
|
||||
fmt.Printf("Listing pods in namespace: %s\n", namespace)
|
||||
}
|
||||
if len(args) > 0 {
|
||||
fmt.Printf("Showing pod: %s\n", args[0])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
podsCmd.Flags().StringVarP(&namespace, "namespace", "n", "default", "namespace")
|
||||
podsCmd.Flags().BoolVarP(&allNamespaces, "all-namespaces", "A", false, "list across all namespaces")
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > cmd/get/services.go << 'EOF'
|
||||
package get
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var servicesCmd = &cobra.Command{
|
||||
Use: "services [NAME]",
|
||||
Short: "Display services",
|
||||
Long: `Display one or many services`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("Listing services")
|
||||
if len(args) > 0 {
|
||||
fmt.Printf("Showing service: %s\n", args[0])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > cmd/create/create.go << 'EOF'
|
||||
package create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var CreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create resources",
|
||||
Long: `Create resources from files or stdin`,
|
||||
GroupID: "management",
|
||||
}
|
||||
|
||||
func init() {
|
||||
CreateCmd.AddCommand(deploymentCmd)
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > cmd/create/deployment.go << 'EOF'
|
||||
package create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
image string
|
||||
replicas int
|
||||
)
|
||||
|
||||
var deploymentCmd = &cobra.Command{
|
||||
Use: "deployment NAME",
|
||||
Short: "Create a deployment",
|
||||
Long: `Create a deployment with the specified name`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
fmt.Printf("Creating deployment: %s\n", name)
|
||||
fmt.Printf(" Image: %s\n", image)
|
||||
fmt.Printf(" Replicas: %d\n", replicas)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
deploymentCmd.Flags().StringVar(&image, "image", "", "container image (required)")
|
||||
deploymentCmd.Flags().IntVar(&replicas, "replicas", 1, "number of replicas")
|
||||
deploymentCmd.MarkFlagRequired("image")
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > cmd/delete/delete.go << 'EOF'
|
||||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
force bool
|
||||
)
|
||||
|
||||
var DeleteCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete resources",
|
||||
Long: `Delete resources by names, stdin, or resources`,
|
||||
GroupID: "management",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
for _, resource := range args {
|
||||
if force {
|
||||
fmt.Printf("Force deleting: %s\n", resource)
|
||||
} else {
|
||||
fmt.Printf("Deleting: %s\n", resource)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
DeleteCmd.Flags().BoolVarP(&force, "force", "f", false, "force deletion")
|
||||
}
|
||||
EOF
|
||||
|
||||
# Update root to add nested commands
|
||||
cat >> cmd/root.go << 'EOF'
|
||||
|
||||
func init() {
|
||||
// Add command imports at the top of your root.go:
|
||||
// import (
|
||||
// "CLI_NAME/cmd/get"
|
||||
// "CLI_NAME/cmd/create"
|
||||
// "CLI_NAME/cmd/delete"
|
||||
// )
|
||||
|
||||
// Uncomment after fixing imports:
|
||||
// rootCmd.AddCommand(get.GetCmd)
|
||||
// rootCmd.AddCommand(create.CreateCmd)
|
||||
// rootCmd.AddCommand(delete.DeleteCmd)
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > main.go << 'EOF'
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"CLI_NAME/cmd"
|
||||
_ "CLI_NAME/cmd/get"
|
||||
_ "CLI_NAME/cmd/create"
|
||||
_ "CLI_NAME/cmd/delete"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
EOF
|
||||
sed -i "s/CLI_NAME/$CLI_NAME/g" main.go cmd/root.go
|
||||
;;
|
||||
|
||||
plugin)
|
||||
echo "Plugin structure not yet implemented"
|
||||
exit 1
|
||||
;;
|
||||
|
||||
hybrid)
|
||||
echo "Hybrid structure not yet implemented"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create .gitignore
|
||||
cat > .gitignore << 'EOF'
|
||||
# Binaries
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/CLI_NAME
|
||||
|
||||
# Test binary
|
||||
*.test
|
||||
|
||||
# Coverage
|
||||
*.out
|
||||
*.prof
|
||||
|
||||
# Go workspace
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
EOF
|
||||
sed -i "s/CLI_NAME/$CLI_NAME/g" .gitignore
|
||||
|
||||
# Create README
|
||||
cat > README.md << 'EOF'
|
||||
# CLI_NAME
|
||||
|
||||
A CLI tool built with Cobra.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
CLI_NAME --help
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Build:
|
||||
```bash
|
||||
go build -o CLI_NAME
|
||||
```
|
||||
|
||||
Run:
|
||||
```bash
|
||||
./CLI_NAME
|
||||
```
|
||||
|
||||
Test:
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
This CLI uses STRUCTURE_TYPE command structure.
|
||||
EOF
|
||||
sed -i "s/CLI_NAME/$CLI_NAME/g" README.md
|
||||
sed -i "s/STRUCTURE_TYPE/$STRUCTURE_TYPE/g" README.md
|
||||
|
||||
# Initialize dependencies
|
||||
echo "Downloading dependencies..."
|
||||
go mod tidy
|
||||
|
||||
echo ""
|
||||
echo "✓ CLI created successfully: $CLI_NAME"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " cd $CLI_NAME"
|
||||
echo " go build -o $CLI_NAME"
|
||||
echo " ./$CLI_NAME --help"
|
||||
echo ""
|
||||
181
skills/cobra-patterns/scripts/validate-cobra-cli.sh
Executable file
181
skills/cobra-patterns/scripts/validate-cobra-cli.sh
Executable file
@@ -0,0 +1,181 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Validate Cobra CLI structure and patterns
|
||||
# Usage: ./validate-cobra-cli.sh <cli-directory>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CLI_DIR="${1:-.}"
|
||||
|
||||
if [ ! -d "$CLI_DIR" ]; then
|
||||
echo "Error: Directory not found: $CLI_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Validating Cobra CLI structure in: $CLI_DIR"
|
||||
echo ""
|
||||
|
||||
ERRORS=0
|
||||
WARNINGS=0
|
||||
|
||||
# Check Go module
|
||||
if [ ! -f "$CLI_DIR/go.mod" ]; then
|
||||
echo "❌ ERROR: go.mod not found"
|
||||
((ERRORS++))
|
||||
else
|
||||
echo "✓ go.mod found"
|
||||
fi
|
||||
|
||||
# Check main.go
|
||||
if [ ! -f "$CLI_DIR/main.go" ]; then
|
||||
echo "❌ ERROR: main.go not found"
|
||||
((ERRORS++))
|
||||
else
|
||||
echo "✓ main.go found"
|
||||
|
||||
# Check if main.go has proper structure
|
||||
if ! grep -q "func main()" "$CLI_DIR/main.go"; then
|
||||
echo "❌ ERROR: main() function not found in main.go"
|
||||
((ERRORS++))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check cmd directory
|
||||
if [ ! -d "$CLI_DIR/cmd" ]; then
|
||||
echo "⚠ WARNING: cmd/ directory not found (acceptable for simple CLIs)"
|
||||
((WARNINGS++))
|
||||
else
|
||||
echo "✓ cmd/ directory found"
|
||||
|
||||
# Check root command
|
||||
if [ -f "$CLI_DIR/cmd/root.go" ]; then
|
||||
echo "✓ cmd/root.go found"
|
||||
|
||||
# Validate root command structure
|
||||
if ! grep -q "var rootCmd" "$CLI_DIR/cmd/root.go"; then
|
||||
echo "❌ ERROR: rootCmd variable not found in root.go"
|
||||
((ERRORS++))
|
||||
fi
|
||||
|
||||
if ! grep -q "func Execute()" "$CLI_DIR/cmd/root.go"; then
|
||||
echo "❌ ERROR: Execute() function not found in root.go"
|
||||
((ERRORS++))
|
||||
fi
|
||||
else
|
||||
echo "⚠ WARNING: cmd/root.go not found"
|
||||
((WARNINGS++))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for Cobra dependency
|
||||
if [ -f "$CLI_DIR/go.mod" ]; then
|
||||
if ! grep -q "github.com/spf13/cobra" "$CLI_DIR/go.mod"; then
|
||||
echo "❌ ERROR: Cobra dependency not found in go.mod"
|
||||
((ERRORS++))
|
||||
else
|
||||
echo "✓ Cobra dependency found"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate command files have proper structure
|
||||
if [ -d "$CLI_DIR/cmd" ]; then
|
||||
for cmd_file in "$CLI_DIR/cmd"/*.go; do
|
||||
if [ -f "$cmd_file" ]; then
|
||||
filename=$(basename "$cmd_file")
|
||||
|
||||
# Check for command variable
|
||||
if grep -q "var.*Cmd = &cobra.Command" "$cmd_file"; then
|
||||
echo "✓ Command structure found in $filename"
|
||||
|
||||
# Check for Use field
|
||||
if ! grep -A5 "var.*Cmd = &cobra.Command" "$cmd_file" | grep -q "Use:"; then
|
||||
echo "⚠ WARNING: Use field missing in $filename"
|
||||
((WARNINGS++))
|
||||
fi
|
||||
|
||||
# Check for Short description
|
||||
if ! grep -A10 "var.*Cmd = &cobra.Command" "$cmd_file" | grep -q "Short:"; then
|
||||
echo "⚠ WARNING: Short description missing in $filename"
|
||||
((WARNINGS++))
|
||||
fi
|
||||
|
||||
# Check for Run or RunE
|
||||
if ! grep -A15 "var.*Cmd = &cobra.Command" "$cmd_file" | grep -qE "Run:|RunE:"; then
|
||||
echo "⚠ WARNING: Run/RunE function missing in $filename"
|
||||
((WARNINGS++))
|
||||
fi
|
||||
|
||||
# Prefer RunE over Run for error handling
|
||||
if grep -A15 "var.*Cmd = &cobra.Command" "$cmd_file" | grep -q "Run:" && \
|
||||
! grep -A15 "var.*Cmd = &cobra.Command" "$cmd_file" | grep -q "RunE:"; then
|
||||
echo "⚠ WARNING: Consider using RunE instead of Run in $filename for better error handling"
|
||||
((WARNINGS++))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check for .gitignore
|
||||
if [ ! -f "$CLI_DIR/.gitignore" ]; then
|
||||
echo "⚠ WARNING: .gitignore not found"
|
||||
((WARNINGS++))
|
||||
else
|
||||
echo "✓ .gitignore found"
|
||||
fi
|
||||
|
||||
# Check for README
|
||||
if [ ! -f "$CLI_DIR/README.md" ]; then
|
||||
echo "⚠ WARNING: README.md not found"
|
||||
((WARNINGS++))
|
||||
else
|
||||
echo "✓ README.md found"
|
||||
fi
|
||||
|
||||
# Check if Go code compiles
|
||||
echo ""
|
||||
echo "Checking if code compiles..."
|
||||
cd "$CLI_DIR"
|
||||
if go build -o /tmp/cobra-cli-test 2>&1 | head -20; then
|
||||
echo "✓ Code compiles successfully"
|
||||
rm -f /tmp/cobra-cli-test
|
||||
else
|
||||
echo "❌ ERROR: Code does not compile"
|
||||
((ERRORS++))
|
||||
fi
|
||||
|
||||
# Check for common anti-patterns
|
||||
echo ""
|
||||
echo "Checking for anti-patterns..."
|
||||
|
||||
# Check for os.Exit in command handlers
|
||||
if grep -r "os.Exit" "$CLI_DIR/cmd" 2>/dev/null | grep -v "import" | grep -v "//"; then
|
||||
echo "⚠ WARNING: Found os.Exit() in command handlers - prefer returning errors"
|
||||
((WARNINGS++))
|
||||
fi
|
||||
|
||||
# Check for panic in command handlers
|
||||
if grep -r "panic(" "$CLI_DIR/cmd" 2>/dev/null | grep -v "import" | grep -v "//"; then
|
||||
echo "⚠ WARNING: Found panic() in command handlers - prefer returning errors"
|
||||
((WARNINGS++))
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "Validation Summary"
|
||||
echo "================================"
|
||||
echo "Errors: $ERRORS"
|
||||
echo "Warnings: $WARNINGS"
|
||||
echo ""
|
||||
|
||||
if [ $ERRORS -eq 0 ]; then
|
||||
echo "✓ Validation passed!"
|
||||
if [ $WARNINGS -gt 0 ]; then
|
||||
echo " ($WARNINGS warnings to review)"
|
||||
fi
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Validation failed with $ERRORS errors"
|
||||
exit 1
|
||||
fi
|
||||
71
skills/cobra-patterns/templates/command.go.template
Normal file
71
skills/cobra-patterns/templates/command.go.template
Normal file
@@ -0,0 +1,71 @@
|
||||
package {{.PackageName}}
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// Command-specific flags
|
||||
{{.CommandName}}Name string
|
||||
{{.CommandName}}Force bool
|
||||
{{.CommandName}}DryRun bool
|
||||
)
|
||||
|
||||
// {{.CommandName}}Cmd represents the {{.CommandName}} command
|
||||
var {{.CommandName}}Cmd = &cobra.Command{
|
||||
Use: "{{.CommandName}} [flags]",
|
||||
Short: "{{.ShortDescription}}",
|
||||
Long: `{{.LongDescription}}
|
||||
|
||||
This command provides {{.CommandName}} functionality with proper
|
||||
error handling and validation.
|
||||
|
||||
Examples:
|
||||
{{.CLIName}} {{.CommandName}} --name example
|
||||
{{.CLIName}} {{.CommandName}} --force
|
||||
{{.CLIName}} {{.CommandName}} --dry-run`,
|
||||
Args: cobra.NoArgs,
|
||||
GroupID: "{{.GroupID}}",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Validate required flags
|
||||
if {{.CommandName}}Name == "" {
|
||||
return fmt.Errorf("--name is required")
|
||||
}
|
||||
|
||||
// Check dry-run mode
|
||||
if {{.CommandName}}DryRun {
|
||||
fmt.Printf("DRY RUN: Would execute {{.CommandName}} with name: %s\n", {{.CommandName}}Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute command logic
|
||||
if cmd.Root().PersistentFlags().Lookup("verbose").Changed {
|
||||
fmt.Printf("Executing {{.CommandName}} in verbose mode...\n")
|
||||
}
|
||||
|
||||
if err := execute{{.CommandName}}({{.CommandName}}Name, {{.CommandName}}Force); err != nil {
|
||||
return fmt.Errorf("{{.CommandName}} failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully executed {{.CommandName}}: %s\n", {{.CommandName}}Name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Define flags
|
||||
{{.CommandName}}Cmd.Flags().StringVarP(&{{.CommandName}}Name, "name", "n", "", "resource name (required)")
|
||||
{{.CommandName}}Cmd.Flags().BoolVarP(&{{.CommandName}}Force, "force", "f", false, "force operation")
|
||||
{{.CommandName}}Cmd.Flags().BoolVar(&{{.CommandName}}DryRun, "dry-run", false, "simulate operation without making changes")
|
||||
|
||||
// Mark required flags
|
||||
{{.CommandName}}Cmd.MarkFlagRequired("name")
|
||||
}
|
||||
|
||||
// execute{{.CommandName}} performs the actual operation
|
||||
func execute{{.CommandName}}(name string, force bool) error {
|
||||
// Implementation goes here
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// completionCmd represents the completion command
|
||||
var completionCmd = &cobra.Command{
|
||||
Use: "completion [bash|zsh|fish|powershell]",
|
||||
Short: "Generate completion script",
|
||||
Long: `Generate shell completion script for {{.CLIName}}.
|
||||
|
||||
The completion script must be evaluated to provide interactive
|
||||
completion. This can be done by sourcing it from your shell profile.
|
||||
|
||||
Bash:
|
||||
source <({{.CLIName}} completion bash)
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
# Linux:
|
||||
{{.CLIName}} completion bash > /etc/bash_completion.d/{{.CLIName}}
|
||||
# macOS:
|
||||
{{.CLIName}} completion bash > /usr/local/etc/bash_completion.d/{{.CLIName}}
|
||||
|
||||
Zsh:
|
||||
# If shell completion is not already enabled, enable it:
|
||||
echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
{{.CLIName}} completion zsh > "${fpath[1]}/_{{.CLIName}}"
|
||||
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
|
||||
Fish:
|
||||
{{.CLIName}} completion fish | source
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
{{.CLIName}} completion fish > ~/.config/fish/completions/{{.CLIName}}.fish
|
||||
|
||||
PowerShell:
|
||||
{{.CLIName}} completion powershell | Out-String | Invoke-Expression
|
||||
|
||||
# To load completions for every new session:
|
||||
{{.CLIName}} completion powershell > {{.CLIName}}.ps1
|
||||
# and source this file from your PowerShell profile.
|
||||
`,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
||||
Args: cobra.ExactArgs(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)
|
||||
default:
|
||||
return fmt.Errorf("unsupported shell type: %s", args[0])
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(completionCmd)
|
||||
}
|
||||
13
skills/cobra-patterns/templates/main.go.template
Normal file
13
skills/cobra-patterns/templates/main.go.template
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"{{.ModulePath}}/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
20
skills/cobra-patterns/templates/nested-command.go.template
Normal file
20
skills/cobra-patterns/templates/nested-command.go.template
Normal file
@@ -0,0 +1,20 @@
|
||||
package {{.PackageName}}
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// {{.CommandName}}Cmd represents the {{.CommandName}} parent command
|
||||
var {{.CommandName}}Cmd = &cobra.Command{
|
||||
Use: "{{.CommandName}}",
|
||||
Short: "{{.ShortDescription}}",
|
||||
Long: `{{.LongDescription}}`,
|
||||
GroupID: "{{.GroupID}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add subcommands here
|
||||
// {{.CommandName}}Cmd.AddCommand({{.CommandName}}ListCmd)
|
||||
// {{.CommandName}}Cmd.AddCommand({{.CommandName}}CreateCmd)
|
||||
// {{.CommandName}}Cmd.AddCommand({{.CommandName}}DeleteCmd)
|
||||
}
|
||||
95
skills/cobra-patterns/templates/root.go.template
Normal file
95
skills/cobra-patterns/templates/root.go.template
Normal file
@@ -0,0 +1,95 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
verbose bool
|
||||
output string
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "{{.CLIName}}",
|
||||
Short: "{{.ShortDescription}}",
|
||||
Long: `{{.LongDescription}}
|
||||
|
||||
This is a production-grade CLI application built with Cobra.
|
||||
It provides a complete command-line interface with proper error
|
||||
handling, configuration management, and extensibility.`,
|
||||
Version: "{{.Version}}",
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{.CLIName}}.yaml)")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "text", "output format (text|json|yaml)")
|
||||
|
||||
// Bind flags to viper
|
||||
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
|
||||
viper.BindPFlag("output", rootCmd.PersistentFlags().Lookup("output"))
|
||||
|
||||
// Command groups for organized help
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "basic",
|
||||
Title: "Basic Commands:",
|
||||
})
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "management",
|
||||
Title: "Management Commands:",
|
||||
})
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "other",
|
||||
Title: "Other Commands:",
|
||||
})
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Find home directory.
|
||||
home, err := os.UserHomeDir()
|
||||
cobra.CheckErr(err)
|
||||
|
||||
// Search config in home directory with name ".{{.CLIName}}" (without extension).
|
||||
viper.AddConfigPath(home)
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.SetConfigName(".{{.CLIName}}")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
if err := viper.ReadInConfig(); err == nil && verbose {
|
||||
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
||||
|
||||
// GetVerbose returns whether verbose mode is enabled
|
||||
func GetVerbose() bool {
|
||||
return viper.GetBool("verbose")
|
||||
}
|
||||
|
||||
// GetOutput returns the output format
|
||||
func GetOutput() string {
|
||||
return viper.GetString("output")
|
||||
}
|
||||
Reference in New Issue
Block a user