Skip to main content
โšก Calmops

Command-Line Parsing and Flags in Go

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