Repository navigation
Description
Problem
A setting that is also a flag has to be declared twice today: once on the #[derive(Cli)]/#[derive(Args)] field the program parses, and once on a #[derive(Config)] field that builds SETTINGS_REGISTRY. A struct can't derive both, since each derive refuses the other's keys (usage::Config does not understand `long` ). Registry::drift then checks that the two copies agree.
// What the binary parses.
#[derive(usage_rs::Args)]
pub struct ListenTls {
/// Load the given certificate from disk.
#[usage(long = "listen-tls-cert", env = "MOQ_LISTEN_TLS_CERT", setting = "listen.tls.cert")]
pub cert: Vec<PathBuf>,
}
// The same setting again, for the registry.
#[derive(usage_rs::Config)]
#[usage(prefix = "listen.tls")]
struct ListenTlsSettings {
#[usage(env = "MOQ_LISTEN_TLS_CERT", cli("--listen-tls-cert"), parse = "list_by_comma")]
cert: Option<Vec<String>>,
}Everything the second declaration says is already on the first: the key (setting), the env names (env, deprecated_env), the flags (long/short), the help (the doc comment), and the type (the field). The drift test catches the copies disagreeing, but a CLI that merges every flag into its settings still writes and reviews every flag twice. That is the "second description of every setting" the Config derive was introduced to remove.
Proposal
Let one declaration serve both. Either shape would do:
- The parse derives emit registry entries. A
Cli/Argsfield carryingsetting = "key"contributes a prop: the type comes from the field (Option<T>→ optional,Vec<T>→ list, withvalue_delimiter, or an explicitparse, deciding how a string becomes a list),env/deprecated_envand help come from the same attributes, andcli(...)comes fromlong/short. A root with#[usage(settings)]exposes the joined registry, composed throughProps/concat_propslike flattenedConfiggroups are today. Settings with no flag (file-only ones) stay in a#[derive(Config)]struct that is joined in. - One struct may derive both. Each derive ignores the other's keys instead of refusing them,
env/deprecated_env/doc comments are shared, andConfiginferscli(...)fromlong/short.
Either way, generating usage-config items can stay behind the existing #[usage(settings)] / config gates, so a CLI without settings still never mentions usage-config. That keeps the layering from #896, where only the root names the config crate. Source presence, precedence, deprecated-name handling and provenance (explain) would stay as they are; only the declaration moves.
Use case
moq (a media relay and CLI tools on usage-rs 7.1) merges every connection, TLS, QUIC and logging flag with a TOML file. Its two registries, moq-tokio/src/settings.rs and moq-relay/src/settings.rs, are 646 lines restating about 110 settings (env name, flag name, list parsing) already declared on the parsed structs, plus a drift test. With either shape, those files and the test go away and CLI behavior stays the same.
AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5-5; version: 2.1.316.
(Written by Opus 5.5)
Activity
feat(spec): let a flag with setting declare its setting (#1610)
github-actions commentedon Nov 3, 2026
Fixed in v7.2.0 by #1610.