Config files

The zcli_config plugin loads option defaults from a config file — JSON, TOML, or YAML — with no changes to command code. Your Options structs stay exactly as they are; the plugin fills in unset fields before execute runs.

Enable it in build.zig:

.plugins = &.{
    zcli.builtin(.help, .{}),
    zcli.builtin(.config, .{}),
},

Discovery

The first match wins:

  1. --config <path> — an explicit file passed on the command line (a global option the plugin contributes)
  2. ./{app}.config.json|toml|yaml — a project-local file in the working directory
  3. {user config dir}/{app}/config.json|toml|yaml — the user-level config ($XDG_CONFIG_HOME or ~/.config on POSIX; %APPDATA%, else %USERPROFILE%, on Windows)

If more than one candidate exists, the first in this order is used and the plugin prints a notice naming the file it chose.

Global and per-command values

Top-level keys apply to every command; a table named after a command scopes values to just that command:

# .myapp.config.toml
verbose = true      # global — applies to all commands

[list]              # scoped — applies only to `myapp list`
all = true

Keys match option field names. A scoped value beats a global one for its command.

Precedence

Config sits between explicit user input and your struct defaults:

CLI flag  >  env var  >  command-scoped config  >  global config  >  struct default

A value set on the command line — or read from an option’s env fallback — always wins, even when it happens to equal the struct default; the plugin only fills fields the CLI and env left unset.

Every option type coerces from config. A config scalar is stringified and run through the same value parser the CLI and env use, so bools, all integer widths, floats, enums (including optionals), custom parse types, and arrays / multi-value options (from a config list) all work — no per-type wiring.

Failures are loud, never silent. A malformed config file, an unrecognized extension, an out-of-range number, or an unknown enum variant is skipped with a warning on stderr — the default stays — rather than crashing the CLI or being trusted. A value that won’t parse is never injected.

Config can satisfy a required option and participates in validation and constraints like any other source — a validate hook runs on the final value wherever it came from, and a config value counts as “supplied” for exclusive/requires checks.

Next