Please purchase the course to watch this video.

Full Course
Creating flexible command-line applications in Go is streamlined by building an abstraction for handling subcommands using a tree data structure, where each node represents a command with optional subcommands. This approach simplifies the process of organizing commands like "add" and "subtract" under a root command, and supports features such as nested subcommands, argument parsing, and context-aware help messages. By structuring each command with properties for its name, usage, execution logic, and associated subcommands, developers can easily extend functionality and automatically generate helpful usage outputs for users. Compared to using repetitive switch statements, this method enhances maintainability and extensibility, allowing for seamless integration of features such as nested commands and custom help text. As a next step, implementing a flag system per command—ideally through a dedicated struct for flag management—enables robust, user-friendly interfaces similar to those in established Go CLI frameworks.
Now that we have a basic idea of how subcommands work and how we can set them up using the Go standard library, in this lesson we're going to build our own abstraction to simplify the process of setting up subcommands for our applications.
This abstraction will make our code more maintainable and easier to extend, while also providing a foundation for understanding how popular CLI frameworks like Cobra work under the hood.
Project Setup
Let's start with a new project called Commander:
mkdir commander
cd commander
go mod init commander
Create the basic structure:
package main
import (
"fmt"
"strings"
)
func main() {
// We'll implement our abstraction here
}
// Stub functions for our commands
func add(args []string) {
fmt.Println("Add command called:", strings.Join(args, " "))
}
func subtract(args []string) {
fmt.Println("Subtract command called:", strings.Join(args, " "))
}
Note: In your own code, you might want to set this up as a library in its own package, with main code in a
cmd/
directory for testing. Here we're using the main package to make it easier to follow.
Understanding the Tree Data Structure
We'll use a tree data structure to organize our commands:
Root Command (calc)
├── add subcommand
├── subtract subcommand
└── print subcommand
└── numbers subcommand (nested)
This structure allows us to:
- Have a root command (main application)
- Add multiple subcommands
- Support nested subcommands (unlimited depth)
- Easily traverse and execute commands
Building the Command Abstraction
Step 1: Define the Command Type
Create a new file command.go
:
package main
import (
"fmt"
"os"
"strings"
)
// Command represents a CLI command with its subcommands
type Command struct {
Name string // Command name (e.g., "add", "subtract")
Usage string // Description for help text
Run func(args []string) // Function to execute
subcommands map[string]*Command // Child commands (private)
}
Key design decisions:
Name
: The command identifier used in CLIUsage
: Human-readable description for helpRun
: The function to execute when command is calledsubcommands
: Private field for child commands (map for O(1) lookup)
Step 2: Add Command Registration
// AddCommand adds one or more subcommands to this command
func (c *Command) AddCommand(cmds ...*Command) {
// Initialize map if nil (lazy initialization)
if c.subcommands == nil {
c.subcommands = make(map[string]*Command)
}
// Add each command to the map
for _, cmd := range cmds {
c.subcommands[cmd.Name] = cmd
}
}
Why variadic parameters?
- Allows adding multiple commands in one call:
root.AddCommand(addCmd, subtractCmd)
- More convenient than separate calls
- Follows Go idioms (similar to
append()
)
Step 3: Command Execution Logic
// Execute runs the command tree starting from this command
func (c *Command) Execute() {
args := os.Args[1:] // Skip program name
c.executeWithArgs(args)
}
// executeWithArgs handles the actual execution logic
func (c *Command) executeWithArgs(args []string) {
// No arguments provided
if len(args) == 0 {
c.PrintHelp()
return
}
// Get the command name
commandName := args[0]
// Handle help command
if commandName == "help" {
c.PrintHelp()
return
}
// Look up subcommand
command, exists := c.subcommands[commandName]
if !exists {
fmt.Printf("Command doesn't exist: %s\n\n", commandName)
c.PrintHelp()
return
}
// Execute the command
remainingArgs := args[1:]
if command.Run != nil {
// Leaf command - execute the function
command.Run(remainingArgs)
} else {
// Parent command - continue traversing
command.executeWithArgs(remainingArgs)
}
}
Execution flow:
- Parse command line arguments
- Handle special cases (no args, help)
- Look up subcommand in map
- If command has
Run
function → execute it - If command has no
Run
function → treat as parent, continue traversing
Step 4: Help System
// PrintHelp displays usage information for this command
func (c *Command) PrintHelp() {
fmt.Printf("\n%s\n\n", c.Usage)
fmt.Printf("Usage: %s [command]\n\n", c.Name)
// Print available subcommands
if len(c.subcommands) > 0 {
fmt.Println("Available Commands:")
for _, cmd := range c.subcommands {
fmt.Printf(" %-12s %s\n", cmd.Name, cmd.Usage)
}
fmt.Println()
}
}
Complete Implementation
Here's the full working implementation:
package main
import (
"fmt"
"os"
"strings"
)
type Command struct {
Name string
Usage string
Run func(args []string)
subcommands map[string]*Command
}
func (c *Command) AddCommand(cmds ...*Command) {
if c.subcommands == nil {
c.subcommands = make(map[string]*Command)
}
for _, cmd := range cmds {
c.subcommands[cmd.Name] = cmd
}
}
func (c *Command) Execute() {
args := os.Args[1:]
c.executeWithArgs(args)
}
func (c *Command) executeWithArgs(args []string) {
if len(args) == 0 {
c.PrintHelp()
return
}
commandName := args[0]
if commandName == "help" {
c.PrintHelp()
return
}
command, exists := c.subcommands[commandName]
if !exists {
fmt.Printf("Command doesn't exist: %s\n\n", commandName)
c.PrintHelp()
return
}
remainingArgs := args[1:]
if command.Run != nil {
command.Run(remainingArgs)
} else {
command.executeWithArgs(remainingArgs)
}
}
func (c *Command) PrintHelp() {
fmt.Printf("\n%s\n\n", c.Usage)
fmt.Printf("Usage: %s [command]\n\n", c.Name)
if len(c.subcommands) > 0 {
fmt.Println("Available Commands:")
for _, cmd := range c.subcommands {
fmt.Printf(" %-12s %s\n", cmd.Name, cmd.Usage)
}
fmt.Println()
}
}
// Command implementations
func add(args []string) {
fmt.Println("Add command called:", strings.Join(args, " "))
// Implementation for adding numbers...
}
func subtract(args []string) {
fmt.Println("Subtract command called:", strings.Join(args, " "))
// Implementation for subtracting numbers...
}
func numbers(args []string) {
fmt.Println("Numbers called:", strings.Join(args, ", "))
}
func main() {
// Create individual commands
addCmd := &Command{
Name: "add",
Usage: "Used to add two numbers together",
Run: add,
}
subtractCmd := &Command{
Name: "subtract",
Usage: "Used to subtract two numbers",
Run: subtract,
}
// Create nested command structure
numbersCmd := &Command{
Name: "numbers",
Usage: "Print numbers",
Run: numbers,
}
printCmd := &Command{
Name: "print",
Usage: "Print operations",
}
printCmd.AddCommand(numbersCmd)
// Create root command
rootCmd := &Command{
Name: "calc",
Usage: "Calculator application",
}
// Add all commands to root
rootCmd.AddCommand(addCmd, subtractCmd, printCmd)
// Execute the command tree
rootCmd.Execute()
}
Testing the Implementation
Let's test our abstraction:
# Build the application
go build -o calc
# Test basic commands
./calc add 1 2
# Output: Add command called: 1 2
./calc subtract 10 5
# Output: Subtract command called: 10 5
# Test nested commands
./calc print numbers 4 8 15 16 23 42
# Output: Numbers called: 4, 8, 15, 16, 23, 42
# Test help system
./calc help
# Output: Calculator application
# Usage: calc [command]
# Available Commands:
# add Used to add two numbers together
# subtract Used to subtract two numbers
# print Print operations
# Test invalid command
./calc multiply
# Output: Command doesn't exist: multiply
# [help output follows]
# Test no arguments
./calc
# Output: [help output]
Advanced Features
1. Nested Commands (Unlimited Depth)
Our abstraction supports unlimited nesting:
// Create deeply nested structure
level1 := &Command{Name: "level1", Usage: "Level 1 command"}
level2 := &Command{Name: "level2", Usage: "Level 2 command"}
level3 := &Command{Name: "level3", Usage: "Level 3 command", Run: someFunction}
level2.AddCommand(level3)
level1.AddCommand(level2)
rootCmd.AddCommand(level1)
// Usage: ./calc level1 level2 level3 args...
2. Better Help Formatting
Enhance the help system with better formatting:
import "text/tabwriter"
func (c *Command) PrintHelp() {
fmt.Printf("\n%s\n\n", c.Usage)
fmt.Printf("Usage: %s [command]\n\n", c.Name)
if len(c.subcommands) > 0 {
fmt.Println("Available Commands:")
// Use tabwriter for aligned output
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
for _, cmd := range c.subcommands {
fmt.Fprintf(w, " %s\t%s\n", cmd.Name, cmd.Usage)
}
w.Flush()
fmt.Println()
}
}
3. Command Aliases
Add support for command aliases:
type Command struct {
Name string
Aliases []string // Alternative names
Usage string
Run func(args []string)
subcommands map[string]*Command
}
func (c *Command) AddCommand(cmds ...*Command) {
if c.subcommands == nil {
c.subcommands = make(map[string]*Command)
}
for _, cmd := range cmds {
// Add primary name
c.subcommands[cmd.Name] = cmd
// Add aliases
for _, alias := range cmd.Aliases {
c.subcommands[alias] = cmd
}
}
}
// Usage:
addCmd := &Command{
Name: "add",
Aliases: []string{"plus", "+"},
Usage: "Add two numbers",
Run: add,
}
4. Pre/Post Execution Hooks
Add hooks for setup and cleanup:
type Command struct {
Name string
Usage string
Run func(args []string)
PreRun func(args []string) // Called before Run
PostRun func(args []string) // Called after Run
subcommands map[string]*Command
}
func (c *Command) executeWithArgs(args []string) {
// ... existing logic ...
if command.Run != nil {
if command.PreRun != nil {
command.PreRun(remainingArgs)
}
command.Run(remainingArgs)
if command.PostRun != nil {
command.PostRun(remainingArgs)
}
} else {
command.executeWithArgs(remainingArgs)
}
}
Comparison with Cobra
Our abstraction shares many concepts with Cobra:
Feature | Our Implementation | Cobra |
---|---|---|
Command tree | ✅ Tree structure | ✅ Tree structure |
Nested commands | ✅ Unlimited depth | ✅ Unlimited depth |
Help generation | ✅ Basic help | ✅ Rich help + man pages |
Flag support | ❌ (homework) | ✅ Full flag support |
Aliases | ❌ (possible extension) | ✅ Built-in |
Auto-completion | ❌ | ✅ Bash/Zsh/Fish |
Configuration | ❌ | ✅ Viper integration |
Why build our own?
- Understanding: Learn how CLI frameworks work internally
- Appreciation: Better understand Cobra's complexity and features
- Customization: Build exactly what you need
- Learning: Practice with data structures and Go patterns
🎯 Homework Assignment
Add flag support to our command abstraction. You have two approaches:
Approach 1: Simple Flag Integration
Add *flag.FlagSet
directly to commands:
type Command struct {
Name string
Usage string
Run func(args []string)
Flags *flag.FlagSet // Simple approach
subcommands map[string]*Command
}
Approach 2: Custom Flag Abstraction (Recommended)
Create a custom flag system for better control:
// Custom flag abstraction
type Flags struct {
flagSet *flag.FlagSet // Internal flag set
values map[string]interface{} // Store flag values
}
func NewFlags(name string) *Flags {
return &Flags{
flagSet: flag.NewFlagSet(name, flag.ContinueOnError),
values: make(map[string]interface{}),
}
}
func (f *Flags) AddBoolFlag(name string, defaultValue bool, usage string) {
ptr := f.flagSet.Bool(name, defaultValue, usage)
f.values[name] = ptr
}
func (f *Flags) GetBool(name string) (bool, bool) {
if ptr, exists := f.values[name]; exists {
if boolPtr, ok := ptr.(*bool); ok {
return *boolPtr, true
}
}
return false, false
}
func (f *Flags) Parse(args []string) ([]string, error) {
err := f.flagSet.Parse(args)
return f.flagSet.Args(), err
}
// Add to Command struct
type Command struct {
Name string
Usage string
Run func(args []string)
Flags *Flags // Custom flag system
subcommands map[string]*Command
}
Implementation Requirements
- Flag parsing before command execution
- Help integration - show flags in help output
- Error handling for invalid flags
- Type support - at minimum: bool, string, int
- Default values and validation
Expected Usage
// Define command with flags
addCmd := &Command{
Name: "add",
Usage: "Add two numbers",
Flags: NewFlags("add"),
Run: add,
}
// Add flags
addCmd.Flags.AddBoolFlag("verbose", false, "Enable verbose output")
addCmd.Flags.AddIntFlag("precision", 2, "Decimal precision")
// Usage: ./calc add --verbose --precision 3 1.234 5.678
Testing Your Implementation
# Test flag parsing
./calc add --verbose 1 2
./calc subtract --help
./calc --invalid-flag # Should show error
# Test help with flags
./calc add --help
# Should show:
# Usage: add [flags] [args]
# Flags:
# --verbose Enable verbose output
# --precision Decimal precision (default: 2)
This is a challenging assignment that will deepen your understanding of CLI abstractions and prepare you for using Cobra effectively!
Summary
We've successfully built a powerful command abstraction that provides:
✅ Tree-based command structure - Unlimited nesting depth
✅ Clean API - Easy command registration and execution
✅ Help system - Automatic help generation
✅ Error handling - Graceful handling of invalid commands
✅ Extensible design - Easy to add new features
Key concepts learned:
- Tree data structures for hierarchical commands
- Interface design for clean APIs
- Lazy initialization for maps
- Recursive algorithms for tree traversal
- Variadic functions for flexible APIs
What's Next?
In the next lessons, we'll explore more standard library packages:
- File compression (zip files)
- Data hashing for file integrity
- Advanced I/O patterns
Understanding command abstractions will also prepare us for the next module where we'll explore Cobra - seeing how our simple implementation compares to a production-ready framework.
This foundation helps you:
- Appreciate Cobra's sophisticated features
- Build custom CLI tools when Cobra is overkill
- Understand CLI framework internals
- Make informed decisions about tool selection
Add Flag Support
Choose Your Implementation Approach
Add flag support to the command abstraction. You have two approaches:
Approach 1: Simple Flag Integration
Add *flag.FlagSet
directly to commands:
type Command struct {
Name string
Usage string
Run func(args []string)
Flags *flag.FlagSet // Simple approach
subcommands map[string]*Command
}
Approach 2: Custom Flag Abstraction (Recommended)
Create a custom flag system for better control:
// Custom flag abstraction
type Flags struct {
flagSet *flag.FlagSet // Internal flag set
values map[string]interface{} // Store flag values
}
func NewFlags(name string) *Flags {
return &Flags{
flagSet: flag.NewFlagSet(name, flag.ContinueOnError),
values: make(map[string]interface{}),
}
}
func (f *Flags) AddBoolFlag(name string, defaultValue bool, usage string) {
ptr := f.flagSet.Bool(name, defaultValue, usage)
f.values[name] = ptr
}
func (f *Flags) GetBool(name string) (bool, bool) {
if ptr, exists := f.values[name]; exists {
if boolPtr, ok := ptr.(*bool); ok {
return *boolPtr, true
}
}
return false, false
}
func (f *Flags) Parse(args []string) ([]string, error) {
err := f.flagSet.Parse(args)
return f.flagSet.Args(), err
}
// Add to Command struct
type Command struct {
Name string
Usage string
Run func(args []string)
Flags *Flags // Custom flag system
subcommands map[string]*Command
}
Implementation Requirements
- Flag parsing before command execution
- Help integration - show flags in help output
- Error handling for invalid flags
- Type support - at minimum: bool, string, int
- Default values and validation
Expected Usage
// Define command with flags
addCmd := &Command{
Name: "add",
Usage: "Add two numbers",
Flags: NewFlags("add"),
Run: add,
}
// Add flags
addCmd.Flags.AddBoolFlag("verbose", false, "Enable verbose output")
addCmd.Flags.AddIntFlag("precision", 2, "Decimal precision")
// Usage: ./calc add --verbose --precision 3 1.234 5.678
Testing Your Implementation
# Test flag parsing
./calc add --verbose 1 2
./calc subtract --help
./calc --invalid-flag # Should show error
# Test help with flags
./calc add --help
# Should show:
# Usage: add [flags] [args]
# Flags:
# --verbose Enable verbose output
# --precision Decimal precision (default: 2)
This is a challenging assignment that will deepen your understanding of CLI abstractions and prepare you for using Cobra effectively!