Command-Line Parsing and Flags in Go
Introduction
Parsing command-line arguments and flags is fundamental to CLI applications. Go provides the flag package for basic parsing and libraries like Cobra for advanced use cases. This guide covers both approaches.
Proper flag parsing ensures your CLI applications are user-friendly and handle edge cases gracefully.
Flag Package Basics
Simple Flag Parsing
package main
import (
"flag"
"fmt"
"log"
)
// SimpleExample demonstrates basic flag parsing
func SimpleExample() {
// Define flags
name := flag.String("name", "World", "Name to greet")
count := flag.Int("count", 1, "Number of greetings")
verbose := flag.Bool("verbose", false, "Enable verbose output")
// Parse flags
flag.Parse()
// Use flags
for i := 0; i < *count; i++ {
fmt.Printf("Hello, %s!\n", *name)
}
if *verbose {
fmt.Printf("Greeted %s %d times\n", *name, *count)
}
}
Good: Proper Flag Parsing Implementation
package main
import (
"flag"
"fmt"
"log"
"os"
"strings"
)
// Config holds parsed configuration
type Config struct {
Name string
Count int
Verbose bool
Output string
Format string
Tags []string
}
// ParseFlags parses command-line flags
func ParseFlags() (*Config, error) {
config := &Config{}
// Define flags
flag.StringVar(&config.Name, "name", "World", "Name to greet")
flag.StringVar(&config.Name, "n", "World", "Name to greet (shorthand)")
flag.IntVar(&config.Count, "count", 1, "Number of greetings")
flag.IntVar(&config.Count, "c", 1, "Number of greetings (shorthand)")
flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose output")
flag.BoolVar(&config.Verbose, "v", false, "Enable verbose output (shorthand)")
flag.StringVar(&config.Output, "output", "", "Output file")
flag.StringVar(&config.Output, "o", "", "Output file (shorthand)")
flag.StringVar(&config.Format, "format", "text", "Output format (text, json, yaml)")
flag.StringVar(&config.Format, "f", "text", "Output format (shorthand)")
// Custom flag for tags
tagsFlag := flag.String("tags", "", "Comma-separated tags")
// Parse flags
flag.Parse()
// Validate flags
if err := validateConfig(config); err != nil {
return nil, err
}
// Parse tags
if *tagsFlag != "" {
config.Tags = strings.Split(*tagsFlag, ",")
}
return config, nil
}
// validateConfig validates the configuration
func validateConfig(config *Config) error {
if config.Name == "" {
return fmt.Errorf("name cannot be empty")
}
if config.Count < 1 {
return fmt.Errorf("count must be at least 1")
}
if config.Count > 1000 {
return fmt.Errorf("count cannot exceed 1000")
}
validFormats := map[string]bool{
"text": true,
"json": true,
"yaml": true,
}
if !validFormats[config.Format] {
return fmt.Errorf("invalid format: %s", config.Format)
}
return nil
}
// Execute executes the application
func Execute(config *Config) error {
if config.Verbose {
fmt.Printf("Configuration: %+v\n", config)
}
for i := 0; i < config.Count; i++ {
fmt.Printf("Hello, %s!\n", config.Name)
}
return nil
}
func main() {
config, err := ParseFlags()
if err != nil {
log.Fatal(err)
}
if err := Execute(config); err != nil {
log.Fatal(err)
}
}
Bad: Improper Flag Parsing
package main
import (
"flag"
"fmt"
)
// BAD: No validation
func BadParseFlags() {
name := flag.String("name", "", "Name")
count := flag.Int("count", 0, "Count")
flag.Parse()
// No validation
// No error handling
fmt.Printf("Name: %s, Count: %d\n", *name, *count)
}
// BAD: No error handling
func BadExecute() {
// No error checking
// No validation
fmt.Println("Done")
}
Problems:
- No validation
- No error handling
- No default values
- No help text
Advanced Flag Parsing
Custom Flag Types
package main
import (
"flag"
"fmt"
"strings"
"time"
)
// StringSlice implements flag.Value for string slices
type StringSlice []string
func (s *StringSlice) String() string {
return strings.Join(*s, ",")
}
func (s *StringSlice) Set(value string) error {
*s = append(*s, value)
return nil
}
// Duration implements custom duration parsing
type Duration struct {
time.Duration
}
func (d *Duration) Set(value string) error {
duration, err := time.ParseDuration(value)
if err != nil {
return err
}
d.Duration = duration
return nil
}
// Example usage
func CustomFlagExample() {
var tags StringSlice
var timeout Duration
flag.Var(&tags, "tag", "Tags (can be used multiple times)")
flag.Var(&timeout, "timeout", "Timeout duration (e.g., 30s, 5m)")
flag.Parse()
fmt.Printf("Tags: %v\n", tags)
fmt.Printf("Timeout: %v\n", timeout.Duration)
}
Subcommands
package main
import (
"flag"
"fmt"
"log"
"os"
)
// Command represents a subcommand
type Command interface {
Name() string
Usage() string
Execute(args []string) error
}
// CreateCommand implements the create subcommand
type CreateCommand struct {
fs *flag.FlagSet
name string
}
func (c *CreateCommand) Name() string {
return "create"
}
func (c *CreateCommand) Usage() string {
return "create [options]"
}
func (c *CreateCommand) Execute(args []string) error {
c.fs = flag.NewFlagSet(c.Name(), flag.ExitOnError)
c.fs.StringVar(&c.name, "name", "", "Name of resource")
if err := c.fs.Parse(args); err != nil {
return err
}
if c.name == "" {
return fmt.Errorf("name is required")
}
fmt.Printf("Creating resource: %s\n", c.name)
return nil
}
// ListCommand implements the list subcommand
type ListCommand struct {
fs *flag.FlagSet
format string
limit int
}
func (c *ListCommand) Name() string {
return "list"
}
func (c *ListCommand) Usage() string {
return "list [options]"
}
func (c *ListCommand) Execute(args []string) error {
c.fs = flag.NewFlagSet(c.Name(), flag.ExitOnError)
c.fs.StringVar(&c.format, "format", "table", "Output format")
c.fs.IntVar(&c.limit, "limit", 10, "Limit results")
if err := c.fs.Parse(args); err != nil {
return err
}
fmt.Printf("Listing resources (format: %s, limit: %d)\n", c.format, c.limit)
return nil
}
// CommandRegistry manages commands
type CommandRegistry struct {
commands map[string]Command
}
func NewCommandRegistry() *CommandRegistry {
return &CommandRegistry{
commands: make(map[string]Command),
}
}
func (cr *CommandRegistry) Register(cmd Command) {
cr.commands[cmd.Name()] = cmd
}
func (cr *CommandRegistry) Execute(args []string) error {
if len(args) == 0 {
return fmt.Errorf("no command specified")
}
cmdName := args[0]
cmd, exists := cr.commands[cmdName]
if !exists {
return fmt.Errorf("unknown command: %s", cmdName)
}
return cmd.Execute(args[1:])
}
// Example usage
func SubcommandExample() {
registry := NewCommandRegistry()
registry.Register(&CreateCommand{})
registry.Register(&ListCommand{})
if err := registry.Execute(os.Args[1:]); err != nil {
log.Fatal(err)
}
}
Environment Variable Integration
package main
import (
"flag"
"fmt"
"os"
"strconv"
)
// ConfigFromEnv loads configuration from environment variables
type ConfigFromEnv struct {
Host string
Port int
Debug bool
LogLevel string
}
// LoadFromEnv loads configuration from environment
func LoadFromEnv() *ConfigFromEnv {
config := &ConfigFromEnv{
Host: getEnv("APP_HOST", "localhost"),
Port: getEnvInt("APP_PORT", 8080),
Debug: getEnvBool("APP_DEBUG", false),
LogLevel: getEnv("APP_LOG_LEVEL", "info"),
}
return config
}
// getEnv gets an environment variable with a default
func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
// getEnvInt gets an integer environment variable
func getEnvInt(key string, defaultValue int) int {
if value, exists := os.LookupEnv(key); exists {
if intVal, err := strconv.Atoi(value); err == nil {
return intVal
}
}
return defaultValue
}
// getEnvBool gets a boolean environment variable
func getEnvBool(key string, defaultValue bool) bool {
if value, exists := os.LookupEnv(key); exists {
return value == "true" || value == "1" || value == "yes"
}
return defaultValue
}
// ParseFlagsWithEnv parses flags with environment variable fallback
func ParseFlagsWithEnv() *ConfigFromEnv {
config := LoadFromEnv()
// Override with flags
flag.StringVar(&config.Host, "host", config.Host, "Server host")
flag.IntVar(&config.Port, "port", config.Port, "Server port")
flag.BoolVar(&config.Debug, "debug", config.Debug, "Enable debug mode")
flag.StringVar(&config.LogLevel, "log-level", config.LogLevel, "Log level")
flag.Parse()
return config
}
Best Practices
1. Provide Defaults
// Always provide sensible defaults
flag.String("name", "default", "Description")
2. Validate Input
// Validate all parsed values
if err := validateConfig(config); err != nil {
return nil, err
}
3. Use Short and Long Forms
// Provide both short and long flag names
flag.StringVar(&name, "name", "", "Name")
flag.StringVar(&name, "n", "", "Name (shorthand)")
4. Document Flags
// Provide clear descriptions
flag.String("output", "", "Output file path")
Common Pitfalls
1. No Validation
Always validate parsed flags.
2. Missing Defaults
Provide sensible defaults for all flags.
3. Poor Error Messages
Provide clear error messages for invalid input.
4. No Help Text
Always document your flags.
Resources
Summary
Proper flag parsing is essential for CLI applications. Key takeaways:
- Use the flag package for simple applications
- Use Cobra for complex CLI tools
- Always validate parsed values
- Provide sensible defaults
- Document all flags
- Support both short and long forms
- Integrate with environment variables
By mastering flag parsing, you can build user-friendly CLI applications.
Comments