| name | cobra-modularity |
| description | Build modular CLI applications with Cobra framework. Use when structuring CLI commands, implementing modular command architecture, handling flags and arguments, or when user mentions Cobra, CLI modularity, command registration, or spf13/cobra. |
Cobra Modular CLI Architecture
Build scalable, maintainable CLI applications using Cobra with modular command registration patterns.
Your Role: CLI Architect
You help structure CLI applications with clean, modular architecture. You:
✅ Design modular command structure - Self-contained command modules
✅ Implement Register pattern - Commands register themselves
✅ Handle flags properly - Persistent vs local, parsing, validation
✅ Structure subcommands - Nested command hierarchies
✅ Apply Cobra idioms - RunE for errors, PreRun hooks, etc.
✅ Follow project patterns - Use existing module conventions
❌ Do NOT centralize commands - Keep modules self-contained
❌ Do NOT modify root unnecessarily - Add via registration only
❌ Do NOT ignore errors - Use RunE, not Run
Core Principles
1. Modular Command Registration
Commands live in their own packages and register with parent commands.
✅ GOOD - Self-registering module:
package demo
import "github.com/spf13/cobra"
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "demo",
Short: "Demo commands",
}
parent.AddCommand(cmd)
spinner.Register(cmd)
list.Register(cmd)
}
❌ BAD - Centralized registration:
func init() {
RootCmd.AddCommand(demoCmd)
RootCmd.AddCommand(listCmd)
RootCmd.AddCommand(spinnerCmd)
}
2. Command Structure Pattern
Standard module layout:
modules/demo/
├── cmd.go # Register function
├── spinner/
│ ├── cmd.go # spinner.Register()
│ └── spinner.go # Implementation
└── list/
├── cmd.go # list.Register()
└── list.go # Implementation
Benefits:
- Module is self-contained
- Easy to add/remove commands
- No changes to root when adding modules
- Testable in isolation
3. Error Handling
Use RunE (not Run):
✅ GOOD:
cmd := &cobra.Command{
Use: "fetch",
Short: "Fetch data",
RunE: run,
}
func run(cmd *cobra.Command, args []string) error {
if err := doWork(); err != nil {
return fmt.Errorf("fetch failed: %w", err)
}
return nil
}
❌ BAD:
cmd := &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
doWork()
},
}
Cobra Command Structure
Basic Command
package mycommand
import (
"github.com/spf13/cobra"
)
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "mycommand",
Short: "Short description",
Long: `Long description with examples`,
RunE: run,
}
parent.AddCommand(cmd)
}
func run(cmd *cobra.Command, args []string) error {
return nil
}
With Flags
Local flags (command-specific):
var (
flagName string
flagCount int
flagVerbose bool
)
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "process",
Short: "Process data",
RunE: run,
}
cmd.Flags().StringVarP(&flagName, "name", "n", "", "Name (required)")
cmd.MarkFlagRequired("name")
cmd.Flags().IntVarP(&flagCount, "count", "c", 10, "Count")
cmd.Flags().BoolVarP(&flagVerbose, "verbose", "v", false, "Verbose output")
parent.AddCommand(cmd)
}
func run(cmd *cobra.Command, args []string) error {
return nil
}
Persistent flags (inherited by subcommands):
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "server",
Short: "Server commands",
}
cmd.PersistentFlags().StringP("config", "c", "", "Config file")
parent.AddCommand(cmd)
start.Register(cmd)
stop.Register(cmd)
}
With Arguments
Exact args:
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete item by ID",
Args: cobra.ExactArgs(1),
RunE: run,
}
func run(cmd *cobra.Command, args []string) error {
id := args[0]
return deleteItem(id)
}
Range of args:
Args: cobra.RangeArgs(1, 3),
Args: cobra.MinimumNArgs(1),
Args: cobra.MaximumNArgs(2),
Custom validation:
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("requires exactly one arg")
}
if !isValidID(args[0]) {
return fmt.Errorf("invalid ID: %s", args[0])
}
return nil
},
Module Patterns
Demo Module Pattern (CLY)
Parent command - modules/demo/cmd.go:
package demo
import (
"github.com/spf13/cobra"
spinner "github.com/yurifrl/cly/modules/demo/spinner"
)
var DemoCmd = &cobra.Command{
Use: "demo",
Short: "Demo TUI components",
}
func Register(parent *cobra.Command) {
parent.AddCommand(DemoCmd)
}
func init() {
spinner.Register(DemoCmd)
}
Subcommand - modules/demo/spinner/cmd.go:
package spinner
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
)
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "spinner",
Short: "Spinner demo",
RunE: run,
}
parent.AddCommand(cmd)
}
func run(cmd *cobra.Command, args []string) error {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
return err
}
return nil
}
Utility Module Pattern (CLY)
Standalone command - modules/uuid/cmd.go:
package uuid
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
)
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "uuid",
Short: "Generate UUIDs",
Long: "Interactive UUID generator with history",
RunE: run,
}
parent.AddCommand(cmd)
}
func run(cmd *cobra.Command, args []string) error {
p := tea.NewProgram(initialModel())
_, err := p.Run()
return err
}
Registered in root - cmd/root.go:
import (
"github.com/yurifrl/cly/modules/uuid"
"github.com/yurifrl/cly/modules/demo"
)
func init() {
uuid.Register(RootCmd)
demo.Register(RootCmd)
}
Advanced Patterns
PreRun Hooks
Validate before run:
cmd := &cobra.Command{
Use: "deploy",
PreRunE: func(cmd *cobra.Command, args []string) error {
if !fileExists(configFile) {
return fmt.Errorf("config not found: %s", configFile)
}
return nil
},
RunE: run,
}
Setup before run:
PreRunE: func(cmd *cobra.Command, args []string) error {
db, err = connectDB()
return err
},
PostRun Hooks
Cleanup after run:
PostRun: func(cmd *cobra.Command, args []string) {
db.Close()
tempFile.Remove()
},
Persistent PreRun (Inherited)
cmd := &cobra.Command{
Use: "api",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return loadConfig()
},
}
Flag Dependencies
Require flag if another present:
cmd.Flags().String("format", "", "Output format")
cmd.Flags().String("output", "", "Output file")
cmd.MarkFlagsRequiredTogether("format", "output")
Mutually exclusive flags:
cmd.Flags().Bool("json", false, "JSON output")
cmd.Flags().Bool("yaml", false, "YAML output")
cmd.MarkFlagsMutuallyExclusive("json", "yaml")
Subcommand Groups
parent := &cobra.Command{
Use: "api",
Short: "API commands",
}
parent.AddGroup(&cobra.Group{
ID: "server",
Title: "Server Commands:",
})
cmd1 := &cobra.Command{
Use: "start",
GroupID: "server",
}
cmd2 := &cobra.Command{
Use: "stop",
GroupID: "server",
}
parent.AddCommand(cmd1, cmd2)
Configuration Integration
With Viper
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "server",
Short: "Run server",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initConfig()
},
RunE: run,
}
cmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Config file")
viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config"))
parent.AddCommand(cmd)
}
func initConfig() error {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME/.myapp")
}
viper.AutomaticEnv()
return viper.ReadInConfig()
}
func run(cmd *cobra.Command, args []string) error {
port := viper.GetInt("server.port")
return nil
}
Best Practices
1. Keep Commands Focused
One responsibility per command:
cly uuid # Generate UUIDs
cly demo spinner # Show spinner demo
cly utils # Does everything
2. Use Meaningful Names
Use: "generate",
Use: "list-users",
Use: "deploy-app",
Use: "do",
Use: "run",
Use: "execute",
3. Provide Good Help
cmd := &cobra.Command{
Use: "deploy <environment>",
Short: "Deploy application",
Long: `Deploy the application to specified environment.
Environments: dev, staging, prod
Examples:
cly deploy dev
cly deploy prod --version v1.2.3`,
Example: ` cly deploy dev
cly deploy prod --version v1.2.3`,
}
4. Validate Early
PreRunE: func(cmd *cobra.Command, args []string) error {
if port < 1 || port > 65535 {
return fmt.Errorf("invalid port: %d", port)
}
if !commandExists("docker") {
return fmt.Errorf("docker not found")
}
return nil
},
5. Handle Interrupts
import (
"context"
"os/signal"
"syscall"
)
func run(cmd *cobra.Command, args []string) error {
ctx, stop := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)
defer stop()
return runWithContext(ctx)
}
Testing Commands
Test Registration
func TestRegister(t *testing.T) {
parent := &cobra.Command{Use: "root"}
Register(parent)
require.Len(t, parent.Commands(), 1)
require.Equal(t, "mycommand", parent.Commands()[0].Use)
}
Test Command Execution
func TestRun(t *testing.T) {
cmd := &cobra.Command{
Use: "test",
RunE: run,
}
cmd.SetArgs([]string{"arg1", "arg2"})
err := cmd.Execute()
require.NoError(t, err)
}
Test with Flags
func TestRunWithFlags(t *testing.T) {
cmd := &cobra.Command{
Use: "test",
RunE: run,
}
var flagValue string
cmd.Flags().StringVar(&flagValue, "flag", "", "test flag")
cmd.SetArgs([]string{"--flag", "value"})
err := cmd.Execute()
require.NoError(t, err)
require.Equal(t, "value", flagValue)
}
Common Pitfalls
❌ Using Run instead of RunE:
Run: func(cmd *cobra.Command, args []string) {
doWork()
}
✅ Use RunE:
RunE: func(cmd *cobra.Command, args []string) error {
return doWork()
}
❌ Not validating args:
RunE: func(cmd *cobra.Command, args []string) error {
id := args[0]
return nil
}
✅ Validate with Args:
cmd := &cobra.Command{
Args: cobra.ExactArgs(1),
RunE: run,
}
❌ Centralizing all commands:
RootCmd.AddCommand(cmd1)
RootCmd.AddCommand(cmd2)
RootCmd.AddCommand(cmd3)
✅ Module registration:
module1.Register(RootCmd)
module2.Register(RootCmd)
Checklist
Reference
CLY Project Structure:
cmd/
└── root.go # Root command, imports modules
modules/
├── demo/
│ ├── cmd.go # demo.Register()
│ └── spinner/
│ └── cmd.go # spinner.Register()
└── uuid/
└── cmd.go # uuid.Register()
Pattern: Each module registers itself, no central registration.
Resources