Skip to content

LarsArtmann/cmdguard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

514 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cmdguard

CI Go Reference Go Report Card License: MIT

Build production Go CLIs with type-safe flags, dependency injection, and zero panics.

cmdguard wraps Cobra with compile-time type safety, struct-tag-driven flags, and built-in dependency injection via samber/do/v2. Your flags are typed structs — no more stringly-typed Flags().GetString("name") calls that fail at runtime.


Why cmdguard?

Raw Cobra — flags are strings, validated at runtime:

var name string
var count int
rootCmd.Flags().StringVarP(&name, "name", "n", "World", "Name to greet")
rootCmd.Flags().IntVarP(&count, "count", "c", 1, "Number of greetings")
// Oops — forgot to add "count"? You find out at runtime.

cmdguard — flags are typed structs, validated at construction:

type GreetFlags struct {
    Name  string `flag:"name"  short:"n" default:"World" help:"Name to greet"`
    Count int    `flag:"count" short:"c" default:"1"    help:"Number of greetings"`
}
// Missing handler? Duplicate command? Invalid name? Caught at AddCommand time.

Quick Start

go get github.com/larsartmann/cmdguard
package main

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

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

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"`
}

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() {
    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)
    }

    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)
    }

    v2.AddCommand(cli, greetCmd)
    cli.ExecuteAndExit(context.Background())
}
$ go run main.go greet -n "cmdguard" --shout
HELLO, CMDGUARD!

Features

Category Highlights
Type-safe flags Struct tags (flag, short, default, help, env, required, count) — no string lookups
Per-command flag types Each Command[T, F] has its own F — mix different flag structs freely
Dependency injection Built-in samber/do/v2 with Provide, Invoke, lifecycle hooks
Environment variables env:"DB_HOST" tag with WithEnvPrefix("MYAPP_") prefix support
12 output formats table, JSON, CSV, YAML, Markdown, XML, HTML, D2, Mermaid, and more
Signal handling WithSignalHandling[T]() — Ctrl+C cancels context in all handlers
Typo suggestions "did you mean?" for flags and subcommands (Levenshtein distance)
Constructor validation Missing handlers, duplicate names, invalid flags — caught at AddCommand time
Counting flags count:"true" for -v/-vv/-vvv verbosity patterns
Extensible types RegisterTypeHandler() for custom flag types with full parse/validate support
Middleware TimingMiddleware, RecoveryMiddleware, or write your own
Shell completion Dynamic completion via WithCompletion[T, F](fn)
Man page generation GenerateManPageCommand[T](cli) for roff output
Positional args WithExactArgs, WithMinimumArgs, WithRangeArgs, WithNoArgs, or custom
Zero panics Every v2 API function returns errors — never panics in library code
220+ tests ~82% coverage, race-detected, fuzz-tested

Dependency Injection

Register services on the CLI scope and invoke them in handlers:

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

// Register (lazy initialization)
v2.Provide(scope, func(i do.Injector) (*Database, error) {
    return &Database{DSN: "postgres://..."}, nil
})

// Invoke in handlers
v2.NewCommand[AppConfig, v2.NoFlags]("query",
    func(ctx context.Context, cfg *AppConfig, flags v2.NoFlags) error {
        db, _ := v2.Invoke[*Database](cli.Scope())
        return db.Query(ctx)
    },
)

Services can implement HealthCheck and Shutdown for lifecycle management.


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.
)

Priority chain: explicit flag → env var → default value.


Rich Output

v2.OutputTable(v2.FormatTable, headers, rows)  // Aligned terminal table
v2.OutputTable(v2.FormatJSON, headers, rows)    // JSON array
v2.OutputTable(v2.FormatYAML, headers, rows)    // YAML

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

All 12 formats: table, json, csv, tsv, markdown, xml, yaml, html, d2, tree, mermaid, dot.


Subcommands

listCmd, _ := v2.NewCommand[AppConfig, v2.NoFlags]("list", listHandler,
    v2.WithShort[AppConfig, v2.NoFlags]("List users"),
)
createCmd, _ := v2.NewCommand[AppConfig, v2.NoFlags]("create", createHandler,
    v2.WithShort[AppConfig, v2.NoFlags]("Create a user"),
)
userCmd, _ := v2.NewParentCommand[AppConfig, v2.NoFlags]("user",
    "User management", []v2.Command[AppConfig, v2.NoFlags]{listCmd, createCmd},
    v2.WithShort[AppConfig, v2.NoFlags]("User management"),
)
v2.AddCommand(cli, userCmd)

Lifecycle Hooks

v2.NewCommand[AppConfig, *Flags]("deploy", runHandler,
    v2.WithPreRunE[AppConfig, *Flags](func(ctx context.Context, cfg *AppConfig, flags *Flags) error {
        return validateConfig(flags)
    }),
    v2.WithPostRunE[AppConfig, *Flags](func(ctx context.Context, cfg *AppConfig, flags *Flags) error {
        return cleanup()
    }),
)

PostRunE only fires on success — Cobra semantics.


Built-in Value Types

Type Validation
Duration Wraps time.Duration
Enum[T] Validated against allowed values
LogLevel debug / info / warn / error
URL Validated URL string
Email RFC 5322 email validation
Port 1–65535 range
FilePath Path cleaning and existence checks
HostPort host:port validation

Add your own with RegisterTypeHandler():

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{} },
})

Flag Tags Reference

type Flags struct {
    Name    string `flag:"name"    short:"n" default:"World"  help:"Name"`
    Verbose int    `flag:"verbose" short:"v" help:"Verbosity" count:"true"`
    Host    string `flag:"host"             default:"localhost" env:"DB_HOST" help:"DB host"`
    Mode    string `flag:"mode"  required:"true"                help:"Required!"`
}
Tag Purpose Example
flag Flag name (required) flag:"name"
short Short flag short:"n"
default Default value default:"World"
help Help text help:"Name to greet"
env Environment variable env:"DB_HOST"
required Mark as required required:"true"
count Counting flag count:"true"

Command Options

Option Purpose
WithShort[T, F](short) Short description
WithLong[T, F](long) Long description
WithExample[T, F](example) Example usage
WithAliases[T, F](aliases...) Alternative names
WithFlags[T, F](flags) Typed flags struct
WithPreRunE[T, F](fn) Pre-validation hook
WithPostRunE[T, F](fn) Post-success cleanup
WithHidden[T, F](bool) Hide from help
WithDeprecated[T, F](msg) Deprecation message
WithGroupID[T, F](id) Help group name
WithExactArgs[T, F](n) Require exactly n positional args
WithMinimumArgs[T, F](n) Require at least n positional args
WithMaximumArgs[T, F](n) Allow at most n positional args
WithRangeArgs[T, F](min, max) Require between min and max args
WithNoArgs[T, F]() Reject any positional args
WithCompletion[T, F](fn) Dynamic shell completion

CLI Options

cli, _ := v2.NewCLI[AppConfig]("myapp", "My app", AppConfig{},
    v2.WithCLIVersion[AppConfig]("1.0.0"),
    v2.WithEnvPrefix[AppConfig]("MYAPP_"),
    v2.WithSignalHandling[AppConfig](),
    v2.WithFang[AppConfig](true),                  // Styled help output
    v2.WithMiddleware[AppConfig](myMiddleware),     // Wrap all handlers
    v2.WithStrictValidation[AppConfig](),           // Require WithShort on commands
    v2.WithConfigValidation[AppConfig](validateFn), // Validate config after parsing
)
Option Purpose
WithCLIVersion[T](v) Version string
WithCLILong[T](desc) Long description
WithSilenceErrors[T]() Suppress error printing
WithSilenceUsage[T]() Suppress usage on error
WithFang[T](bool) Styled help output
WithEnvPrefix[T](prefix) Prefix for env vars
WithSignalHandling[T]() Cancel context on SIGINT/SIGTERM
WithMiddleware[T](mw...) Middleware for all commands
WithGroup[T](id, title) Help group on root
WithConfigValidation[T](fn) Validate config after flag parsing
WithStrictValidation[T]() Require WithShort on all commands
WithDraconianValidation[T]() Strict + require WithExample on leaf commands

Error Handling

// All v2 functions return errors — no panics
cli, err := v2.NewCLI[Config]("app", "...", Config{})
cmd, err := v2.NewCommand[Config, NoFlags]("test", handler)

// Sentinel errors for errors.Is()
errors.Is(err, v2.ErrInvalidCommand)
errors.Is(err, v2.ErrMissingHandler)
errors.Is(err, v2.ErrDuplicateCommand)

// Rich error types with context
v2.NewCommandError(name, err)
v2.NewFlagError(name, err)
v2.NewFlagErrorWithSuggestion(name, err, suggestion) // includes typo fix
v2.NewExitError(code, err)                            // custom exit code

Examples

See the examples/ directory:


Documentation


License

MIT

About

Type-safe CLI framework for Go — wraps Cobra with struct-tag flags, dependency injection, constructor validation, and zero panics

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages