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.
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.go get github.com/larsartmann/cmdguardpackage 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!| 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 |
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.
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.
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.
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)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.
| 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{} },
})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" |
| 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, _ := 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 |
// 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 codeSee the examples/ directory:
basic/— Minimal v2 demotyped/— Full DI, lifecycle hooks, typed flags, nested commandsdi/— Dependency injection patternsdi-patterns/— Service registration patternsenv-tags/— Environment variable bindingscounting/— Counting flags (-v/-vv/-vvv)error-handling/— Error handling patternsoutput/— Rich output formattingadvanced-flags/— Custom flag typesvalidation/— Validation patternssignals/— Signal handling
- Quick Start Guide — Learn cmdguard in 5 minutes
- CLI Design Principles — Design guidelines
- API Reference — Full API docs on pkg.go.dev