close

structcli

package module
v0.18.0 Latest Latest
Warning

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

Go to latest
Published: May 4, 2026 License: MIT Imports: 36 Imported by: 1

Image README

Coverage Documentation GoReportCard

Human-friendly, AI-native CLIs from Go structs

Declare your CLI contract once in Go structs. structcli turns it into flags, env vars, config-file loading, validation, organized help, and machine-readable contracts for agents.

  • Less Cobra/Viper boilerplate
  • Better CLIs for humans
  • Better contracts for automation and LLMs
  • Compiles to WASM out of the box

Stop writing plumbing. Start shipping commands.

⚡ Quick Start

Build with Ona

Start with a plain Go struct:

package main

import (
	"fmt"

	"github.com/leodido/structcli"
	"github.com/spf13/cobra"
	"go.uber.org/zap/zapcore"
)

type Options struct {
	LogLevel zapcore.Level
	Port     int
}

func main() {
	opts := &Options{}
	cli := &cobra.Command{
		Use: "myapp",
		RunE: func(c *cobra.Command, args []string) error {
			fmt.Println(opts) // already populated

			return nil
		},
	}

	structcli.Bind(cli, opts)
	structcli.ExecuteOrExit(cli)
}

Bind creates flags and env vars from your struct and registers it for auto-unmarshal. ExecuteOrExit hydrates the struct from flags, env vars, config, and defaults before your RunE fires.

❯ go run examples/minimal/main.go --help
# Usage:
#   myapp [flags]
#
# Flags:
#   -h, --help                     help for myapp
#       --loglevel zapcore.Level    {debug,info,warn,error,dpanic,panic,fatal} (default info)
#       --port int

Add tags when you want aliases, env vars, shorthand, defaults, and descriptions:

type Options struct {
	LogLevel zapcore.Level `flag:"level" flagdescr:"Set logging level" flagenv:"true"`
	Port     int           `flagshort:"p" flagdescr:"Server port" flagenv:"true" default:"3000"`
}
❯ go run examples/simple/main.go -h
# A simple CLI example
#
# Usage:
#   myapp [flags]
#
# Flags:
#   -h, --help                  help for myapp
#       --level zapcore.Level   Set logging level {debug,info,warn,error,dpanic,panic,fatal} (default info)
#   -p, --port int              Server port (default 3000)
#
# Global Flags:
#       --jsonschema string[="true"]   output JSON Schema and exit (bare: this command, =tree: full subtree)
#       --mcp                          serve MCP over stdio
❯ MYAPP_LOGLEVEL=debug go run examples/simple/main.go
# &{debug 3000}
❯ MYAPP_LOGLEVEL=error MYAPP_PORT=9000 go run examples/simple/main.go --level dpanic
# &{dpanic 9000}

Built-in types like zapcore.Level are validated automatically too.

Out of the box, your CLI supports:

  • 📝 Command-line flags (--level info, -p 8080)
  • 🌍 Environment variables (MYAPP_PORT=8080)
  • 💦 Options precedence (flags > env vars > config file > defaults)
  • ✅ Automatic validation and type conversion
  • 📚 Beautiful help output with proper grouping

Add the AI-native wiring below and it also gains machine-readable JSON Schema, structured JSON errors, semantic exit codes, and optional MCP tool-server mode for agents.

Build AI-Native CLIs

structcli does not just generate flags for humans. It can make your CLI legible to agents too.

Instead of scraping --help and guessing, an agent can discover the contract, call the command correctly, and recover from structured failures.

structcli.Setup(rootCmd,
    structcli.WithJSONSchema(),
    structcli.WithHelpTopics(helptopics.Options{ReferenceSection: true}),  // "mycli env-vars" and "mycli config-keys"
    structcli.WithFlagErrors(),  // Optional, but recommended for typed flag-parse errors
    structcli.WithMCP(),         // Optional, exposes the CLI as an MCP server over stdio
)
structcli.ExecuteOrExit(rootCmd)

With that wiring:

  • --jsonschema exposes flags, defaults, required inputs, enums, and env bindings for the current command; --jsonschema=tree dumps the entire subtree in one call
  • mycli env-vars and mycli config-keys list every environment variable binding and config file key across the command tree
  • HandleError / ExecuteOrExit emit structured JSON errors instead of forcing callers to parse human-oriented output
  • --mcp exposes the same command tree as MCP tools over stdio, with typed inputs and structured tool-call failures
  • semantic exit codes tell the caller whether it should fix input, fix config, retry, or escalate to a human

The same contract spans flags, env vars, config, validation, and enum constraints.

$ mycli srv --jsonschema
{
  "properties": {
    "port": {
      "type": "integer",
      "default": 3000,
      "x-structcli-env-vars": ["MYCLI_SRV_PORT"]
    },
    "secret-key": {
      "type": "string",
      "x-structcli-env-vars": ["MYCLI_SRV_SECRET_KEY"],
      "x-structcli-env-only": true
    }
  },
  "x-structcli-config-flag": "config"
}

Use --jsonschema=tree to dump the entire command subtree in a single call: no need to invoke each subcommand separately:

$ mycli --jsonschema=tree   # JSON array of schemas for all commands
$ mycli srv --jsonschema=tree  # schemas for srv + its subcommands

No --help parsing. No guessing what failed. Just a CLI that can explain itself and fail in machine-actionable ways.

Use exitcode.Category(code) and exitcode.IsRetryable(code) to decide what to do next. See jsonschema.WithEnumInDescription() for schema customization, and pass schema options through WithJSONSchema with jsonschema.Options{SchemaOpts: ...}.

For CLIs that capture output streams during command construction, configure mcp.Options.CommandFactory so each MCP tool call builds a fresh command with the tool-call stdout and stderr writers. This keeps MCP protocol output separate from command output while preserving the existing command tree schema. If the command constructor requires stdin, the factory can wire a non-interactive reader such as strings.NewReader("").

For build-time discovery, generate.WriteAll produces SKILL.md, llms.txt, and AGENTS.md from the same struct definitions: wire it into //go:generate and the files stay in sync automatically.

Read the full AI-native guide or walk through the runnable structured error example.

⬇️ Install

go get github.com/leodido/structcli

📦 Key Features

🧩 Declarative Flags Definition

Define flags once using Go struct tags.

No more boilerplate for Flags().StringVarP, Flags().IntVar, viper.BindPFlag, etc.

Yes, you can nest structs too.

type ServerOptions struct {
	// Basic flags
	Host string `flag:"host" flagdescr:"Server host" default:"localhost"`
	Port int    `flagshort:"p" flagdescr:"Server port" flagrequired:"true" flagenv:"true"`

	// Environment variable binding
	APIKey string `flagenv:"true" flagdescr:"API authentication key"`

	// Network contracts using net families
	BindIP        net.IP     `flag:"bind-ip" flaggroup:"Network" flagdescr:"Bind interface IP" flagenv:"true"`
	BindMask      net.IPMask `flag:"bind-mask" flaggroup:"Network" flagdescr:"Bind interface mask" flagenv:"true"`
	AdvertiseCIDR net.IPNet  `flag:"advertise-cidr" flaggroup:"Network" flagdescr:"Advertised service subnet (CIDR)" flagenv:"true"`
	TrustedPeers  []net.IP   `flag:"trusted-peers" flaggroup:"Network" flagdescr:"Trusted peer IPs (comma separated)" flagenv:"true"`

	// Flag grouping for organized help
	LogLevel zapcore.Level `flag:"log-level" flaggroup:"Logging" flagdescr:"Set log level"`
	LogFile  string        `flag:"log-file" flaggroup:"Logging" flagdescr:"Log file path" flagenv:"true"`

	// Nested structs for organization
	Database DatabaseConfig `flaggroup:"Database"`

	// Enum type (registered via RegisterEnum)
	TargetEnv Environment `flag:"target-env" flagdescr:"Set the target environment" default:"dev"`
}

type DatabaseConfig struct {
	URL      string `flag:"db-url" flagdescr:"Database connection URL"`
	MaxConns int    `flagdescr:"Max database connections" default:"10" flagenv:"true"`
}

See full example for more details.

🛠️ Automatic Environment Variable Binding

Automatically generate environment variables binding them to configuration files (YAML, JSON, TOML, etc.) and flags.

From the previous options struct, you get the following env vars automatically:

  • FULL_SRV_PORT
  • FULL_SRV_APIKEY
  • FULL_SRV_BIND_IP
  • FULL_SRV_BIND_MASK
  • FULL_SRV_ADVERTISE_CIDR
  • FULL_SRV_TRUSTED_PEERS
  • FULL_SRV_DATABASE_MAXCONNS
  • FULL_SRV_LOGFILE, FULL_SRV_LOG_FILE

Every struct field with the flagenv:"true" tag gets an environment variable (two if the struct field also has the flag:"..." tag, see struct field LogFile). Use flagenv:"only" for fields that should be settable exclusively via environment variable or config file: CLI usage (--flag=value) is rejected at runtime.

The prefix of the environment variable name is the CLI name plus the command name to which those options are attached to.

Environment variables are command-scoped for command-local options. For example, if Port is attached to the srv command, FULL_SRV_PORT is used (not FULL_PORT).

⚙️ Configuration File Support

Set up configuration file discovery (flag, environment variable, and fallback paths) via Setup:

structcli.Setup(rootCmd,
    structcli.WithAppName("full"),
    structcli.WithConfig(config.Options{}),
)

Enable strict config-key validation with:

structcli.Setup(rootCmd,
    structcli.WithAppName("full"),
    structcli.WithConfig(config.Options{ValidateKeys: true}),
)

When enabled, Unmarshal fails if command-relevant config contains unknown keys.

WithAppName sets the env prefix. WithConfig registers the --config flag and defers config loading to ExecuteC's bind pipeline (before auto-unmarshal). Ordering between Setup and Bind does not matter: WithAppName retroactively patches env annotations on already-defined flags.

Individual SetupConfig, SetupDebug, etc. remain available for power users who need fine-grained control.

The line above:

  • creates --config global flag
  • creates FULL_CONFIG env var
  • sets /etc/full/, $HOME/.full/, $PWD/.full/ as fallback paths for config.yaml

Magic, isn't it?

What's left? Tell your CLI to load the configuration file (if any).

rootC.PersistentPreRunE = func(c *cobra.Command, args []string) error {
	_, configMessage, configErr := structcli.UseConfigSimple(c)
	if configErr != nil {
		return configErr
	}
	if configMessage != "" {
		c.Println(configMessage)
	}

	return nil
}

UseConfigSimple(c) loads config into the root config scope and merges only the relevant section into c's effective scope.

🧠 Viper Model Scopes

structcli uses two different viper scopes on purpose:

  • structcli.GetConfigViper(rootOrLeafCmd) -> root-scoped config source (config file data tree)
  • structcli.GetViper(cmd) -> command-scoped effective values (flags/env/defaults + command-relevant config)

This separation keeps config-file loading isolated from runtime command state.

If you need imperative values in tests or application code, write to the right scope:

// 1) Effective override for one command context
structcli.GetViper(cmd).Set("timeout", 60)

// 2) Config-tree style injection (top-level + command section)
structcli.GetConfigViper(rootCmd).Set("srv", map[string]any{
  "port": 8443,
})

Global viper.Set(...) is not used by structcli.Unmarshal(...) resolution. Use GetViper/GetConfigViper instead.

📜 Configuration Is First-Class Citizen

Configuration can mirror your command hierarchy.

Settings can be global (at the top level) or specific to a command or subcommand. The most specific section always takes precedence.

# Global settings apply to all commands unless overridden by a specific section.
# `dryrun` matches the `DryRun` struct field name.
dryrun: true
verbose: 1 # A default verbosity level for all commands.

# Config for the `srv` command (`full srv`)
srv:
  # `port` matches the `Port` field name.
  port: 8433
  # Network options
  bind-ip: "10.20.0.10"
  bind-mask: "ffffff00"
  advertise-cidr: "10.20.0.0/24"
  trusted-peers: "10.20.0.11,10.20.0.12"
  # `log-level` matches the `flag:"log-level"` tag.
  log-level: "warn"
  # `logfile` matches the `LogFile` field name.
  logfile: /var/log/mysrv.log

  # Flattened keys can set options in nested structs.
  # `db-url` (from `flag:"db-url"` tag) maps to ServerOptions.Database.URL.
  db-url: "postgres://user:pass@db/prod"

  # Nested keys are also supported.
  database:
    # Struct field key style
    url: "postgres://user:pass@db/prod"
    # Alias key style (from `flag:"db-url"`)
    db-url: "postgres://user:pass@db/prod"

# Config for the `usr` command group.
usr:
  # This nested section matches the `usr add` command (`full usr add`).
  # Its settings are ONLY applied to 'usr add'.
  add:
    name: "Config User"
    email: "config.user@example.com"
    age: 42
    # Command specific override
    dry: false
# NOTE: Per the library's design, there is no other fallback other than from the top-level.
# A command like 'usr delete' would ONLY use the global keys above (if those keys/flags are attached to it),
# as an exact 'usr.delete' section is not defined.

This configuration system supports:

  • Hierarchical Structure: Nest keys to match your command path (e.g., usr: { add: { ... } }).
  • Strict Precedence: Only settings from the global scope and the exact command path section are merged. There is no automatic fallback to parent command sections.
  • Flexible Keys: You can use struct field names and aliases (flag:"...") in both flattened and nested forms.
  • Supported Forms for Nested Fields: db-url, database.url, database: { url: ... }, and database: { db-url: ... }.

✅ Built-in Validation & Transformation

Supports validation, transformation, and custom flag type definitions through simple interfaces.

Your struct must implement Options (via Attach) and can optionally implement ValidatableOptions and TransformableOptions.

type UserConfig struct {
	Email string `flag:"email" flagdescr:"User email" validate:"email"`
	Age   int    `flag:"age" flagdescr:"User age" validate:"min=18,max=120"`
	Name  string `flag:"name" flagdescr:"User name" mod:"trim,title"`
}

func (o *ServerOptions) Validate(ctx context.Context) []error {
    // Automatic validation
}

func (o *ServerOptions) Transform(ctx context.Context) error {
    // Automatic transformation
}

See a full working example here.

🚧 Automatic Debugging Support

Create a --debug-options flag (plus a matching env var) for troubleshooting config/env/flags resolution.

structcli.Setup(rootCmd, structcli.WithDebug(debug.Options{}))

Or standalone: structcli.SetupDebug(rootCmd, debug.Options{}).

The flag accepts text (default when used bare) or json for machine-readable output. Truthy values like true, 1, yes are treated as text for backward compatibility.

Text output: an aligned table showing each flag's resolved value and where it came from:

❯ go run examples/full/main.go srv --debug-options --config examples/full/config.yaml -p 3333
# ...
# Command: full srv
#
# Flags:
#   --apikey                 secret-api-key                       (default)
#   --config                 examples/full/config.yaml            (flag)
#   --database.maxconns      3                                    (default)
#   --db-url                 postgres://user:pass@localhost/mydb  (default)
#   --debug-options          text                                 (flag)
#   --host                   production-server                    (default)
#   --log-file               /var/log/mysrv.log                   (default)
#   --log-level              debug                                (default)
#   --port                   3333                                 (flag)
#   --target-env             dev                                  (default)
#   ...
#
# Values:
#   apikey: secret-api-key
#   host: production-server
#   log-level: debug
#   port: 3333
#   ...

JSON output: structured data for AI agents and tooling:

❯ go run examples/full/main.go srv --debug-options=json --config examples/full/config.yaml -p 3333
# ...
# {
#   "command": "full srv",
#   "flags": [
#     ...
#     {"name": "config", "value": "examples/full/config.yaml", "default": "", "changed": true, "source": "flag"},
#     {"name": "db-url", "value": "postgres://user:pass@localhost/mydb", "default": "", "changed": false, "source": "default"},
#     {"name": "log-level", "value": "debug", "default": "info", "changed": false, "source": "default"},
#     {"name": "port", "value": "3333", "default": "0", "changed": true, "source": "flag"},
#     ...
#   ],
#   "values": {"apikey": "secret-api-key", "host": "production-server", "log-level": "debug", "port": 3333, ...}
# }

Source attribution resolves each flag to flag (CLI), env, config, or default. For env-sourced flags, the text output includes the variable name (e.g., (env: MYAPP_LOG_LEVEL)).

The flag can also be activated via environment variable: FULL_DEBUG_OPTIONS=json.

📋 Self-Documenting Help Topics

WithHelpTopics (or standalone SetupHelpTopics) adds two help topic commands that list every environment variable binding and every valid configuration file key across the command tree.

structcli.Setup(rootCmd, structcli.WithHelpTopics(helptopics.Options{}))

Call this after all subcommands and flags are defined (typically right before ExecuteOrExit).

By default the commands appear as regular subcommands under "Available Commands:". Set ReferenceSection: true to move them into a dedicated "Reference:" section instead.

Environment variable listing: mycli env-vars:

Environment Variables

  mycli (global):
    MYCLI_VERBOSE  --verbose  bool           false

  mycli serve:
    MYCLI_SERVE_HOST  --host  string         localhost
    MYCLI_SERVE_PORT  --port  int            8080

Configuration key listing: mycli config-keys:

Configuration Keys

  Config flag: --config
  Supported formats: yaml, json, toml. Searches: $HOME/.mycli, /etc/mycli

  mycli (global):
    output   --output   string         text
    verbose  --verbose  bool           false

  mycli serve:
    host      --host      string         localhost
    port      --port      int            8080
    tls-cert  --tls-cert  string         ""

  Keys can be nested under the command name in the config file.

With ReferenceSection: true, both topics appear under "Reference:" in --help output instead of "Available Commands:". Flags marked flagenv:"only" show an (env-only) suffix in the env-vars listing and are excluded from config-keys (since they are hidden). Config keys derived from embedded struct field paths appear as aliases (e.g., database.maxconnsalias for --database.maxconns).

For machine-readable cross-tree data, use --jsonschema=tree instead: it provides the same information in structured JSON.

↪️ Sharing Options Between Commands

In complex CLIs, multiple commands often need access to the same global configuration and shared resources (like a logger or a database connection). structcli provides a pattern using the ContextInjector interface to achieve this without resorting to global variables, by propagating a single "source of truth" through the command context.

The pattern allows you to:

  • Populate a shared options struct once from flags, environment variables, or a config file.
  • Initialize "computed state" (like a logger) based on those options.
  • Share this single, fully-prepared "source of truth" with any subcommand that needs it.
🍩 In a Nutshell

Create a shared struct that implements the ContextInjector interface. This struct will hold both the configuration flags and the computed state (e.g., the logger).

// This struct holds our shared state.
type CommonOptions struct {
    LogLevel zapcore.Level `flag:"loglevel" flagdescr:"Logging level" default:"info"`
    Logger   *zap.Logger   `flagignore:"true"` // This field is computed, not a flag.
}

// Context injects the struct into the command context during auto-unmarshal.
func (o *CommonOptions) Context(ctx context.Context) context.Context { /* ... */ }

// FromContext retrieves the struct from context (user-side, not interface-enforced).
func (o *CommonOptions) FromContext(ctx context.Context) error { /* ... */ }

// Initialize is a custom method to create the computed state.
func (o *CommonOptions) Initialize() error { /* ... */ }

Bind the shared struct to the root command. Bind registers it for auto-unmarshal: the bind pipeline populates it and calls Context() to inject it before any PreRunE or RunE fires.

structcli.Bind(rootC, commonOpts)

Important: Bind on root creates local flags on the root command. By default, Cobra rejects unknown flags before finding the subcommand, so app --loglevel info sub would fail. Set rootC.TraverseChildren = true so root parses its own flags first, then resolves the subcommand. Alternatively, bind the shared struct on each leaf command that needs the flags.

If you need to initialize computed state (like a logger) after unmarshal, use a PersistentPreRunE hook: by the time it fires, commonOpts is already populated:

rootC.PersistentPreRunE = func(c *cobra.Command, args []string) error {
	// commonOpts is already populated by the bind pipeline.
	return commonOpts.Initialize()
}

Retrieve the state in subcommands. In your subcommand's RunE, call .FromContext() to retrieve the shared, initialized object.

func(c *cobra.Command, args []string) error {
    config := &CommonOptions{}
    if err := config.FromContext(c.Context()); err != nil {
        return err
    }
    config.Logger.Info("Executing subcommand...")

    return nil
},

This pattern ensures that subcommands remain decoupled while having access to a consistent, centrally-managed state.

Note: The deprecated ContextOptions interface (which embeds Options and requires Attach) still works for backward compatibility. New code should use ContextInjector instead.

For a complete, runnable implementation of this pattern, see the loginsvc example located in the /examples/loginsvc directory.

🎯 Enum Registration

Register string or integer enum types once in init() and use them as plain struct fields. structcli handles flag creation, help text with allowed values, shell completion, validation, and config/env decoding automatically.

String enums (RegisterEnum)
type Environment string

const (
	EnvDev  Environment = "dev"
	EnvProd Environment = "prod"
)

func init() {
	structcli.RegisterEnum[Environment](map[Environment][]string{
		EnvDev:  {"dev", "development"},   // first string is canonical, rest are aliases
		EnvProd: {"prod", "production"},
	})
}

type DeployOptions struct {
	TargetEnv Environment `flag:"target-env" flagdescr:"Target environment" default:"dev" flagenv:"true"`
}

This produces --target-env with help text showing {dev,prod}, shell completion for all values including aliases, and case-insensitive parsing that accepts both prod and production.

Integer enums (RegisterIntEnum)
type Priority int

const (
	PriorityLow    Priority = 0
	PriorityMedium Priority = 1
	PriorityHigh   Priority = 2
)

func init() {
	structcli.RegisterIntEnum[Priority](map[Priority][]string{
		PriorityLow:    {"low"},
		PriorityMedium: {"medium", "med"},
		PriorityHigh:   {"high", "hi"},
	})
}

Both functions panic on duplicate registration or empty values. Call them in init() before any Define() calls.

See full example for enum registration in a complete CLI.

🪃 Custom Type Handlers

For types that need custom parsing logic beyond what enum registration provides, structcli offers two mechanisms: per-type registration and per-field hooks.

Per-type: RegisterType[T]

Register custom hooks for a type once in init(). Every struct field of type T uses them automatically: no special tags needed. RegisterType panics on nil hooks or duplicate registration: call it in init() before any Define/Bind.

import (
    "github.com/leodido/structcli"
    "github.com/leodido/structcli/values"
)

type ListenAddress struct {
    Host string
    Port int
    raw  string
}

func init() {
    structcli.RegisterType[ListenAddress](structcli.TypeHooks[ListenAddress]{
        Define: func(name, descr string, sf reflect.StructField, fv reflect.Value) (pflag.Value, string) {
            ptr := fv.Addr().Interface().(*ListenAddress)
            *ptr = ListenAddress{Host: "localhost", Port: 8080, raw: "localhost:8080"}

            return values.NewString(&ptr.raw), descr + " (host:port)"
        },
        Decode: func(input any) (any, error) {
            return ParseListenAddress(input.(string))
        },
    })
}

type ServerOptions struct {
    ListenAddr ListenAddress `flag:"listen" flagdescr:"Listen address"`
}

Both Define and Decode are required. Panics on duplicate registration or nil hooks. Call in init() before any Define/Bind calls.

For enum types, prefer RegisterEnum/RegisterIntEnum: they wrap RegisterType with less boilerplate.

Per-field: FieldHookProvider and FieldCompleter

When the same type needs different behavior in different fields, or when a standard type needs custom handling for a specific field, implement FieldHookProvider on your options struct. Map keys are struct field names.

type ServerOptions struct {
    ListenAddr string `flag:"listen" flagdescr:"Listen address"`
    Mode       string `flag:"mode" flagdescr:"Server mode"`
}

func (o *ServerOptions) FieldHooks() map[string]structcli.FieldHook {
    return map[string]structcli.FieldHook{
        "ListenAddr": {
            Define: func(name, descr string, sf reflect.StructField, fv reflect.Value) (pflag.Value, string) {
                ptr := fv.Addr().Interface().(*string)
                *ptr = "localhost:8080"

                return values.NewString(ptr), descr + " (host:port)"
            },
            Decode: func(input any) (any, error) {
                return input, nil
            },
        },
    }
}

For shell completion, implement FieldCompleter:

func (o *ServerOptions) CompletionHooks() map[string]structcli.CompleteHookFunc {
    return map[string]structcli.CompleteHookFunc{
        "ListenAddr": func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
            return []string{"localhost:8080", "0.0.0.0:8080", "0.0.0.0:443"}, cobra.ShellCompDirectiveNoFileComp
        },
    }
}

FieldCompleter works for any field that becomes a flag, not only fields with FieldHookProvider hooks.

Precedence

Hook resolution follows this order (highest to lowest):

  1. FieldHookProvider: per-field hooks on the options struct
  2. RegisterType / RegisterEnum: per-type hooks in the global registry
  3. Built-in registry: time.Duration, zapcore.Level, slog.Level, etc.

Completion precedence:

  • If a completion function is already registered on a flag before Define, structcli preserves it.
  • If Define auto-registers a FieldCompleter hook, a later manual RegisterFlagCompletionFunc on the same flag returns Cobra's already registered error.

In values we provide pflag.Value implementations for standard types.

See the custom types example for a runnable demo of all three mechanisms, or the full example for a complete CLI.

🧱 Built-in Custom Types

Type Description Example Values Special Features
zapcore.Level Zap logging levels debug, info, warn, error, dpanic, panic, fatal Enum validation
slog.Level Standard library logging levels debug, info, warn, error, error+2, ... Level offsets: ERROR+2, INFO-4
time.Duration Time durations 30s, 5m, 2h, 1h30m Go duration parsing
[]time.Duration Duration slices 30s,5m, 1s,2m30s Comma-separated / repeated flags
[]bool Boolean slices true,false,true Comma-separated / repeated flags
[]uint Unsigned integer slices 1,2,3,42 Comma-separated / repeated flags
[]byte Raw textual bytes hello, abc123 Raw textual input
structcli.Hex Hex-decoded textual input 68656c6c6f, 48656c6c6f Hex decoding
structcli.Base64 Base64-decoded textual input aGVsbG8=, YWJjMTIz Base64 decoding
net.IP IP address 127.0.0.1, 10.42.0.10, 2001:db8::1 IP parsing
net.IPMask IPv4 mask 255.255.255.0, ffffff00 Dotted or hex mask parsing
net.IPNet CIDR subnet 10.42.0.0/24, 2001:db8::/64 CIDR parsing
[]net.IP IP slices 10.0.0.1,10.0.0.2 Comma-separated / repeated flags
[]string String slices item1,item2,item3 Comma-separated
[]int Integer slices 1,2,3,42 Comma-separated
map[string]string String maps env=prod,team=platform key=value pairs
map[string]int Integer maps cpu=2,memory=4 key=value pairs with int parsing
map[string]int64 64-bit integer maps ok=1,fail=2 key=value pairs with int64 parsing

Note on JSON output: net.IPMask is a byte slice under the hood, so Go's encoding/json renders it as base64 (for example 255.255.255.0 appears as ////AA==). This is expected.

All built-in types support:

  • Command-line flags with validation and help text
  • Environment variables with automatic binding
  • Configuration files (YAML, JSON, TOML)
  • Type validation with helpful error messages

Slices and maps use the same contract across flags, env vars, and config.

See examples/collections/main.go for a runnable version of this example.

type AdvancedOptions struct {
	Retries   []uint          `flag:"retries" flagenv:"true"`
	Backoffs  []time.Duration `flag:"backoffs" flagenv:"true"`
	FeatureOn []bool          `flag:"feature-on" flagenv:"true"`
	Labels    map[string]string `flag:"labels" flagenv:"true"`
	Limits    map[string]int    `flag:"limits" flagenv:"true"`
	Counts    map[string]int64  `flag:"counts" flagenv:"true"`
}
❯ myapp --retries 1,2,3 --backoffs 1s,5s --feature-on true,false --labels env=prod,team=platform --limits cpu=8,memory=16 --counts ok=10,fail=3
❯ MYAPP_RETRIES=1,2,3 MYAPP_BACKOFFS=1s,5s MYAPP_FEATURE_ON=true,false MYAPP_LABELS=env=prod,team=platform MYAPP_LIMITS=cpu=8,memory=16 MYAPP_COUNTS=ok=10,fail=3 myapp
❯ go run examples/collections/main.go --config examples/collections/config.yaml
retries: "1,2,3"
backoffs:
  - 1s
  - 5s
feature-on: "true,false"
labels:
  env: prod
  team: platform
limits:
  cpu: 8
  memory: 16
counts: "ok=10,fail=3"

🧰 Reusable Flag Kits

The flagkit package provides pre-built, embeddable flag structs that standardize common CLI flag declarations. Each type encapsulates one flag with an opinionated name, type, and default matching industry conventions. This gives AI agents and scripts a consistent vocabulary across CLIs built with structcli.

import "github.com/leodido/structcli/flagkit"

type LogsOptions struct {
    flagkit.Follow                                                    // --follow/-f (default: false)
    Service string `flag:"service" flagshort:"s" flagdescr:"Service name" flagrequired:"true"`
}

func (o *LogsOptions) Attach(c *cobra.Command) error {
    if err := structcli.Define(c, o); err != nil {
        return err
    }
    flagkit.AnnotateCommand(c) // marks flagkit-owned flags for doc generation
    return nil
}

Attach is needed here because of the custom flagkit.AnnotateCommand call. For structs without custom define-time logic, use structcli.Bind(cmd, opts) instead: it handles both plain structs and Options implementors.

Available types:

Type Flag Default Description
Follow --follow / -f false Opt-in streaming (agents won't hang)
LogLevel --log-level info Log level via zapcore (alias for ZapLogLevel)
ZapLogLevel --log-level info Log level backed by zapcore.Level
SlogLogLevel --log-level info Log level backed by slog.Level (stdlib)
Output --output / -o text Output format (string enum, user-registered)
Verbose --verbose / -v 0 Verbosity count (-v, -vv, -vvv)
DryRun --dry-run false Preview without making changes
Timeout --timeout 30s Operation timeout (time.Duration)
Quiet --quiet / -q false Suppress non-essential output

When the generate package detects flagkit annotations, it emits a "Development Notes" section in AGENTS.md guiding AI coding agents to prefer flagkit types over ad-hoc flag declarations.

See go doc github.com/leodido/structcli/flagkit for the full taxonomy and composition examples.

🎨 Beautiful, Organized Help Output

Organize your --help output into logical groups for better readability.

❯ go run examples/full/main.go --help
# A demonstration of the structcli library with beautiful CLI features
#
# Usage:
#   full [flags]
#   full [command]
#
# Available Commands:
#   completion  Generate the autocompletion script for the specified shell
#   help        Help about any command
#   logs        Show service logs
#   preset      Demonstrate flag presets with validation and transformation
#   srv         Start the server
#   usr         User management
#
# Flags:
#   -h, --help   help for full
#
# Utility Flags:
#       --dry             
#   -v, --verbose count
#
# Global Flags:
#       --config string                   config file (fallbacks to: {/etc/full,{executable_dir}/.full,$HOME/.full,...}/config.{yaml,json,toml})
#       --debug-options string[="text"]   debug output format (text, json)
#       --jsonschema string[="true"]      output JSON Schema and exit (bare: this command, =tree: full subtree)
#       --mcp                             serve MCP over stdio
#
# Reference:
#   config-keys List all configuration file keys
#   env-vars    List all environment variable bindings
❯ go run examples/full/main.go srv --help
# Start the server with the specified configuration
#
# Usage:
#   full srv [flags]
#   full srv [command]
#
# Available Commands:
#   version     Print version information
#
# Flags:
#       --apikey string                  API authentication key
#       --deep-setting string             (default "default-deep-setting")
#       --deep.deeper.nodefault string
#       --deeper-setting string           (default "default-deeper-setting")
#   -h, --help                           help for srv
#       --host string                    Server host (default "localhost")
#   -p, --port int                       Server port
#       --target-env string              Set the target environment {dev,prod,staging} (default "dev")
#
# Database Flags:
#       --database.maxconns int   Max database connections (default 10)
#       --db-url string           Database connection URL
#
# Logging Flags:
#       --log-file string           Log file path
#       --log-level zapcore.Level   Set log level {debug,info,warn,error,dpanic,panic,fatal} (default info)
#
# Network Flags:
#       --advertise-cidr ipNet    Advertised service subnet (CIDR) (default 127.0.0.0/24)
#       --bind-ip ip              Bind interface IP (default 127.0.0.1)
#       --bind-mask ipMask        Bind interface mask (default ffffff00)
#       --trusted-peers ipSlice   Trusted peer IPs (comma separated) (default 127.0.0.2,127.0.0.3)
#
# Security Flags:
#       --token-base64 bytesBase64   Token bytes encoded as base64 (default aGVsbG8=)
#       --token-hex bytesHex         Token bytes encoded as hex (default 68656c6c6f)
#
# Global Flags:
#       --config string                   config file (fallbacks to: {/etc/full,{executable_dir}/.full,$HOME/.full,...}/config.{yaml,json,toml})
#       --debug-options string[="text"]   debug output format (text, json)
#       --jsonschema string[="true"]      output JSON Schema and exit (bare: this command, =tree: full subtree)
#       --mcp                             serve MCP over stdio
#
# Use "full srv [command] --help" for more information about a command.

🌐 WebAssembly (WASM)

structcli avoids reflect.Value.MethodByName and reflect.Value.Call, so the Go compiler's dead-code elimination (DCE) works correctly and WASM binaries stay lean. All features work under GOOS=wasip1 GOARCH=wasm with the standard Go compiler, including custom type hooks (RegisterType[T], FieldHookProvider), JSON Schema generation, structured errors, and debug output.

Build and run any structcli program as WASM:

GOOS=wasip1 GOARCH=wasm go build -o myapp.wasm .
wasmtime run --dir=/ myapp.wasm serve --port 8080

This means structcli CLIs can run in sandboxed WASM runtimes, edge functions, and browser-based environments. Combined with --jsonschema and --mcp, an agent can discover the CLI contract, call it in a sandboxed WASM runtime, and handle structured failures without ever touching the host OS.

🏷️ Available Struct Tags

Use these tags in your struct fields to control the behavior:

Tag Description Example
flag Sets a custom name for the flag (otherwise, generated from the field name) flag:"log-level"
flagpreset Defines CLI-only preset aliases for this field's flag. Each preset is <alias-flag-name>=<value-for-this-field-flag>. No env/config keys are created. flagpreset:"logeverything=5;logquiet=0"
flagshort Sets a single-character shorthand for the flag flagshort:"l"
flagdescr Provides the help text for the flag flagdescr:"Logging level"
default Sets the default value for the flag default:"info"
flagenv Enables binding to an environment variable ("true", "false", or "only") flagenv:"true"
flagrequired Marks the flag as required ("true"/"false") flagrequired:"true"
flaghidden Hides the flag from help/usage output and machine-readable schemas while keeping it fully functional ("true"/"false") flaghidden:"true"
flaggroup Assigns the flag to a group in the help message flaggroup:"Database"
flagignore Skips creating a flag for this field ("true"/"false") flagignore:"true"
flagtype Specifies a special flag type. Currently supports count flagtype:"count"

flagpreset is syntactic sugar: it creates alias flags that set the canonical flag value. Format: <alias>=<value>; multiple entries can be separated by ; or ,. Example: flagpreset:"logeverything=5;logquiet=0" makes --logeverything behave like --loglevel=5. If both alias and canonical flags are passed, the last assignment in argv wins. It does not bypass transform/validate flow.

flaghidden:"true" + flagenv:"true" vs flagenv:"only":

  • flaghidden:"true" + flagenv:"true": hidden from help, but accepts CLI input via --flag=value. Use for flags that should be discoverable only by advanced users or scripts.
  • flagenv:"only": hidden from help, rejects CLI input at runtime. The field is settable only via environment variable or config file. Use for secrets and deployment-time configuration that should never appear on a command line.

flagenv:"only" is incompatible with flagshort, flagpreset, and flagtype (these are CLI-only concepts). It supports flagdescr, flaggroup, flagrequired, and default. FieldHookProvider Define/Decode hooks work normally on flagenv:"only" fields (the flag is created then hidden). FieldCompleter hooks are skipped since hidden flags have no CLI completion.

📖 Documentation

For comprehensive documentation and advanced usage patterns, visit the documentation.

Start here for repo-local guides:

🤝 Contributing

Contributions are welcome!

Please feel free to submit a Pull Request.

Image Documentation

Index

Constants

View Source
const (
	// DefaultValidateTagName is the struct tag name for validation rules.
	// Matches the go-playground/validator default. Exported so callers
	// configuring their own validator can reference the structcli default
	// (e.g. validator.New().SetTagName(structcli.DefaultValidateTagName)).
	DefaultValidateTagName = "validate"

	// DefaultModTagName is the struct tag name for transformation rules.
	// Matches the go-playground/mold default. Exported so callers
	// configuring their own mold can reference the structcli default.
	DefaultModTagName = "mod"
)
View Source
const (

	// ConfigFlagAnnotation is the command annotation key that stores the
	// config flag name set by WithConfig. Exported for use by generate/.
	ConfigFlagAnnotation = "leodido/structcli/config-flag"
)
View Source
const Version = "0.18.0"

Version is the current release version of structcli. It MUST match the git tag (without the "v" prefix) at release time. The release CI workflow verifies this.

Variables

This section is empty.

Functions

func Bind added in v0.17.0

func Bind(c *cobra.Command, opts any) error

Bind defines flags from opts on cmd and registers opts for auto-unmarshal during ExecuteC/ExecuteOrExit.

opts must be a non-nil struct pointer. If opts implements Options (has Attach), Attach is called. Otherwise flags are defined directly from struct tags.

Multiple Bind calls per command are supported; unmarshal order matches call order (FIFO). Define runs immediately; flags exist on the command after Bind returns.

As a side effect, the first Bind call on a command tree installs a cobra.Command.PersistentPreRunE on root that warns to stderr when the tree is executed via cmd.Execute() instead of ExecuteC. This warning is best-effort: it is suppressed when a child command defines its own PersistentPreRunE (Cobra only runs the nearest ancestor's hook), and it can be overwritten if the caller sets root.PersistentPreRunE after calling Bind.

func Define

func Define(c *cobra.Command, o Options, defineOpts ...DefineOption) error

Define creates flags from struct field tags and binds them to the command.

It processes struct tags to generate appropriate cobra flags, handles environment variable binding, sets up flag groups, and configures the usage template.

func EnvPrefix

func EnvPrefix() string

EnvPrefix returns the current global environment variable prefix without the trailing underscore.

func ExecuteC added in v0.17.0

func ExecuteC(cmd *cobra.Command) (*cobra.Command, error)

ExecuteC prepares the command tree for execution and delegates to cmd.ExecuteC().

Preparation (idempotent, safe to call multiple times on the same tree):

  • Sets SilenceErrors and SilenceUsage on the root command.
  • Runs SetupUsage on every command in the tree.
  • Recursively wraps PersistentPreRunE on every command to run the bind pipeline (auto-unmarshal for all Bind-registered options, root-to-leaf, FIFO per command).
  • When WithConfig was used in Setup, auto-loads config (UseConfigSimple) once before the first auto-unmarshal.
  • Skips the bind pipeline when execution is intercepted (--jsonschema, --mcp).
  • Preserves any user-set PersistentPreRunE or PersistentPreRun.
  • Warns (once per tree) if non-leaf commands have Bind-registered local flags but root.TraverseChildren is false.
  • Suppresses the Bind warning hook by setting an annotation that is cleared after execution returns.

Returns the executed subcommand and any error.

func ExecuteOrExit added in v0.13.0

func ExecuteOrExit(cmd *cobra.Command)

ExecuteOrExit is a convenience wrapper around ExecuteC for the common main() pattern. On error it writes structured JSON to stderr and exits with a semantic exit code. On success it exits 0.

func main() {
    structcli.ExecuteOrExit(buildMyCLI())
}

func GetConfigViper added in v0.10.0

func GetConfigViper(c *cobra.Command) *viper.Viper

GetConfigViper returns the root-scoped config-source viper for c.

SetupConfig/UseConfig read configuration file data into this viper. Unmarshal then merges command-relevant settings from this viper into the effective command-scoped viper returned by GetViper.

Use this viper for imperative config-tree style injection (eg. top-level keys and command sections). Use GetViper for direct command-effective overrides.

func GetOrSetAppName

func GetOrSetAppName(name, cName string) string

GetOrSetAppName resolves the app name consistently.

When name is given, use it (and set as prefix if none exists). When cName is given, use it if no prefix exists, or if existing prefix matches cName. Otherwise, when an environment prefix already exists, return the app name that corresponds to it. Finally, it falls back to empty string.

func GetViper

func GetViper(c *cobra.Command) *viper.Viper

GetViper returns the effective command-scoped viper associated with c.

This is the runtime source used by Unmarshal and includes flags, env vars, defaults, plus command-relevant config merged from the root-scoped config viper.

Use this for imperative overrides that must affect option resolution for c.

func HandleError added in v0.13.0

func HandleError(cmd *cobra.Command, err error, w io.Writer) int

HandleError classifies err, writes a JSON StructuredError to w, and returns a semantic exit code.

The cmd parameter must be the command where the error originated, not the root command. This is because HandleError looks up flag metadata (type, enum values, env var bindings) from cmd's flag annotations to produce accurate error details. If the root command is passed for a subcommand error, the metadata lookup yields empty results and the output is degraded (no expected type, no enum check, no env var attribution).

Use ExecuteOrExit to get this right automatically. It uses cobra's ExecuteC to obtain the correct command. If calling HandleError directly, use cobra.Command.ExecuteC:

cmd, err := rootCmd.ExecuteC()
if err != nil {
    os.Exit(structcli.HandleError(cmd, err, os.Stderr))
}

HandleError has no side effects beyond reading the current process environment to improve source attribution and writing the structured error JSON to w.

If err is nil, HandleError returns exitcode.OK and writes nothing.

func IsDebugActive

func IsDebugActive(c *cobra.Command) bool

IsDebugActive checks if the debug option is set for the command c, either through a command-line flag or an environment variable.

func IsHelpTopicCommand added in v0.16.0

func IsHelpTopicCommand(c *cobra.Command) bool

IsHelpTopicCommand returns true if the command was registered by SetupHelpTopics.

func RegisterEnum added in v0.16.0

func RegisterEnum[E ~string](values map[E][]string)

RegisterEnum registers a string-based enum type for automatic flag handling. After registration, struct fields of type E work without any special tag or interface. This is a convenience wrapper over RegisterType for the common case of string-based enums with named values and aliases.

values maps each enum constant to its string representations. The first string in each slice is the canonical name shown in help text and shell completion; additional strings are accepted as aliases during parsing (case-insensitive). Canonical names appear sorted alphabetically in help text.

Must be called in init() before any Define() calls. Panics if the type is already registered (duplicate registration or conflict with a built-in), or if values is empty.

Example:

type Environment string
const (
    EnvDev  Environment = "dev"
    EnvProd Environment = "prod"
)

func init() {
    structcli.RegisterEnum[Environment](map[Environment][]string{
        EnvDev:  {"dev", "development"},
        EnvProd: {"prod", "production"},
    })
}

func RegisterIntEnum added in v0.16.0

func RegisterIntEnum[E ~int | ~int8 | ~int16 | ~int32 | ~int64](values map[E][]string)

RegisterIntEnum registers an integer-based enum type for automatic flag handling. Same semantics as RegisterEnum but for types with a signed integer underlying type (e.g., custom iota-based enums). Uses enumflag/v2 internally for flag parsing.

Values appear in help text sorted by their integer value.

Unsigned integer types (~uint, ~uint8, etc.) are not supported. For those, use RegisterType with manual Define/Decode hooks.

Must be called in init() before any Define() calls. Panics if the type is already registered, or if values is empty.

Example:

type Priority int
const (
    PriorityLow    Priority = 0
    PriorityMedium Priority = 1
    PriorityHigh   Priority = 2
)

func init() {
    structcli.RegisterIntEnum[Priority](map[Priority][]string{
        PriorityLow:    {"low"},
        PriorityMedium: {"medium", "med"},
        PriorityHigh:   {"high", "hi"},
    })
}

func RegisterType added in v0.18.0

func RegisterType[T any](hooks TypeHooks[T])

RegisterType registers custom flag hooks for type T.

After registration, struct fields of type T work without any special tag or interface. The define hook is called once per field during Define/Bind; the decode hook is called during Unmarshal for env/config values.

Must be called in init() before any Define/Bind calls. Panics if T is already registered (duplicate or conflict with a built-in). Panics if Define is nil. Panics if Decode is nil.

func Reset

func Reset()

func SetEnvPrefix

func SetEnvPrefix(str string)

SetEnvPrefix sets the global environment variable prefix for the application.

The prefix is automatically appended with an underscore when generating environment variable names.

func Setup added in v0.17.0

func Setup(cmd *cobra.Command, opts ...SetupOption) error

Setup configures the root command with the selected features.

It calls the underlying Setup* functions in the correct internal order. Individual Setup* functions remain available for power users.

Most With* options are variadic: call them with no arguments for defaults (e.g., WithJSONSchema()) WithDebug is the exception: it requires an explicit debug.Options because AppName and Exit have no sensible defaults.

Ordering is handled internally:

  1. AppName + env annotation patching (if flags already exist from earlier Bind calls)
  2. Config (registers --config flag, defers auto-load to ExecuteC)
  3. Debug (registers --debug-options flag)
  4. JSON Schema (registers --jsonschema flag, wraps execution)
  5. Help Topics (adds help topic subcommands)
  6. Flag Errors (intercepts flag parsing errors)
  7. MCP (registers --mcp flag, wraps execution)

func SetupConfig

func SetupConfig(rootC *cobra.Command, cfgOpts config.Options) error

SetupConfig creates the --config global flag and wires config discovery for the root command.

Works only for the root command.

Call this before attaching/defining options when you rely on app-prefixed environment variables (eg. FULL_*), because SetupConfig is what initializes the global env prefix used while defining env annotations.

Configuration file data is loaded into a root-scoped config viper (see GetConfigViper), then merged into the active command scoped viper during UseConfig/Unmarshal.

Set config.Options.ValidateKeys to enable strict config-key validation during Unmarshal for command-relevant config entries.

func SetupDebug

func SetupDebug(rootC *cobra.Command, debugOpts debug.Options) error

SetupDebug creates the --debug-options global flag and sets up debug behavior.

Works only for the root command.

func SetupFlagErrors added in v0.13.0

func SetupFlagErrors(rootC *cobra.Command)

SetupFlagErrors installs a flag error interceptor on the root command.

When active, cobra's flag parsing errors (invalid values, unknown flags) are wrapped in typed structclierrors.FlagError values. HandleError then uses errors.As to classify them, eliminating regex parsing at classification time.

Call this on the root command before Execute():

structcli.SetupFlagErrors(rootCmd)

This is optional. If not called, HandleError falls back to regex-based classification of cobra's string errors.

func SetupHelpTopics added in v0.16.0

func SetupHelpTopics(rootC *cobra.Command, opts helptopics.Options) error

SetupHelpTopics adds "env-vars" and "config-keys" reference commands to the root command. By default they appear as regular subcommands under "Available Commands:". Set ReferenceSection to move them into a dedicated "Reference:" section.

Text is generated lazily at invocation time, so commands added after this call are included.

func SetupJSONSchema added in v0.13.0

func SetupJSONSchema(rootC *cobra.Command, opts jsonschema.Options) error

SetupJSONSchema adds a --jsonschema persistent flag to the root command.

When the flag is set, the command prints its JSON Schema to stdout and returns without running the command's normal execution path. Works only for the root command.

func SetupMCP added in v0.14.0

func SetupMCP(rootC *cobra.Command, opts structclimcp.Options) error

SetupMCP adds a --mcp persistent flag to the root command.

When the flag is set, the command serves a minimal MCP server over stdio and returns without running the command's normal execution path. Works only for the root command.

func SetupUsage

func SetupUsage(c *cobra.Command)

SetupUsage generates and sets a dynamic usage function for the command.

It also groups flags based on the `flaggroup` annotation.

func Unmarshal

func Unmarshal(c *cobra.Command, opts Options, hooks ...mapstructure.DecodeHookFunc) error

Unmarshal populates opts with values from flags, environment variables, defaults, and configuration files.

It automatically handles decode hooks, validation, transformation, and context updates based on the options type.

Resolution happens from the effective command-scoped viper (GetViper(c)). Before decoding, Unmarshal merges command-relevant config from the root-scoped config-source viper (GetConfigViper(c)).

func UseConfig

func UseConfig(readWhen func() bool) (inUse bool, mes string, err error)

UseConfig attempts to read the configuration file based on the provided condition.

The readWhen function determines whether config reading should be attempted. Returns whether config was loaded, a status message, and any error encountered.

When SetupConfig was configured, this reads into the root-scoped config viper and merges command-relevant settings into the active command scoped viper.

If SetupConfig was not called, UseConfig falls back to reading on the global viper singleton. Prefer SetupConfig for deterministic command-scoped behavior.

func UseConfigSimple

func UseConfigSimple(c *cobra.Command) (inUse bool, message string, err error)

UseConfigSimple is a simpler version of UseConfig that uses c.IsAvailableCommand() as the readWhen function.

It does not check for the config file when the command is not available (eg., help).

The config file (if found) is loaded through the root-scoped config viper and merged into c's effective scoped viper.

func UseDebug

func UseDebug(c *cobra.Command, w io.Writer)

UseDebug manually triggers debug output for the given options.

When --debug-options=json, output goes to w as a JSON object. When --debug-options or --debug-options=text, output goes to w as a human-readable table.

Debug output is automatically triggered when the debug flag is enabled.

Types

type Base64 added in v0.12.0

type Base64 []byte

Base64 represents binary data provided as base64-encoded textual input.

type CommandSchema added in v0.13.0

type CommandSchema struct {
	Name        string                 `json:"name"`
	CommandPath string                 `json:"command_path"`
	Description string                 `json:"description,omitempty"`
	Flags       map[string]*FlagSchema `json:"flags"`
	Groups      map[string][]string    `json:"groups,omitempty"`
	Subcommands []string               `json:"subcommands,omitempty"`
	EnvPrefix   string                 `json:"env_prefix,omitempty"`
	ConfigFlag  string                 `json:"config_flag,omitempty"`
	Example     string                 `json:"example,omitempty"`    // Usage examples from cobra.Command.Example
	Aliases     []string               `json:"aliases,omitempty"`    // Command aliases from cobra.Command.Aliases
	ValidArgs   []string               `json:"valid_args,omitempty"` // Valid positional arguments from cobra.Command.ValidArgs
}

CommandSchema describes a command's inputs in machine-readable form.

func JSONSchema added in v0.13.0

func JSONSchema(c *cobra.Command, opts ...jsonschema.Opt) ([]*CommandSchema, error)

JSONSchema returns machine-readable schemas for a command's inputs.

By default it returns a single-element slice with the schema for the given command. Pass jsonschema.WithFullTree() to walk the entire command tree and return schemas for all subcommands.

It extracts all flag metadata from cobra annotations set during Define(), including types, defaults, descriptions, environment variables, groups, presets, and enum values.

func (*CommandSchema) ToJSONSchema added in v0.13.0

func (cs *CommandSchema) ToJSONSchema() ([]byte, error)

ToJSONSchema converts a CommandSchema to a JSON Schema draft 2020-12 document.

Standard JSON Schema fields (type, properties, required, enum, default, description) are used for core flag metadata. structcli-specific metadata is preserved in x-structcli-* extension fields.

type CompleteHookFunc added in v0.18.0

type CompleteHookFunc = internalhooks.CompleteHookFunc

CompleteHookFunc defines how to provide shell completion candidates for a flag.

type ContextInjector added in v0.17.0

type ContextInjector interface {
	Context(context.Context) context.Context
}

ContextInjector is a struct that propagates values into the command context after unmarshalling.

Context is called automatically during Unmarshal() to derive a new context. Does not require Options (Attach). Works with plain struct pointers via Bind.

Reading values back from context (FromContext) is a user-side pattern, not part of this interface.

type ContextOptions

type ContextOptions interface {
	Options
	Context(context.Context) context.Context
	FromContext(context.Context) error
}

ContextOptions extends Options with context manipulation capabilities.

For the Bind API, consider using ContextInjector instead. It only requires the Context method (propagation) and works with plain struct pointers. FromContext is a user-side pattern; structcli never calls it internally. ContextOptions remains the right choice when using Define/Unmarshal directly or when the type already implements Options.

type DecodeHookFunc added in v0.18.0

type DecodeHookFunc = internalhooks.DecodeHookFunc

DecodeHookFunc defines how to decode a raw value into a custom type during Unmarshal.

type DefineHookFunc added in v0.18.0

type DefineHookFunc = internalhooks.DefineHookFunc

DefineHookFunc defines how to create a pflag.Value for a custom type during Define/Bind.

type DefineOption

type DefineOption func(*defineContext)

DefineOption configures the behavior of the Define function.

func WithExclusions

func WithExclusions(exclusions ...string) DefineOption

WithExclusions sets flags to exclude from definition based on flag names or paths.

Exclusions are case-insensitive and apply only to the specific command.

func WithModTagName added in v0.13.0

func WithModTagName(name string) DefineOption

WithModTagName sets the struct tag name used to read transformation rules.

Defaults to DefaultModTagName ("mod", the go-playground/mold default). Use this when your mold instance is configured with a custom tag name.

func WithValidateTagName added in v0.13.0

func WithValidateTagName(name string) DefineOption

WithValidateTagName sets the struct tag name used to read validation rules.

Defaults to DefaultValidateTagName ("validate", the go-playground/validator default). Use this when your validator is configured with a custom tag name (eg. validator.New().SetTagName("binding")).

type EnumValuer added in v0.13.0

type EnumValuer interface {
	EnumValues() []string
}

EnumValuer is an optional interface that pflag.Value implementations can satisfy to declare their allowed values at the type level.

When a pflag.Value returned by a DefineHookFunc (built-in or custom) implements EnumValuer, structcli stores the allowed values as a flag annotation during Define(). This is the authoritative source of enum values; no description string parsing is needed.

Example:

type myEnumFlag struct {
    pflag.Value          // embed the underlying pflag.Value
    allowed []string
}
func (f *myEnumFlag) EnumValues() []string { return f.allowed }

type FieldCompleter added in v0.18.0

type FieldCompleter interface {
	CompletionHooks() map[string]CompleteHookFunc
}

FieldCompleter provides per-field shell completion hooks.

Map keys are struct field names. Works for any field that becomes a flag, not only fields with FieldHookProvider hooks.

If a completion function is already registered on a flag before Define, structcli preserves it (the FieldCompleter hook is not applied).

type FieldHook added in v0.18.0

type FieldHook struct {
	// Define creates the pflag.Value for this field.
	Define DefineHookFunc

	// Decode converts raw input to the field's type during Unmarshal.
	Decode DecodeHookFunc
}

FieldHook bundles the Define and Decode hooks for a single struct field.

If Define is nil, the field falls through to the type registry or built-in handling. If Decode is nil, the default decode path is used. Setting Decode without Define is an error (caught at Define/Bind time).

type FieldHookProvider added in v0.18.0

type FieldHookProvider interface {
	FieldHooks() map[string]FieldHook
}

FieldHookProvider provides per-field Define/Decode hooks.

Implement this interface when the same type needs different flag behavior in different fields, or when a standard type needs custom handling for a specific field.

Map keys are struct field names (e.g., "ListenAddr", not the flag name "listen"). Unknown keys that do not match any struct field cause an error at Define/Bind time.

Precedence: FieldHookProvider > RegisterType > built-in registry.

type FlagSchema added in v0.13.0

type FlagSchema struct {
	Name        string       `json:"name"`
	Shorthand   string       `json:"shorthand,omitempty"`
	Type        string       `json:"type"`
	Default     string       `json:"default,omitempty"`
	Description string       `json:"description,omitempty"`
	Required    bool         `json:"required,omitempty"`
	EnvOnly     bool         `json:"env_only,omitempty"`
	EnvVars     []string     `json:"env_vars,omitempty"`
	Group       string       `json:"group,omitempty"`
	FieldPath   string       `json:"field_path,omitempty"`
	Enum        []string     `json:"enum,omitempty"`
	Presets     []PresetInfo `json:"presets,omitempty"`
}

FlagSchema describes a single flag in machine-readable form.

type Hex added in v0.12.0

type Hex []byte

Hex represents binary data provided as hex-encoded textual input.

type Options

type Options interface {
	Attach(*cobra.Command) error
}

Options represents a struct that can define command-line flags, env vars, config file keys.

Types implementing this interface can be used with Define() to automatically generate flags from struct fields.

type PresetInfo added in v0.13.0

type PresetInfo struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

PresetInfo describes a preset alias for a flag.

type SetupOption added in v0.17.0

type SetupOption func(*setupConfig)

SetupOption configures a feature in Setup.

func WithAppName added in v0.17.0

func WithAppName(name string) SetupOption

WithAppName sets the application name used for environment variable prefixes and config file discovery. When flags already exist on the command tree (from earlier Bind calls), their env annotations are retroactively patched to include the new prefix.

If a sub-option (e.g., debug.Options.AppName or config.Options.AppName) specifies a different name, Setup returns an error.

func WithConfig added in v0.17.0

func WithConfig(opts ...config.Options) SetupOption

WithConfig enables config file discovery and the --config flag on the root command. The actual config loading (UseConfigSimple) is deferred to ExecuteC's bind pipeline, before the first auto-unmarshal.

func WithDebug added in v0.17.0

func WithDebug(opts debug.Options) SetupOption

WithDebug enables the debug flag (--debug-options) on the root command.

Unlike other With* options, WithDebug requires an explicit debug.Options argument because AppName and Exit have no sensible zero-value defaults.

func WithFlagErrors added in v0.17.0

func WithFlagErrors() SetupOption

WithFlagErrors enables structured flag error interception on the root command.

func WithHelpTopics added in v0.17.0

func WithHelpTopics(opts ...helptopics.Options) SetupOption

WithHelpTopics enables help topic commands on the root command. Pass helptopics.Options{} for defaults.

func WithJSONSchema added in v0.17.0

func WithJSONSchema(opts ...jsonschema.Options) SetupOption

WithJSONSchema enables the --jsonschema flag on the root command. Pass jsonschema.Options{} for defaults.

func WithMCP added in v0.17.0

func WithMCP(opts ...structclimcp.Options) SetupOption

WithMCP enables the --mcp flag on the root command. Pass mcp.Options{} for defaults.

type StructuredError added in v0.13.0

type StructuredError struct {
	Error    string `json:"error"`
	ExitCode int    `json:"exit_code"`
	Message  string `json:"message"`

	// Input error fields
	Flag      string   `json:"flag,omitempty"`
	Got       string   `json:"got,omitempty"`
	Expected  string   `json:"expected,omitempty"`
	Command   string   `json:"command,omitempty"`
	Hint      string   `json:"hint,omitempty"`
	Available []string `json:"available,omitempty"`

	// Validation fields
	Violations []Violation `json:"violations,omitempty"`

	// Config fields
	ConfigFile string `json:"config_file,omitempty"`
	Key        string `json:"key,omitempty"`

	// Environment variable fields
	EnvVar string `json:"env_var,omitempty"`
}

StructuredError is the JSON object written to stderr by HandleError.

Every field is optional except Error, ExitCode, and Message. Agents parse this to decide whether to self-correct, fix the environment, or report to a human.

type Transformable added in v0.17.0

type Transformable interface {
	Transform(context.Context) error
}

Transformable is a struct that supports transformation after unmarshalling.

Transform is called automatically during Unmarshal(), before Validate. Does not require Options (Attach). Works with plain struct pointers via Bind.

type TransformableOptions

type TransformableOptions interface {
	Options
	Transform(context.Context) error
}

TransformableOptions extends Options with transformation capabilities.

For the Bind API, consider using Transformable instead. It does not require implementing Attach and works with plain struct pointers. TransformableOptions remains the right choice when using Define/Unmarshal directly or when the type already implements Options.

type TypeHooks added in v0.18.0

type TypeHooks[T any] struct {
	Define DefineHookFunc
	Decode DecodeHookFunc
}

TypeHooks defines custom flag behavior for a type.

Define creates the pflag.Value for this type. Called during Define/Bind for each struct field of type T. Receives the specific field's value and metadata.

Decode converts raw input (string from env/config) to T during Unmarshal.

type Validatable added in v0.17.0

type Validatable interface {
	Validate(context.Context) []error
}

Validatable is a struct that supports validation after unmarshalling.

Validate is called automatically during Unmarshal(), after Transform. Does not require Options (Attach). Works with plain struct pointers via Bind.

type ValidatableOptions

type ValidatableOptions interface {
	Options
	Validate(context.Context) []error
}

ValidatableOptions extends Options with validation capabilities.

For the Bind API, consider using Validatable instead. It does not require implementing Attach and works with plain struct pointers. ValidatableOptions remains the right choice when using Define/Unmarshal directly or when the type already implements Options.

type Violation added in v0.13.0

type Violation struct {
	Field   string `json:"field"`
	Rule    string `json:"rule,omitempty"`
	Param   string `json:"param,omitempty"`
	Value   any    `json:"value,omitempty"`
	Message string `json:"message"`
}

Violation represents a single validation failure for a field.

Image Directories

Path Synopsis
examples
full module
Package exitcode defines semantic exit codes for structcli-powered CLIs.
Package exitcode defines semantic exit codes for structcli-powered CLIs.
Package flagkit provides reusable, embeddable flag structs that standardize common CLI flag declarations for use with structcli.
Package flagkit provides reusable, embeddable flag structs that standardize common CLI flag declarations for use with structcli.
Package generate produces static discovery files from structcli command trees.
Package generate produces static discovery files from structcli command trees.
internal
cmd
env
proptest/gen
Package gen provides shared rapid generators for property-based tests.
Package gen provides shared rapid generators for property-based tests.
tag
Package structerr configures structured error output for structcli-powered CLIs.
Package structerr configures structured error output for structcli-powered CLIs.

Jump to

Keyboard shortcuts

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