cmdguard

module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 30, 2026 License: MIT

README

cmdguard

CI

A Go library for building validated CLI applications with type-safe flags and dependency injection.

cmdguard wraps Cobra with type-safe validation and dependency injection.

The v2 API provides a type-safe, DI-powered, no-panic CLI construction experience.

Installation

go get github.com/larsartmann/cmdguard

Quick Start

The v2 API uses CLI[T] with a single type parameter for your config type. Each command can have its own flags type via Command[T, F].

package main

import (
    "context"
    "fmt"
    "os"
    "strings"

    "github.com/larsartmann/cmdguard/pkg/cmdguard/v2"
)

// AppConfig is your application-level configuration
type AppConfig struct {
    Verbose bool   `flag:"verbose" short:"v" default:"false" help:"Enable verbose output"`
    Output  string `flag:"output" short:"o" default:"text" help:"Output format"`
}

// GreetFlags defines command-specific flags (fully typed!)
type GreetFlags struct {
    Name  string `flag:"name" short:"n" default:"World" help:"Name to greet"`
    Shout bool   `flag:"shout" short:"s" default:"false" help:"Uppercase output"`
}

func main() {
    // Create CLI with typed config
    cli, err := v2.NewCLI[AppConfig]("myapp", "My CLI application", AppConfig{})
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to create CLI: %v\n", err)
        os.Exit(1)
    }

    // Add a command with typed flags (constructor validates at creation)
    greetCmd, err := v2.NewCommand[AppConfig, *GreetFlags]("greet",
        func(ctx context.Context, cfg *AppConfig, flags *GreetFlags) error {
            msg := fmt.Sprintf("Hello, %s!", flags.Name)
            if flags.Shout {
                msg = strings.ToUpper(msg)
            }
            fmt.Println(msg)
            return nil
        },
        v2.WithShort[AppConfig, *GreetFlags]("Greet someone"),
        v2.WithFlags[AppConfig, *GreetFlags](&GreetFlags{}),
    )
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to create command: %v\n", err)
        os.Exit(1)
    }

    err = v2.AddCommand(cli, greetCmd)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to add command: %v\n", err)
        os.Exit(1)
    }

    // Execute
    if err := cli.Execute(context.Background()); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

Features

Feature Description
Type-safe config Yes (type parameter T)
Type-safe flags Yes (type parameter F)
DI integration Yes (samber/do/v2)
Flag tags Yes (flag, short, default, help, env, count)
Environment variables env:"VAR" tag with WithEnvPrefix prefix support
Counting flags count:"true" for -v/-vv/-vvv verbosity
Signal handling WithSignalHandling for SIGINT/SIGTERM
Rich output 12 formats via go-output (table/json/csv/yaml/...)
Lifecycle hooks Yes (PreRunE, PostRunE)
Health checks Yes
Graceful shutdown Yes
$EDITOR integration EditInEditor() for config editing
Typo suggestions Flag and subcommand "did you mean?"
Extensible types RegisterTypeHandler() for custom flag types
Fuzz testing 7 fuzz targets for input parsers

v2 API Reference

CLI[T]

The main CLI type with a single type parameter T for your application config.

CLI Options (v2.2)
cli, err := v2.NewCLI[T](name, shortDescription, defaultConfig)

// With options
cli, err := v2.NewCLI[T](name, short, defaults,
    v2.WithCLIVersion[T]("2.2.0"),
    v2.WithCLILong[T]("A longer description..."),
    v2.WithSilenceErrors[T](),
    v2.WithSilenceUsage[T](),
    v2.WithFang[T](true),            // fang styling (replaces WithColor)
    v2.WithEnvPrefix[T]("MYAPP_"),   // prefix for env var lookups
    v2.WithSignalHandling[T](),       // SIGINT/SIGTERM context cancellation
)

// Add subcommands (standalone function — each command has its own flags type)
err = v2.AddCommand(cli, cmd)

// Execution
err := cli.Execute(ctx)           // Returns error
cli.ExecuteAndExit(ctx)           // Calls os.Exit(1) on error

// DI and lifecycle
scope := cli.Scope()              // *Scope for service registration
err := cli.HealthCheck()          // Run health checks
err := cli.Shutdown(ctx)          // Graceful shutdown

// Configuration access
cfg := cli.Config()               // *T - typed config
cli.SetConfig(cfg)                // Update config

// Advanced
cmd := cli.RootCommand()          // Underlying cobra.Command
cli.AddGlobalFlag(name, short, default, help)
cli.AddGlobalBoolFlag(name, short, default, help)
cli.SetVersion("1.0.0")
Command[T, F]

Type-safe command definition created via constructors:

// Leaf command — requires use string and handler
func NewCommand[T, F any](use string, runE func(ctx context.Context, cfg *T, flags F) error, opts ...CommandOption[T, F]) (Command[T, F], error)

// Parent command — requires use, long description, and subcommands
func NewParentCommand[T, F any](use string, long string, subcommands []Command[T, F], opts ...CommandOption[T, F]) (Command[T, F], error)

// Panic variants for compile-time-known configuration
func MustNewCommand[T, F any](...) Command[T, F]
func MustNewParentCommand[T, F any](...) Command[T, F]

Command options:

Option Purpose
WithShort[T, F](short) Short description
WithLong[T, F](long) Long description
WithAliases[T, F](aliases...) Alternative names
WithExample[T, F](example) Example usage
WithFlags[T, F](flags) Typed flags struct
WithPreRunE[T, F](preRunE) Pre-validation hook
WithPostRunE[T, F](postRunE) Post-success cleanup
WithSubcommands[T, F](cmds...) Child commands
WithHidden[T, F](hidden) Hide from help
WithDeprecated[T, F](msg) Deprecation message
WithGroupID[T, F](group) Help group name
Flag Tags

Define flags using struct tags:

type MyFlags struct {
    // Required: flag name
    Name string `flag:"name"`

    // Optional: short flag (-n)
    Name string `flag:"name" short:"n"`

    // Optional: default value
    Name string `flag:"name" default:"World"`

    // Optional: help text
    Name string `flag:"name" help:"Name to greet"`

    // All together
    Name    string `flag:"name" short:"n" default:"World" help:"Name to greet"`
    Count   int    `flag:"count" short:"c" default:"1" help:"Number of times"`
    Verbose bool   `flag:"verbose" short:"v" default:"false" help:"Verbose output"`
}

Supported types: string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, []string, time.Duration.

Custom types: Duration, Enum, LogLevel, LogFormat, URL, Email, Port, FilePath, HostPort.

Add your own with RegisterTypeHandler(reflect.Type, TypeHandler).

NoFlags

Use v2.NoFlags for commands without flags:

cmd, err := v2.NewCommand[AppConfig, v2.NoFlags]("version",
    func(ctx context.Context, cfg *AppConfig, flags v2.NoFlags) error {
        fmt.Println("v1.0.0")
        return nil
    },
    v2.WithShort[AppConfig, v2.NoFlags]("Print version"),
)
Mixing Flag Types

AddCommand is a standalone function, not a method, because each command can have a different flags type:

cli, _ := v2.NewCLI[AppConfig]("myapp", "...", AppConfig{})

// Command with GreetFlags
greetCmd, _ := v2.NewCommand[AppConfig, *GreetFlags]("greet", greetHandler,
    v2.WithFlags[AppConfig, *GreetFlags](&GreetFlags{}),
)
v2.AddCommand(cli, greetCmd)

// Command with ConfigFlags (different type!)
configCmd, _ := v2.NewCommand[AppConfig, *ConfigFlags]("config", configHandler,
    v2.WithFlags[AppConfig, *ConfigFlags](&ConfigFlags{}),
)
v2.AddCommand(cli, configCmd)

// Command with no flags
versionCmd, _ := v2.NewCommand[AppConfig, v2.NoFlags]("version", versionHandler)
v2.AddCommand(cli, versionCmd)
DI Integration

cmdguard v2 provides built-in dependency injection through samber/do/v2, enabling clean service management and lifecycle handling.

Registering Services
cli, _ := v2.NewCLI[AppConfig]("myapp", "...", AppConfig{})
scope := cli.Scope()

// Register with constructor (lazy initialization)
err := v2.Provide(scope, func(i do.Injector) (*Database, error) {
    cfg, err := v2.Invoke[*AppConfig](scope)
    if err != nil {
        return nil, err
    }
    return &Database{DSN: cfg.DSN}, nil
})

// Register pre-constructed value
err = v2.ProvideValue(scope, &Logger{Level: "info"})
Invoking Services
RunE: func(ctx context.Context, cfg *AppConfig, flags *GreetFlags) error {
    db, err := v2.Invoke[*Database](cli.Scope())
    if err != nil {
        return v2.NewServiceError("*Database", err)
    }
    // Use db...
    return nil
},
Lifecycle Management

Services can implement lifecycle hooks:

// Health checks — implement do.HealthcheckerWithContext
func (d *Database) HealthCheck(ctx context.Context) error {
    return d.Ping(ctx)
}

// Graceful shutdown — implement do.Shutdowner
func (s *Server) Shutdown() error {
    return s.server.Close()
}

// Run health checks
err := cli.HealthCheck()

// Graceful shutdown
err := cli.Shutdown(ctx)
Scope Hierarchy
scope := cli.Scope()

// Create child scopes for isolation
childScope := scope.Child("worker")

// Access scope hierarchy
path := scope.Path()     // Returns ["myapp"]
isRoot := scope.IsRoot() // Returns true for root scope
Lifecycle Hooks
cmd, err := v2.NewCommand[AppConfig, *Flags]("example", runHandler,
    v2.WithPreRunE[AppConfig, *Flags](func(ctx context.Context, cfg *AppConfig, flags *Flags) error {
        // Validation before main handler
        if flags.Count < 1 {
            return fmt.Errorf("count must be at least 1")
        }
        return nil
    }),
    v2.WithPostRunE[AppConfig, *Flags](func(ctx context.Context, cfg *AppConfig, flags *Flags) error {
        // Cleanup after main handler (only called on success)
        return nil
    }),
)
Environment Variables (v2.2)

Flags can read from environment variables:

type DBFlags struct {
    Host     string `flag:"host"     env:"DB_HOST"     default:"localhost" help:"Database host"`
    Port     int    `flag:"port"     env:"DB_PORT"     default:"5432"      help:"Database port"`
    Password string `flag:"password" env:"DB_PASSWORD"                     help:"Database password"`
}

cli, _ := v2.NewCLI[AppConfig]("myapp", "...", AppConfig{},
    v2.WithEnvPrefix[AppConfig]("MYAPP_"),
)
// Reads MYAPP_DB_HOST, MYAPP_DB_PORT, etc.
Counting Flags (v2.2)
type Flags struct {
    Verbose int `flag:"verbose" short:"v" help:"Verbosity" count:"true"`
}
// -v → 1, -vv → 2, -vvv → 3
Signal Handling (v2.2)
cli, _ := v2.NewCLI[AppConfig]("myapp", "...", AppConfig{},
    v2.WithSignalHandling[AppConfig](),
)
// Ctrl+C cancels context in handlers
Rich Output (v2.2)
// 12 output formats
v2.OutputTable(v2.FormatTable, headers, rows)
v2.OutputTable(v2.FormatJSON, headers, rows)

format, _ := v2.ParseOutputFormat("yaml")
v2.OutputTable(format, headers, rows)

// Formats: table, json, csv, tsv, markdown, xml, d2, yaml, html, tree, mermaid, dot
Extensible Types (v2.2)
// Register a custom type handler
v2.RegisterTypeHandler(reflect.TypeFor[MyType](), v2.TypeHandlerFunc{
    ParseFunc: func(value string, _ v2.FlagTag) (any, error) {
        return MyType{Value: value}, nil
    },
    DefaultFunc: func(_ v2.FlagTag) any { return MyType{} },
})
Functional Options

Commands can be built with functional options:

// Using NewCommand with functional options
cmd, err := v2.NewCommand[AppConfig, v2.NoFlags]("version",
    func(ctx context.Context, cfg *AppConfig, flags v2.NoFlags) error {
        fmt.Println("v1.0.0")
        return nil
    },
    v2.WithShort[AppConfig, v2.NoFlags]("Print version"),
)

// Using MustNewCommand for compile-time-known config (panics on error)
cmd := v2.MustNewCommand[AppConfig, v2.NoFlags]("version", versionHandler,
    v2.WithShort[AppConfig, v2.NoFlags]("Print version"),
)

Examples

Nested Commands
func main() {
    cli, _ := v2.NewCLI[AppConfig]("myapp", "My CLI", AppConfig{})

    migrateCmd, _ := v2.NewCommand[AppConfig, v2.NoFlags]("migrate",
        func(ctx context.Context, cfg *AppConfig, flags v2.NoFlags) error {
            return runMigrations()
        },
        v2.WithShort[AppConfig, v2.NoFlags]("Run migrations"),
    )

    rollbackCmd, _ := v2.NewCommand[AppConfig, v2.NoFlags]("rollback",
        func(ctx context.Context, cfg *AppConfig, flags v2.NoFlags) error {
            return rollbackMigrations()
        },
        v2.WithShort[AppConfig, v2.NoFlags]("Rollback migrations"),
    )

    dbCmd, _ := v2.NewParentCommand[AppConfig, v2.NoFlags]("db",
        "Database operations",
        []v2.Command[AppConfig, v2.NoFlags]{migrateCmd, rollbackCmd},
        v2.WithShort[AppConfig, v2.NoFlags]("Database operations"),
    )

    v2.AddCommand(cli, dbCmd)
    cli.ExecuteAndExit(context.Background())
}
Full DI Example

See examples/typed/main.go for a complete example with DI, lifecycle hooks, typed flags, and nested commands.

How It Works

cmdguard validates commands at construction time:

  • Returns errors on invalid commands—no panics
  • Type-safe flags ensure flags are properly typed at compile time
  • DI integration enables clean service management and lifecycle handling

This approach catches configuration errors during development, not production.

Philosophy

cmdguard is designed for production CLIs:

  • Type-safe flags with struct tags
  • Dependency injection for services
  • Graceful error handling—never panics
  • Constructor validation catches errors early

Project Status

Status Description
v2.2.0 Full type-safe API with DI, env, signals, output
License MIT

Documentation

License

MIT

Directories

Path Synopsis
examples
advanced-flags command
Package main demonstrates advanced flag usage with cmdguard v2.
Package main demonstrates advanced flag usage with cmdguard v2.
basic command
Basic example demonstrating simple cmdguard v2 usage.
Basic example demonstrating simple cmdguard v2 usage.
counting command
Package main demonstrates counting flags with cmdguard v2.
Package main demonstrates counting flags with cmdguard v2.
di command
Package main demonstrates cmdguard v2 with dependency injection.
Package main demonstrates cmdguard v2 with dependency injection.
di-patterns command
Package main demonstrates dependency injection patterns with cmdguard v2.
Package main demonstrates dependency injection patterns with cmdguard v2.
env-tags command
Package main demonstrates environment variable configuration with cmdguard v2.
Package main demonstrates environment variable configuration with cmdguard v2.
error-handling command
Package main demonstrates error handling patterns with cmdguard v2.
Package main demonstrates error handling patterns with cmdguard v2.
internal
Package internal provides shared utilities for cmdguard examples.
Package internal provides shared utilities for cmdguard examples.
output command
Package main demonstrates rich output formatting with cmdguard v2.
Package main demonstrates rich output formatting with cmdguard v2.
signals command
Package main demonstrates signal handling with cmdguard v2.
Package main demonstrates signal handling with cmdguard v2.
subcommands command
Package main demonstrates NewParentCommand for command groups with subcommands.
Package main demonstrates NewParentCommand for command groups with subcommands.
typed command
Typed example demonstrating the v2 API with type-safe commands, typed flags, DI integration, and lifecycle hooks.
Typed example demonstrating the v2 API with type-safe commands, typed flags, DI integration, and lifecycle hooks.
validation command
Package main demonstrates input validation patterns with cmdguard v2.
Package main demonstrates input validation patterns with cmdguard v2.
glamour module
manpage module
pkg
cmdguard/v2
Package v2 provides a type-safe, DI-powered CLI framework with no panics.
Package v2 provides a type-safe, DI-powered CLI framework with no panics.
testutil
Package testutil provides shared testing utilities for cmdguard tests.
Package testutil provides shared testing utilities for cmdguard tests.
prompts module
spinner module
telemetry module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL