README
¶
cmdguard
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.
API Stability: The v2 API is stable and will only receive additive changes until v3. See CHANGELOG.md for deprecation policy.
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 |
| Flow context | BranchingFlowContext — track command path and share values across hierarchy |
| Editor support | EditInEditor() — open $EDITOR for user input |
| Config files | WithConfigFile[T](paths...) — JSON/YAML/TOML auto-loading with flag override |
| 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, SpinnerMiddleware, TelemetryMiddleware, or write your own |
| Interactive prompts | |
| Markdown help | |
| Color control | |
| 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 |
| 356 tests (706 cases) | 82.8% 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 |
| | WithValidArgs[T, F](args...) | Restrict args to allowed values |
| | WithSubcommands[T, F](cmds...) | Attach child commands (parent) |
| 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 |
WithConfigFile[T](paths...) |
Auto-load JSON config from first found path |
WithConfigFileLoader[T](l, paths...) |
Load config with custom loader (YAML/TOML) |
WithGlamourHelp[T]() |
Render markdown in command help text |
WithTelemetry[T](tracer) |
OpenTelemetry spans for all 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
// ExitCoder interface — check with errors.As
var exitCoder v2.ExitCoder
errors.As(err, &exitCoder)
exitCoder.ExitCode() // returns custom exit code
Must Constructors
MustNewCommand and MustNewParentCommand panic on error — use when configuration is known at compile time:
greetCmd := v2.MustNewCommand[AppConfig, *GreetFlags]("greet", greetHandler,
v2.WithShort[AppConfig, *GreetFlags]("Greet someone"),
v2.WithFlags[AppConfig, *GreetFlags](&GreetFlags{}),
)
parentCmd := v2.MustNewParentCommand[AppConfig, v2.NoFlags]("user",
"User management", []v2.Command[AppConfig, v2.NoFlags]{listCmd, createCmd},
v2.WithShort[AppConfig, v2.NoFlags]("User management"),
)
Config Files
JSON (built-in)
cli, _ := v2.NewCLI[AppConfig]("myapp", "...", AppConfig{},
v2.WithConfigFile[AppConfig]("~/.config/myapp/config.json", "/etc/myapp/config.json"),
)
Paths are tried in order; missing files are silently skipped. Supports $ENV and ~ expansion.
YAML / TOML (custom loaders)
import "github.com/larsartmann/cmdguard/pkg/cmdguard/v2/configload"
cli, _ := v2.NewCLI[AppConfig]("myapp", "...", AppConfig{},
v2.WithConfigFileLoader[AppConfig](configload.YAML(), "config.yaml"),
)
configload.YAML() and configload.TOML() return ConfigFileLoader implementations. See pkg/cmdguard/v2/configload/ for available loaders.
Precedence: config file → environment variables → explicit flags → defaults.
Man Page Generation
manCmd, err := v2.GenerateManPageCommand[AppConfig](cli)
if err != nil {
log.Fatal(err)
}
v2.AddCommand(cli, manCmd)
// $ myapp man
Generates roff-formatted man pages from your command structure.
BranchingFlowContext
Track the command execution path and share values across the hierarchy:
func handler(ctx context.Context, cfg *AppConfig, flags *Flags) error {
bfc, ok := v2.GetBranchingFlowContext(ctx)
if ok {
fmt.Println("Path:", bfc.PathString()) // "myapp.resource.list"
bfc.SetValue("key", "value") // propagates to children
val, _ := bfc.GetValue("key") // looks up hierarchy
_ = val
}
return nil
}
Color Output
cmdguard uses fang for styled help output via lipgloss. A --no-color flag is registered by default — pass it to disable color output. Lipgloss also respects the NO_COLOR environment variable automatically.
// Check if color is disabled
if cli.NoColor() {
// use plain output
}
cli, _ := v2.NewCLI[AppConfig]("myapp", "...", AppConfig{},
v2.WithFang[AppConfig](true), // styled help (default)
v2.WithFang[AppConfig](false), // plain text help
)
EditInEditor
Open the user's $EDITOR to edit content interactively:
edited, err := v2.EditInEditor(ctx, "# Edit your message here\n")
if err != nil {
return err
}
fmt.Println("User wrote:", edited)
Version Command
cli, _ := v2.NewCLI[AppConfig]("myapp", "...", AppConfig{},
v2.WithCLIVersion[AppConfig]("1.0.0"),
)
versionCmd := v2.MustVersionCommand[AppConfig](cli)
v2.AddCommand(cli, versionCmd)
// $ myapp version
Test Helpers
The testutil subpackage provides a harness for testing cmdguard CLIs:
import "github.com/larsartmann/cmdguard/pkg/cmdguard/v2/testutil"
result := testutil.TestCLI(t, cli, []string{"greet", "--name", "Alice"})
result.AssertNoError()
result.AssertExitCode(0)
result.AssertOutputContains("Hello, Alice!")
Examples
See examples/taskctl/ — a production-grade task manager CLI demonstrating all features: DI, typed flags, middleware, subcommands, config files, rich output, and more.
Development
# Enter dev shell (Go 1.26, gopls, golangci-lint)
nix develop
# Run tests
go test ./... -count=1 -timeout 120s -race
# Lint
golangci-lint run ./...
# Format (Nix + Go via treefmt)
nix fmt
# Check everything
nix flake check
See CONTRIBUTING.md for full contribution guidelines.
Documentation
- Tutorial — Build a task manager CLI step by step
- Quick Start Guide — Learn cmdguard in 5 minutes
- Migrating from Cobra — Step-by-step migration guide
- Framework Comparison — vs Kong, sflags, go-flags, urfave/cli
- Performance — Benchmark results and overhead analysis
- CLI Design Principles — Design guidelines
- API Reference — Full API docs on pkg.go.dev
License
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
taskctl
command
Package main demonstrates every major cmdguard feature in a production-grade task manager.
|
Package main demonstrates every major cmdguard feature in a production-grade task manager. |
|
flightrecorder
module
|
|
|
glamour
module
|
|
|
manpage
module
|
|
|
pkg
|
|
|
cmdguard/v2
Package v2 provides a type-safe, dependency-injection-powered CLI framework built on Cobra.
|
Package v2 provides a type-safe, dependency-injection-powered CLI framework built on Cobra. |
|
cmdguard/v2/configload
Package configload provides optional config file loaders for YAML and TOML.
|
Package configload provides optional config file loaders for YAML and TOML. |
|
cmdguard/v2/testutil
Package testutil provides testing utilities for cmdguard v2 consumers.
|
Package testutil provides testing utilities for cmdguard v2 consumers. |
|
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
|