clap-config-fallback
Merge CLI arguments and config files without duplication, while preserving clap's behavior.
Add configuration-file fallback to clap without losing clap parsing, validation, or error handling.
Why?
clap is excellent at parsing CLI arguments and producing high-quality diagnostics.
What it does not do out of the box is merge a config file with CLI arguments while preserving the same parsing and validation contract.
Common alternatives often:
- reimplement parsing and drift from clap behavior,
- duplicate structs (one for CLI, one for config), or
- validate after parsing in a separate pass.
clap-config-fallback keeps clap as the single source of truth for parsing and validation.
Installation
[]
= { = "4", = ["derive"] }
= { = "0.1", = ["derive"] }
Optional format features:
tomlyamljson
All three are enabled by default.
Quick start
use ;
use ;
Example config.toml:
= "127.0.0.1"
= 8080
= { = "serve" }
How it works
#[derive(ConfigParser)] generates an intermediate optional Opts struct and runs a two-phase parse:
- Parse CLI arguments into
Opts. - Resolve a config file path (if one is defined).
- Load and deserialize config into a generated
Configstruct. - Merge values with precedence CLI > config.
- Reconstruct synthetic CLI args from merged
Opts. - Run your original clap parser for final parsing + validation.
Because the final pass is still clap, you keep clap’s errors and validation behavior.
Derives overview
clap-config-fallback provides three derives that mirror clap's structure and are designed to be
used together:
| clap | clap-config-fallback | Role |
|---|---|---|
Parser |
ConfigParser |
Root CLI + config entry point |
Args |
ConfigArgs |
Nested argument groups |
Subcommand |
ConfigSubcommand |
Enum-based subcommands |
Precedence and edge cases
- CLI always wins over config for the same field.
- Config fallback only runs when a
#[config(path)]field is present and resolves toSome(path). - If the path field has a clap
default_value, that default also enables fallback. - If no path is available, parsing behaves like normal clap parsing.
- A non-existent file is reported as a
claperror. - Unknown/unsupported format (or feature-disabled format) is reported as a
claperror.
Requirements
Using ConfigParser has a few constraints:
- The root type must be a named struct.
- Fields that participate in fallback must be serializable/deserializable.
- Field values must be representable as CLI argument values.
- Nested flattened structs must also derive
ConfigArgs. - Subcommands use an internally tagged representation in the configuration file.
- Subcommands must follow the canonical structure (see below).
Canonical structure
Subcommands must follow this structure to enable configuration fallback.
ConfigParser cannot be derived on enums, which is the only intentional incompatibility with
clap.
Configuration fallback requires resolving the configuration file before parsing the command.
With an enum root, the command must be selected first, which makes it impossible to define or access a config path beforehand.
Use a root struct that defines the config path and delegates to subcommands:
Derive attributes
#[config(path)]
Marks the field that stores the configuration file path.
- Optional (you may omit it entirely).
- Must be one of:
StringOption<String>
If no path field exists, config fallback is disabled.
#[config(format = "...")]
Forces how the config file is parsed for the #[config(path)] field.
Supported values:
tomlyamljsonauto(default; detect by extension)
#[config(skip)]
Excludes a field from config fallback while keeping normal CLI parsing.
port: u16,
#[config(skip_all)]
Disables config fallback for all fields in the struct.
#[config(value_format = ...)]
Controls how a value is converted back into CLI arguments after merging.
duration: Duration,
#[config(tag = "...")]
Defines the field used to select the active subcommand in the configuration.
Required for ConfigSubcommand
#[config(alias = "...", aliases = ["...", "..."])]
Adds a configuration-only alias for a field using #[command(...)].
This is useful for nested config sections, especially because clap does not support aliases on
#[command(...)] fields.
Attribute reference
| Attribute | ConfigParser |
ConfigArgs |
ConfigSubcommand |
Purpose |
|---|---|---|---|---|
#[config(path)] |
✅ | ❌ | ❌ | Marks the config file path field |
#[config(format = "...")] |
✅ | ❌ | ❌ | Forces config format: toml, yaml, json, or auto |
#[config(skip)] |
✅ | ✅ | ✅ | Excludes a field or variant from config generation |
#[config(skip_all)] |
✅ | ✅ | ✅ | Excludes all fields or variants from config generation |
#[config(value_format = ...)] |
✅ | ✅ | ✅ | Converts a merged value back into a CLI-compatible string |
#[config(tag = "...")] |
❌ | ❌ | ✅ | Defines the config field used to select a subcommand variant |
#[config(alias = "...")] |
✅ | ✅ | ✅ | Adds one configuration-only alias for a #[command(...)] field |
#[config(aliases = ["...", "..."])] |
✅ | ✅ | ✅ | Adds multiple configuration-only aliases for a #[command(...)] field |