Command structure patterns
goopt offers several approaches to organizing commands and flags:
1. Flag-Centric Approach (Path-Based)
// path is a comma-separated list of command paths - commands are created on the fly and flags are shared across commands
type Options struct {
Host string `goopt:"name:host;path:server start,server stop"`
Port int `goopt:"name:port;path:server start"`
}
Advantages:
- Flags can be shared across commands
- Clear visibility of flag reuse
- Flexible command path definition
- Good for commands sharing many flags
Trade-offs:
- Command structure less visible in code
- Can become hard to maintain for complex hierarchies
- Need to carefully manage path strings
2. Explicit Command Declaration with flags
type Options struct {
Server Command {
Name: "server",
Subcommands: []Command{
{Name: "start", Description: "Start server"},
{Name: "stop", Description: "Stop server"},
}
},
Host string `goopt:"name:host;path:server start,server stop"` // This is a flag - the path is a comma-separated list of command paths - commands are created on the fly if not found
}
Advantages:
- Clear command hierarchy
- Full control over command properties
- Good for static command structures
- Easy to add command-specific behavior
Trade-offs:
- More verbose
- Less flexible for dynamic command structures
- Flags can be shared across commands via the path but must be defined outside of the command struct
3. Struct Tag Command Definition
type Options struct {
Server struct {
Start struct {
Host string `goopt:"name:host"` // This is a flag
Port int `goopt:"name:port"` // This is a flag
} `goopt:"kind:command"`
Stop struct{} `goopt:"kind:command"`
} `goopt:"kind:command"`
}
Advantages:
- Clear command hierarchy in code structure
- Natural nesting of commands
- Command-specific flags clearly grouped
- Good for complex command hierarchies
Trade-offs:
- Can’t easily share flags between commands
- More nested structures
- Can lead to deeper type hierarchies
4. Shared Resources via Pointers
type ServerConfig struct {
Host string `goopt:"name:host"`
Port int `goopt:"name:port"`
}
type Options struct {
Start struct {
*ServerConfig `goopt:"name:server"` // This is a pointer to a ServerConfig struct - the name can be overridden with the name tag
} `goopt:"kind:command"` // command start has a pointer to a ServerConfig struct
Stop struct {
*ServerConfig `goopt:"name:server"` // This is a pointer to a ServerConfig struct - the name can be overridden with the name tag
} `goopt:"kind:command"` // command stop has a pointer to a ServerConfig struct
}
Advantages:
- Reuse configuration structures
- DRY principle for shared settings
- Good for modular command configurations
- Flexible composition of command options
Trade-offs:
- Need to manage nil pointers
- Less explicit flag visibility
- Can hide dependencies
Best Practices
- Choose based on your primary use case:
- Flag-centric for shared flags across commands
- Explicit commands for static hierarchies
- Struct tags for complex, unique command trees
- Pointer sharing for modular configurations
- Consider maintenance:
- Prefer struct tags for deep hierarchies
- Use explicit commands for simple hierarchies
- Use flag paths for flat command structures
- Consider visibility:
- Make command structure clear in code
- Document shared flags
- Use consistent naming patterns
- Handle complexity:
- Break down deep hierarchies
- Use shared structs for common patterns
- Consider generating complex structures
- Error handling:
- Validate command paths
- Check for flag name collisions
- Handle nil pointers appropriately