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. π
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 1m30sdirectly into atime.Duration. - Lists: Handle
-items apple,banana,orangeinto a[]string. - Enums: Define accepted values like
-color REDfor a customColortype.
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
- Deploy Subcommand:
- Flags:
--environment(dev/staging/prod),--service,--version - Validate environment is one of allowed values
- Simulate deployment with progress indicators
- Support
--dry-runflag for testing
- Flags:
- 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
- Flags:
- Config Subcommand:
config get <key>- Retrieve configuration valueconfig set <key> <value>- Update configurationconfig list- Show all configurations- Store config in JSON/YAML file
- Logs Subcommand:
- Flags:
--service,--level(info/warn/error),--since - Custom flag for
--tail(number of lines) --followfor live streaming- Color-coded output based on log level
- Flags:
- Health Subcommand:
- Check status of multiple services
- Flags:
--timeout(custom duration type) - Display health status with emojis (β /β)
- Exit with appropriate status code
- Global Flags:
--config- Custom config file path--verbose- Enable detailed output--quiet- Suppress non-essential output--output- Format (text/json/yaml)
- 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. πβ¨