Repository navigation
Commit af36eb3
feat(spec): let a flag with setting declare its setting (#1610)
A setting that a flag sets had to be declared twice: once on the
`#[derive(Args)]`/`#[derive(Cli)]` field the program parses, and again
on a `#[derive(Config)]` field so it reached `SETTINGS_REGISTRY`.
`Registry::drift` caught the two copies disagreeing, but every
flag-backed setting was still written and reviewed twice.
[moq](https://github.com/moq-dev/moq), for example, keeps 646 lines of
settings registries that restate about 110 flags.
Now the flag is the declaration. A field with `setting = "key"` provides
everything the registry entry needs:
| Registry entry | Comes from |
| --- | --- |
| type | the field's type (`Vec<T>` is a list, a counter is `uint`, an
unrecognised type is `string`) |
| environment names | `env`, `env_fallback`, `deprecated_env` |
| how one string becomes a list | `delimiter = ','` or `':'` |
| help | the doc comment |
| flags | every flag bound to the key, including a negation, and the
same key bound on several subcommands |
```rust
#[derive(usage_rs::Args)]
struct ListenTls {
/// Load the given certificate from disk.
#[usage(long = "listen-tls-cert", env = "EX_LISTEN_TLS_CERT", delimiter = ',',
setting = "listen.tls.cert")]
cert: Vec<std::path::PathBuf>,
}
```
The root `Cli` now exposes `SETTINGS_PROPS` and `SETTINGS_REGISTRY`. In
the example above, the registry holds `listen.tls.cert` as a
`list<path>` that `EX_LISTEN_TLS_CERT=a.pem,b.pem` or
`--listen-tls-cert` sets. Its `cli` is exactly the bound flags, so
`Ex::SETTINGS_REGISTRY.drift(Ex::SETTINGS_BINDINGS)` is always empty.
When the root names `config = Settings`, that struct's settings are
joined in, and the spec's `config` block documents both kinds. A root
with no `config` type now gets a `config` block too, listing the
settings its flags declare, so docs, the JSON schema and config-key
completion can see them.
When several subcommands bind the same key, they become one setting.
Their field types must match, and so must their `env` and
`deprecated_env` lists and their `delimiter` when more than one gives
one. A list setting must split on `,` or `:`, the delimiters a registry
parser also splits the environment on. Breaking any of these rules is a
compile error that points to a `Config` declaration instead, unless a
`Config` field already declares the key.
**Compatibility.** When a key is declared both on a flag and on the
`config` struct, the struct's declaration wins. A CLI that declares
every setting there today gets the same registry and compiles unchanged.
A setting that needs a default, `merge`, `scope` or `choices` stays a
`Config` field, and so does one no flag sets. Flag-declared settings are
always optional and have no registry default. A hand-written
`CommandArgs` or `Subcommands` implementation that binds a key without
declaring it still compiles. Its key is left to whatever registry it
already uses.
**Layering.** Groups describe their settings in a new
`usage_argv::spec::SettingDecl`, which doesn't mention `usage-config`,
through a `SETTING_DECLS` constant on `CommandArgs` and `Subcommands`
(empty by default). A library of flags still doesn't depend on
`usage-config`. The root converts these declarations at compile time
through `usage-rs`, the only crate that depends on both `usage-argv` and
`usage-config`. The code it generates is only emitted for CLIs that
resolve settings.
**Validation.** `cargo test --all --all-features` passes. The new
`conformance/tests/derive_settings_declared.rs` covers these cases:
- types, environment names, splitting and help reach the registry
- one key bound on two subcommands with different spellings becomes one
setting
- the `Config` declaration wins a shared key
- the flag and the environment reach the same setting through `resolve`
- the spec round-trips through usage-lib, with and without a `config`
type
- a binding with no declaration behind it is skipped
- mismatched environment names or delimiters for one key are refused,
and so is a delimiter the registry can't split on
`mise run lint` fails only on a clippy `duplicated_attributes` error in
`benches/shadows/pitchfork`, which fails the same way on `main`. I
haven't measured perf or binary size locally.
Closes #1608
*AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5-5;
version: 2.1.316.*
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->
## Summary by CodeRabbit
* **New Features**
* CLI flags can now declare settings that appear in the generated
settings registry and documentation, including their types, environment
variables, and help text.
* Settings declared by flags are optional and have no registry default.
When a configuration type declares the same setting, its definition
takes precedence.
* **Documentation**
* Updated the configuration guide with details on flag-declared settings
and how they interact with configuration types.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes compile-time registry and spec generation for every
settings-resolving CLI, but preserves behavior when `Config` owns keys
and adds broad conformance tests.
>
> **Overview**
> **Flag-backed settings no longer need a duplicate `Config` field** for
registry metadata. Fields with `setting = "key"` now drive `SettingDecl`
tables (via new `SETTING_DECLS` on `CommandArgs` / `Subcommands`), and
the root `#[derive(Cli)]` joins them into **`SETTINGS_PROPS`** and
**`SETTINGS_REGISTRY`**, with CLI spellings taken from
**`SETTINGS_BINDINGS`** so drift stays aligned by construction.
>
> The derive layer emits declarations from each bound field (type, `env`
/ deprecated env, list `delimiter`, doc help). **`usage-rs`** converts
those tables to `PropMeta` at compile time. When `config = Type` is set,
**`Config` still wins** on shared keys (defaults, merge, scope);
flag-only settings are optional with no registry default. KDL
**`config`** blocks now document the **joined** registry, including CLIs
with no `Config` type.
>
> **`concat_setting_decls`** and compile-time checks fold duplicate keys
across subcommands and reject mismatched types, env names, or
delimiters.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c9e2b64. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>1 parent e7dcfd4 commit af36eb3
8 files changed
Lines changed: 949 additions & 18 deletions
File tree
- argv/src
- conformance/tests
- derive/src
- docs/rust
- usage-rs/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4895 | 4895 | | |
4896 | 4896 | | |
4897 | 4897 | | |
| 4898 | + | |
| 4899 | + | |
| 4900 | + | |
| 4901 | + | |
| 4902 | + | |
| 4903 | + | |
| 4904 | + | |
| 4905 | + | |
| 4906 | + | |
| 4907 | + | |
| 4908 | + | |
| 4909 | + | |
| 4910 | + | |
| 4911 | + | |
| 4912 | + | |
| 4913 | + | |
| 4914 | + | |
| 4915 | + | |
| 4916 | + | |
| 4917 | + | |
| 4918 | + | |
| 4919 | + | |
| 4920 | + | |
| 4921 | + | |
| 4922 | + | |
| 4923 | + | |
| 4924 | + | |
| 4925 | + | |
| 4926 | + | |
| 4927 | + | |
| 4928 | + | |
| 4929 | + | |
| 4930 | + | |
| 4931 | + | |
| 4932 | + | |
| 4933 | + | |
| 4934 | + | |
| 4935 | + | |
| 4936 | + | |
| 4937 | + | |
| 4938 | + | |
| 4939 | + | |
| 4940 | + | |
| 4941 | + | |
| 4942 | + | |
| 4943 | + | |
| 4944 | + | |
| 4945 | + | |
| 4946 | + | |
| 4947 | + | |
| 4948 | + | |
| 4949 | + | |
| 4950 | + | |
| 4951 | + | |
| 4952 | + | |
| 4953 | + | |
| 4954 | + | |
| 4955 | + | |
| 4956 | + | |
| 4957 | + | |
| 4958 | + | |
| 4959 | + | |
| 4960 | + | |
| 4961 | + | |
| 4962 | + | |
| 4963 | + | |
| 4964 | + | |
| 4965 | + | |
| 4966 | + | |
| 4967 | + | |
| 4968 | + | |
| 4969 | + | |
| 4970 | + | |
| 4971 | + | |
| 4972 | + | |
| 4973 | + | |
| 4974 | + | |
| 4975 | + | |
| 4976 | + | |
| 4977 | + | |
| 4978 | + | |
| 4979 | + | |
| 4980 | + | |
| 4981 | + | |
| 4982 | + | |
| 4983 | + | |
| 4984 | + | |
| 4985 | + | |
| 4986 | + | |
| 4987 | + | |
| 4988 | + | |
| 4989 | + | |
| 4990 | + | |
| 4991 | + | |
4898 | 4992 | | |
4899 | 4993 | | |
4900 | 4994 | | |
| |||
5023 | 5117 | | |
5024 | 5118 | | |
5025 | 5119 | | |
| 5120 | + | |
| 5121 | + | |
| 5122 | + | |
| 5123 | + | |
| 5124 | + | |
5026 | 5125 | | |
5027 | 5126 | | |
5028 | 5127 | | |
| |||
5467 | 5566 | | |
5468 | 5567 | | |
5469 | 5568 | | |
| 5569 | + | |
| 5570 | + | |
| 5571 | + | |
5470 | 5572 | | |
5471 | 5573 | | |
5472 | 5574 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
0 commit comments