Post

24. Working with CLI and Flags

πŸ› οΈ Unlock the full potential of command-line interfaces! This post guides you through mastering CLI arguments, flag parsing, custom flag types, and organizing complex tools with subcommands and popular libraries. πŸš€

24. Working with CLI and Flags

What we will learn in this post?

  • πŸ‘‰ Command-Line Arguments
  • πŸ‘‰ Flag Package
  • πŸ‘‰ Custom Flag Types
  • πŸ‘‰ Subcommands
  • πŸ‘‰ Popular CLI Libraries

os.Args – Your Program’s Input! πŸš€

Ever wondered how your Go program can react to information you type when running it? That’s where os.Args comes in! It’s a special slice (think dynamic list) that holds all the pieces of text given to your program.

What’s Inside? πŸ€”

When you run a Go program (e.g., ./myprogram hello world), os.Args captures everything:

  • os.Args[0]: This is always the name of your program itself (e.g., myprogram).
  • os.Args[1:]: These are the actual arguments or inputs you provided (e.g., hello, world). Each word becomes a separate string in the slice.

Getting Started! πŸ–οΈ

Here’s a simple example to access and print arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
	"fmt"
	"os" // Don't forget to import "os"
)

func main() {
	fmt.Println("Hello from:", os.Args[0]) // Prints the program's name

	if len(os.Args) > 1 {
		fmt.Println("First extra arg:", os.Args[1]) // Accesses the first user-provided argument
	} else {
		fmt.Println("No additional arguments provided.")
	}

	fmt.Println("All arguments:", os.Args) // Prints the entire slice
}

You can run this like: go run your_program.go alpha beta

Manual Parsing Tips! 🧐

To manually parse, you can check len(os.Args) to know how many arguments exist. Then, access specific arguments by their index (e.g., os.Args[1]). Remember, all arguments are strings initially, so you’ll often need to convert them (e.g., to numbers) if required!

graph TD
    A["🏁 Start Go Program"]:::style1 --> B["πŸ“¦ os.Args Slice Initialized"]:::style2
    B --> C["🏷️ Access os.Args[0]<br/>Program Name"]:::style3
    B --> D["πŸ“ Access os.Args[1:]<br/>User Arguments"]:::style4
    D --> E["πŸ”’ Check len(os.Args)<br/>for argument count"]:::style5
    E --> F["βš™οΈ Process Arguments<br/>print, convert"]:::style6
    F --> G["βœ… End"]:::style7
    
    classDef style1 fill:#00ADD8,stroke:#00758f,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#5dc9e2,stroke:#00ADD8,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style6 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style7 fill:#00ADD8,stroke:#00758f,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

Go’s flag Package: Building CLI Tools Easily! πŸš€

The flag package in Go makes building command-line tools super easy! It helps you define, parse, and use arguments (flags) that users provide when running your program. Let’s dive in!

Defining Your Flags πŸ“

You can define flags for different types of input. Each definition requires a name, a default value, and a short description. These functions return a pointer to the flag’s value.

  • String Flag: Use flag.String() for text input.
    1
    
    name := flag.String("name", "World", "Your name to greet")
    
  • Int Flag: Use flag.Int() for whole numbers.
    1
    
    age := flag.Int("age", 30, "Your age")
    
  • Bool Flag: Use flag.Bool() for true/false options.
    1
    
    verbose := flag.Bool("v", false, "Enable verbose output")
    

Parsing and Accessing Flags ✨

Before you can use the flag values, you must call flag.Parse(). This command reads the command-line arguments and assigns them to your defined flags. To get the actual value, you’ll dereference the pointer using *.

1
2
flag.Parse() // Always call this!
fmt.Println("Hello,", *name) // Access value

Here’s the basic flow:

graph TD
    A["πŸ“ Define Flags<br/>flag.String, flag.Int"]:::style1 --> B["βš™οΈ Call flag.Parse()"]:::style2
    B --> C["πŸ”“ Access Values<br/>*flagVariable"]:::style3
    C --> D["πŸš€ Run Your Logic"]:::style4
    
    classDef style1 fill:#00ADD8,stroke:#00758f,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#5dc9e2,stroke:#00ADD8,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

Example CLI Tool: A Simple Greeter πŸ€–

Let’s create a small program that greets a user by their provided name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
	"flag"
	"fmt"
)

func main() {
	// 1. Define the 'name' flag
	name := flag.String("name", "Go Developer", "The name to greet")

	// 2. Parse all command-line flags
	flag.Parse()

	// 3. Access and use the flag's value
	fmt.Printf("Hello, %s!\n", *name)
}

Run this with go run main.go -name "Alice" to see it in action!

Custom Flags with flag.Value 🚩

Ever wished your Go command-line flags could handle more than just simple text or numbers? Say hello to custom flags! By implementing the flag.Value interface, you can make your flags understand complex types like durations, lists, or even custom enums, directly from user input.

The Magic Behind flag.Value ✨

The flag.Value interface is a powerful contract requiring just two methods to bring your custom flag to life. It’s like teaching your program a new language for command-line arguments!

String(): Showing Your Flag’s Value πŸ’¬

This method simply returns the flag’s current value as a string. It’s what the flag package uses to display the default value or current state (e.g., in help messages).

Set(): Parsing Your Flag’s Input βš™οΈ

This is where the real magic happens! Set(s string) takes the raw string input from the command line, parses it according to your custom logic, and updates your custom type’s value. It returns an error if parsing fails (e.g., bad input format).

Why Use Custom Flags? πŸ’‘

They offer fantastic flexibility and cleaner code:

  • Duration: Parse -timeout 1m30s directly into a time.Duration.
  • Lists: Handle -items apple,banana,orange into a []string.
  • Enums: Define accepted values like -color RED for a custom Color type.
classDiagram
    direction LR
    class Value {
        <<interface>>
        πŸ“ +String() string
        βš™οΈ +Set(string) error
    }
    class MyCustomFlagType {
        πŸ”’ -value T
        πŸ“ +String() string
        βš™οΈ +Set(s string) error
    }
    MyCustomFlagType ..|> Value : implements
    
    style Value fill:#00ADD8,stroke:#00758f,color:#fff,stroke-width:3px
    style MyCustomFlagType fill:#5dc9e2,stroke:#00ADD8,color:#fff,stroke-width:3px

This way, your application logic receives beautifully pre-parsed data, simplifying your main program.

Building Go CLIs with Subcommands ✨

Ever wanted your Go CLI to feel like git or docker, with commands like mycli create? Subcommands help organize complex tools, making them user-friendly and manageable.

Benefits:

  • πŸ’‘ Clearer user experience.
  • 🧩 Modular code structure.
  • 🚫 Prevents flag conflicts.

The flag.FlagSet Superpower 🚩

Go’s built-in flag package is your friend! Instead of one global FlagSet, create a new flag.FlagSet for each subcommand. This isolates flags and options, ensuring clarity (e.g., mycli command --flag), so flags for create don’t interfere with delete.

Routing Your Commands πŸ—ΊοΈ

Your program’s first argument (os.Args[1]) dictates which subcommand to run. Use a switch statement (or a map for larger apps) to direct control. Each case will then parse its specific FlagSet using os.Args[2:] (to ignore the command name itself) and execute unique logic.

A Multi-Command Example πŸš€

This pattern makes handling multiple commands straightforward:

1
2
3
4
5
6
7
8
9
10
11
// main.go snippet
func main() {
    if len(os.Args) < 2 { /* show global help */ }
    switch os.Args[1] {
    case "greet":
        // Setup 'greet' FlagSet, parse os.Args[2:], run greet logic.
    case "bye":
        // Setup 'bye' FlagSet, parse os.Args[2:], run bye logic.
    default: /* unknown command error */
    }
}

How it Works (Flowchart):

graph TD
    A["🏁 CLI Start"]:::style1 --> B{"πŸ” First Argument<br/>os.Args[1]"}:::style2
    B -- "πŸ‘‹ greet" --> C["βš™οΈ Parse 'greet' flags<br/>πŸš€ Run greet logic"]:::style3
    B -- "πŸ‘‹ bye" --> D["βš™οΈ Parse 'bye' flags<br/>πŸš€ Run bye logic"]:::style4
    B -- "❌ Other" --> E["🚨 Show Error/Help"]:::style5
    
    classDef style1 fill:#00ADD8,stroke:#00758f,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#5dc9e2,stroke:#00ADD8,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

Go CLI Frameworks: Your Command Line Friends! πŸš€

Building awesome command-line tools in Go? You’ve got great options! Let’s explore how to pick the right one for your project, making your CLIs user-friendly and powerful.

Cobra: The Powerhouse for Complex CLIs! 🐍

Cobra is fantastic for complex CLIs needing many subcommands and nested structures, just like git or kubectl. It gracefully handles flags, arguments, and deep command hierarchies. Think mytool serve --port 8080 and mytool config set user guest. It’s the choice for giants like Kubernetes and Hugo!

When to use Cobra? 🎯

  • When your CLI needs subcommands (e.g., git add, git commit).
  • For large, enterprise-grade tools requiring advanced features like persistent flags and shell autocompletion.

urfave/cli: The Simpler Sidekick! πŸ§‘β€πŸ’»

urfave/cli is perfect for simpler CLIs, especially single-level commands. It’s quicker to set up than Cobra, balancing ease of use with good features. Great for utility scripts or tools without deeply nested commands.

When to use urfave/cli? πŸ’‘

  • When your CLI has a few commands, but not deep nesting.
  • For small to medium-sized projects or personal scripts.
  • If you want faster development for straightforward tools.

Standard flag Package: The Basics! 🚩

Go’s built-in flag package is super basic and lightweight. It’s ideal for the simplest scripts that only need a few command-line options directly on the main command. No subcommands here!

When to use flag? ✏️

  • For very simple utilities or one-off scripts.
  • When you only need a few command-line flags (e.g., myapp --verbose --file myfile.txt).
  • If you don’t need any subcommand structure.

    Choosing Your Framework: A Quick Guide! 🧭

graph TD
    A["🏁 Start CLI Project"]:::style1 --> B{"πŸ€” Need Subcommands<br/>& Complex Structure?"}:::style2
    B -- "βœ… Yes" --> C["🐍 Use Cobra<br/>Kubernetes, Hugo"]:::style3
    B -- "❌ No" --> D{"πŸ› οΈ Few Commands<br/>Simpler Tool?"}:::style4
    D -- "βœ… Yes" --> E["πŸ‘¨β€πŸ’» Use urfave/cli<br/>Medium projects"]:::style5
    D -- "❌ No" --> F{"πŸ“ Very Basic<br/>Few Flags?"}:::style6
    F -- "βœ… Yes" --> G["🚩 Use flag package<br/>Simple scripts"]:::style7
    
    classDef style1 fill:#00ADD8,stroke:#00758f,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#5dc9e2,stroke:#00ADD8,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style6 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style7 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

🎯 Real-World Example: Production Database Migration CLI Tool

Production systems use CLI tools for database migrations, deployments, and DevOps automation!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
package main

import (
	"flag"
	"fmt"
	"os"
	"strings"
	"time"
)

// Migration represents a database migration
type Migration struct {
	ID          string
	Description string
	SQL         string
	AppliedAt   *time.Time
}

// MigrationManager handles database migrations
type MigrationManager struct {
	migrations        []Migration
	appliedMigrations map[string]bool
	databaseURL       string
	verbose           bool
}

func NewMigrationManager(dbURL string, verbose bool) *MigrationManager {
	return &MigrationManager{
		migrations:        []Migration{},
		appliedMigrations: make(map[string]bool),
		databaseURL:       dbURL,
		verbose:           verbose,
	}
}

func (mm *MigrationManager) LoadMigrations() {
	// Simulate loading migrations from files
	mm.migrations = []Migration{
		{ID: "001", Description: "Create users table", SQL: "CREATE TABLE users ..."},
		{ID: "002", Description: "Add email index", SQL: "CREATE INDEX idx_email ..."},
		{ID: "003", Description: "Add timestamps", SQL: "ALTER TABLE users ADD ..."},
	}
	
	if mm.verbose {
		fmt.Printf("βœ… Loaded %d migrations\n", len(mm.migrations))
	}
}

func (mm *MigrationManager) ApplyMigration(id string) error {
	if mm.appliedMigrations[id] {
		return fmt.Errorf("migration %s already applied", id)
	}
	
	for _, migration := range mm.migrations {
		if migration.ID == id {
			if mm.verbose {
				fmt.Printf("πŸ”„ Applying migration %s: %s\n", migration.ID, migration.Description)
			}
			
			// Simulate migration execution
			time.Sleep(100 * time.Millisecond)
			
			now := time.Now()
			migration.AppliedAt = &now
			mm.appliedMigrations[id] = true
			
			fmt.Printf("βœ… Migration %s applied successfully\n", id)
			return nil
		}
	}
	
	return fmt.Errorf("migration %s not found", id)
}

func (mm *MigrationManager) ApplyAll() {
	fmt.Println("πŸš€ Applying all pending migrations...")
	for _, migration := range mm.migrations {
		if !mm.appliedMigrations[migration.ID] {
			if err := mm.ApplyMigration(migration.ID); err != nil {
				fmt.Printf("❌ Error: %v\n", err)
			}
		}
	}
}

func (mm *MigrationManager) Rollback(id string) error {
	if !mm.appliedMigrations[id] {
		return fmt.Errorf("migration %s not applied, cannot rollback", id)
	}
	
	if mm.verbose {
		fmt.Printf("↩️ Rolling back migration %s\n", id)
	}
	
	// Simulate rollback
	time.Sleep(100 * time.Millisecond)
	delete(mm.appliedMigrations, id)
	
	fmt.Printf("βœ… Migration %s rolled back successfully\n", id)
	return nil
}

func (mm *MigrationManager) Status() {
	fmt.Println("\nπŸ“Š Migration Status")
	fmt.Println("=" + strings.Repeat("=", 70))
	fmt.Printf("Database: %s\n\n", mm.databaseURL)
	
	for _, migration := range mm.migrations {
		status := "❌ Pending"
		appliedAt := "N/A"
		
		if mm.appliedMigrations[migration.ID] {
			status = "βœ… Applied"
			if migration.AppliedAt != nil {
				appliedAt = migration.AppliedAt.Format("2006-01-02 15:04:05")
			}
		}
		
		fmt.Printf("[%s] %s - %s (Applied: %s)\n", 
			migration.ID, status, migration.Description, appliedAt)
	}
}

// Subcommand handling
func main() {
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}
	
	// Global flags
	globalFlags := flag.NewFlagSet("global", flag.ExitOnError)
	dbURL := globalFlags.String("db", "postgres://localhost/myapp", "Database URL")
	verbose := globalFlags.Bool("verbose", false, "Enable verbose output")
	
	// Subcommands
	upCmd := flag.NewFlagSet("up", flag.ExitOnError)
	upAll := upCmd.Bool("all", false, "Apply all pending migrations")
	upID := upCmd.String("id", "", "Apply specific migration by ID")
	
	downCmd := flag.NewFlagSet("down", flag.ExitOnError)
	downID := downCmd.String("id", "", "Rollback specific migration by ID")
	
	statusCmd := flag.NewFlagSet("status", flag.ExitOnError)
	
	// Parse global flags first
	globalFlags.Parse(os.Args[2:])
	
	command := os.Args[1]
	manager := NewMigrationManager(*dbURL, *verbose)
	manager.LoadMigrations()
	
	switch command {
	case "up":
		upCmd.Parse(os.Args[2:])
		
		if *upAll {
			manager.ApplyAll()
		} else if *upID != "" {
			if err := manager.ApplyMigration(*upID); err != nil {
				fmt.Printf("❌ Error: %v\n", err)
				os.Exit(1)
			}
		} else {
			fmt.Println("❌ Please specify --all or --id <migration_id>")
			upCmd.PrintDefaults()
			os.Exit(1)
		}
		
	case "down":
		downCmd.Parse(os.Args[2:])
		
		if *downID != "" {
			if err := manager.Rollback(*downID); err != nil {
				fmt.Printf("❌ Error: %v\n", err)
				os.Exit(1)
			}
		} else {
			fmt.Println("❌ Please specify --id <migration_id>")
			downCmd.PrintDefaults()
			os.Exit(1)
		}
		
	case "status":
		statusCmd.Parse(os.Args[2:])
		manager.Status()
		
	case "help":
		printUsage()
		
	default:
		fmt.Printf("❌ Unknown command: %s\n", command)
		printUsage()
		os.Exit(1)
	}
}

func printUsage() {
	fmt.Println("πŸ“¦ Database Migration CLI Tool")
	fmt.Println("\nUsage: dbmigrate [command] [flags]")
	fmt.Println("\nCommands:")
	fmt.Println("  up       Apply migrations")
	fmt.Println("  down     Rollback migrations")
	fmt.Println("  status   Show migration status")
	fmt.Println("  help     Show this help message")
	fmt.Println("\nGlobal Flags:")
	fmt.Println("  --db <url>      Database connection URL")
	fmt.Println("  --verbose       Enable verbose output")
	fmt.Println("\nExamples:")
	fmt.Println("  dbmigrate up --all")
	fmt.Println("  dbmigrate up --id 001")
	fmt.Println("  dbmigrate down --id 003")
	fmt.Println("  dbmigrate status --db postgres://localhost/prod")
}

// This pattern is used in production by:
// - golang-migrate/migrate
// - Flyway migrations
// - Liquibase
// - Alembic (Python)
// - Rails ActiveRecord migrations

Usage Examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Apply all pending migrations
go run main.go up --all --verbose

# Apply specific migration
go run main.go up --id 001

# Check migration status
go run main.go status --db postgres://localhost/myapp

# Rollback a migration
go run main.go down --id 003

# Show help
go run main.go help

🎯 Hands-On Assignment: Build a Multi-Purpose DevOps CLI Tool πŸš€

πŸ“ Your Mission

Build a production-ready DevOps CLI tool with multiple subcommands, custom flag types, and comprehensive help system!

🎯 Requirements

  1. Deploy Subcommand:
    • Flags: --environment (dev/staging/prod), --service, --version
    • Validate environment is one of allowed values
    • Simulate deployment with progress indicators
    • Support --dry-run flag for testing
  2. Backup Subcommand:
    • Flags: --database, --output-path, --compress
    • Custom duration flag for --retention (e.g., "30d", "6h")
    • Show backup size and estimated time
    • List all backups with backup list
  3. Config Subcommand:
    • config get <key> - Retrieve configuration value
    • config set <key> <value> - Update configuration
    • config list - Show all configurations
    • Store config in JSON/YAML file
  4. Logs Subcommand:
    • Flags: --service, --level (info/warn/error), --since
    • Custom flag for --tail (number of lines)
    • --follow for live streaming
    • Color-coded output based on log level
  5. Health Subcommand:
    • Check status of multiple services
    • Flags: --timeout (custom duration type)
    • Display health status with emojis (βœ…/❌)
    • Exit with appropriate status code
  6. Global Flags:
    • --config - Custom config file path
    • --verbose - Enable detailed output
    • --quiet - Suppress non-essential output
    • --output - Format (text/json/yaml)
  7. Help System:
    • Comprehensive help for each subcommand
    • Usage examples for common scenarios
    • Auto-generated flag documentation
    • Color-coded help output

πŸ’‘ Starter Code

style="background: #2c3e50; color: #ecf0f1; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0;">package main import ( "flag" "fmt" "os" "strings" "time" ) // Custom duration flag type type DurationFlag time.Duration func (d *DurationFlag) String() string { return time.Duration(*d).String() } func (d *DurationFlag) Set(s string) error { duration, err := time.ParseDuration(s) if err != nil { return err } *d = DurationFlag(duration) return nil } // Custom environment flag type type EnvironmentFlag string func (e *EnvironmentFlag) String() string { return string(*e) } func (e *EnvironmentFlag) Set(s string) error { allowed := []string{"dev", "staging", "prod"} for _, env := range allowed { if s == env { *e = EnvironmentFlag(s) return nil } } return fmt.Errorf("invalid environment: %s (must be dev, staging, or prod)", s) } // Global configuration type Config struct { ConfigPath string Verbose bool Quiet bool Output string } func main() { if len(os.Args) < 2 { printGlobalHelp() os.Exit(1) } // Global flags globalFlags := flag.NewFlagSet("global", flag.ExitOnError) configPath := globalFlags.String("config", "~/.devops-cli.yaml", "Config file path") verbose := globalFlags.Bool("verbose", false, "Enable verbose output") quiet := globalFlags.Bool("quiet", false, "Suppress output") output := globalFlags.String("output", "text", "Output format (text|json|yaml)") config := &Config{} // Route to subcommand command := os.Args[1] switch command { case "deploy": handleDeploy(os.Args[2:], config) case "backup": handleBackup(os.Args[2:], config) case "config": handleConfig(os.Args[2:], config) case "logs": handleLogs(os.Args[2:], config) case "health": handleHealth(os.Args[2:], config) case "help": if len(os.Args) > 2 { printSubcommandHelp(os.Args[2]) } else { printGlobalHelp() } default: fmt.Printf("❌ Unknown command: %s\n", command) printGlobalHelp() os.Exit(1) } } func handleDeploy(args []string, config *Config) { deployCmd := flag.NewFlagSet("deploy", flag.ExitOnError) var env EnvironmentFlag deployCmd.Var(&env, "environment", "Target environment (dev|staging|prod)") service := deployCmd.String("service", "", "Service name to deploy") version := deployCmd.String("version", "latest", "Version to deploy") dryRun := deployCmd.Bool("dry-run", false, "Simulate deployment") deployCmd.Parse(args) if *service == "" { fmt.Println("❌ Service name is required") deployCmd.PrintDefaults() return } fmt.Printf("πŸš€ Deploying %s:%s to %s\n", *service, *version, env.String()) if *dryRun { fmt.Println("πŸ§ͺ Dry run mode - no changes made") return } // Simulate deployment for i := 0; i <= 100; i += 20 { fmt.Printf("\rπŸ”„ Progress: %d%%", i) time.Sleep(200 * time.Millisecond) } fmt.Println("\nβœ… Deployment completed successfully!") } func handleBackup(args []string, config *Config) { // TODO: Implement backup subcommand fmt.Println("πŸ’Ύ Backup command - implement this!") } func handleConfig(args []string, config *Config) { // TODO: Implement config subcommand fmt.Println("βš™οΈ Config command - implement this!") } func handleLogs(args []string, config *Config) { // TODO: Implement logs subcommand fmt.Println("πŸ“œ Logs command - implement this!") } func handleHealth(args []string, config *Config) { // TODO: Implement health check subcommand fmt.Println("πŸ’š Health command - implement this!") } func printGlobalHelp() { fmt.Println("πŸ› οΈ DevOps CLI Tool") fmt.Println("\nUsage: devops-cli [command] [flags]") fmt.Println("\nCommands:") fmt.Println(" deploy Deploy services to environments") fmt.Println(" backup Create and manage backups") fmt.Println(" config Manage CLI configuration") fmt.Println(" logs View and stream service logs") fmt.Println(" health Check service health status") fmt.Println(" help Show help for commands") fmt.Println("\nGlobal Flags:") fmt.Println(" --config Custom config file") fmt.Println(" --verbose Verbose output") fmt.Println(" --quiet Suppress output") fmt.Println(" --output Output format (text|json|yaml)") } func printSubcommandHelp(command string) { // TODO: Implement detailed help for each subcommand fmt.Printf("πŸ“š Help for: %s\n", command) } </code></pre>

πŸš€ Bonus Challenges

  • Level 2: Add shell completion for bash/zsh
  • Level 3: Implement --version flag with build info
  • Level 4: Add interactive mode with prompts
  • Level 5: Implement progress bars for long operations
  • Level 6: Add JSON/YAML output formatting
  • Level 7: Integrate with Cobra or urfave/cli framework

πŸŽ“ Learning Goals

  • Master flag package and custom flag types 🚩
  • Implement subcommand routing patterns πŸ”€
  • Build production-ready CLI tools πŸš€
  • Apply best practices for CLI UX ✨
  • Handle errors gracefully with proper exit codes βœ…

πŸ’‘ Pro Tip: This pattern is used in production tools like kubectl, docker CLI, git, and terraform!

Share Your Solution! πŸ’¬

Completed the project? Post your code in the comments below! Show us your CLI mastery! βœ¨πŸš€

</div> </details> --- # Conclusion: Master CLI Development in Go πŸŽ“ Go's standard library provides powerful tools for building professional command-line interfaces, from basic flag parsing to complex multi-command applications with custom types and subcommands. By mastering `os.Args`, the `flag` package, custom flag types, and subcommand patterns, you can create production-ready DevOps tools, database utilities, and system administration CLIs that power modern infrastructure – just like kubectl, docker, and terraform. πŸš€βœ¨
This post is licensed under CC BY 4.0 by the author.