Skip to content

Add Command::next_env_prefix for env variable prefixing#6281

Open
veeceey wants to merge 1 commit intoclap-rs:masterfrom
veeceey:feat/issue-3221-env-prefix
Open

Add Command::next_env_prefix for env variable prefixing#6281
veeceey wants to merge 1 commit intoclap-rs:masterfrom
veeceey:feat/issue-3221-env-prefix

Conversation

@veeceey
Copy link
Copy Markdown

@veeceey veeceey commented Feb 23, 2026

Adds Command::next_env_prefix which sets a prefix that gets prepended to all subsequent arg env variable names during build. Modeled directly after next_help_heading as @epage suggested in #3221.

How it works:

  • Command::next_env_prefix("MYAPP") sets the prefix for all future args
  • When an arg has both an env name and an inherited prefix, the env name gets rewritten to MYAPP_<NAME> during _build_self
  • The prefix can be reset with next_env_prefix(None::<&str>)
  • CLI arguments still override env values as expected

The env prefix is stored on each Arg (via env_prefix field) during arg_internal, same pattern as help_heading. The actual name rewriting happens during _build_self which keeps help generation and env lookup unchanged.

Requires both env and string features since we need runtime string construction for the prefixed names.

let cmd = Command::new("myapp")
    .next_env_prefix("MYAPP")
    .arg(Arg::new("config").long("config").env("CONFIG"));
// env var will be MYAPP_CONFIG

Closes #3221

@veeceey
Copy link
Copy Markdown
Author

veeceey commented Feb 23, 2026

Test results (all env tests pass, including 5 new env_prefix tests):

running 30 tests
test env::env_prefix_does_not_affect_args_without_env ... ok
test env::env_os ... ok
test env::env ... ok
test env::env_bool_literal ... ok
test env::env_prefix_cli_overrides_env ... ok
test env::env_prefix_basic ... ok
test env::env_prefix_multiple_args ... ok
test env::env_prefix_reset ... ok
test env::multiple_no_delimiter ... ok
test env::multiple_one ... ok
test env::multiple_three ... ok
test env::no_env ... ok
test env::no_env_no_takes_value ... ok
test env::opt_user_override ... ok
test env::not_possible_value ... ok
test env::positionals ... ok
test env::positionals_user_override ... ok
test env::possible_value ... ok
test env::value_parser ... ok
test env::value_parser_invalid ... ok
test env::with_default ... ok
test env::value_parser_output ... ok
test help_env::show_env ... ok
test help_env::hide_env ... ok
test help_env::hide_env_vals_flag ... ok
test help_env::hide_env_flag ... ok
test help_env::hide_env_vals ... ok
test help_env::show_env_flag ... ok
test help_env::show_env_vals ... ok
test help_env::show_env_vals_flag ... ok

test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 916 filtered out

@epage
Copy link
Copy Markdown
Member

epage commented Feb 23, 2026

We recommend that commits represent how things should be reviewed and merged and not how they were developed. Having fixup commits means they are not atomic.

There is also a strong preference for adding tests in a previous commit, with them passing, showing the current behavior. How to handle this when its a new API is context dependent. In this case, I would add the tests with the prefixes being hardcoded in each env variable and then the main commit would port it to the new API.

Comment thread clap_builder/src/builder/arg.rs
Comment thread clap_builder/src/builder/arg.rs Outdated
Comment thread clap_builder/src/builder/command.rs
Comment thread clap_builder/src/builder/command.rs Outdated
if let Some(Some(ref prefix)) = a.env_prefix {
if let Some((ref env_name, _)) = a.env {
let prefixed = format!("{}_{}", prefix, env_name.to_str().unwrap_or(""));
let value = env::var_os(&prefixed);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not thrilled with us having looked up the env and now we look it up again

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is a tradeoff with the current design. Arg::env() eagerly caches the env value at argument creation time, but we don't know the final env name until the prefix is applied during build. So we have to re-lookup with the prefixed name.

Moving the prefix application earlier (e.g. into arg_internal) doesn't help since Arg::env() already ran by then. The alternative would be lazy env lookup (only during parsing), but that'd be a bigger refactor of existing behavior. The extra env::var_os call at build time is cheap, and the old cached value gets replaced, so there's no functional issue - just a wasted initial lookup.

@veeceey veeceey force-pushed the feat/issue-3221-env-prefix branch from 3450bca to e8ca0e8 Compare February 24, 2026 07:13
Comment thread clap_builder/src/builder/arg.rs
Comment thread tests/builder/env.rs Outdated

#[cfg(feature = "string")]
#[test]
fn env_prefix_cli_overrides_env() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it is testing env support generally rather than env-prefix-specific logic

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, removed that test. The remaining tests focus specifically on prefix behavior.

Comment thread tests/builder/env.rs Outdated
Comment thread tests/builder/env.rs
@veeceey veeceey force-pushed the feat/issue-3221-env-prefix branch from e8ca0e8 to 05b4713 Compare February 28, 2026 06:21
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep in mind that new tests should be added in the previous commit like the other new tests.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Squashed everything into a single commit now - feature code, builder tests, and derive tests all together.

Comment on lines +46 to +59
#[test]
fn command_next_env_prefix_cli_overrides() {
env::set_var("DERIVE_CLI_NAME", "from_env");

#[derive(Debug, Clone, Parser)]
#[command(next_env_prefix = "DERIVE_CLI")]
struct CliOptions {
#[arg(long, env = "NAME")]
name: Option<String>,
}

let m = CliOptions::try_parse_from(vec!["", "--name", "from_cli"]).unwrap();
assert_eq!(m.name.as_deref(), Some("from_cli"));
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be testing env support

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed that test - it was testing general env/cli override behavior, not prefix-specific logic.

Comment on lines +62 to +76
fn command_next_env_prefix_with_flatten() {
env::set_var("FLAT_APP_DB", "mydb");

#[derive(Debug, Clone, Args)]
#[command(next_env_prefix = "FLAT_APP")]
struct DbArgs {
#[arg(long, env = "DB")]
db: Option<String>,
}

#[derive(Debug, Clone, Parser)]
struct CliOptions {
#[command(flatten)]
db_args: DbArgs,
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing some flatten cases that next_help_heading has, like

  • flattened field with help heading

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more flatten cases:

  • flatten_multiple_structs_different_prefixes - two flattened structs each with their own prefix, verifying isolation
  • parent_env_prefix_does_not_leak_into_flatten - parent command's prefix correctly doesn't leak into a flattened struct that doesn't set its own prefix

@veeceey
Copy link
Copy Markdown
Author

veeceey commented Mar 10, 2026

hey @epage, thanks for the thorough review — really helpful feedback. let me go through each point:

  1. Arg::env_prefix vs Command::env_prefix — yeah, rereading Add an env_prefix derive option for a consistent prefix for environment variables #3221 I see they wanted per-arg prefix support too. I'll look into adding that as a follow-up or folding it into this PR if it makes sense.

  2. Hand-written Debug impl — missed that, I'll update it to include the new field.

  3. env feature gate — good catch, the builder methods shouldn't silently do nothing when env isn't enabled. I'll add a compile error or gate the methods properly.

  4. to_str — agreed, I'll find a better approach here. probably working with OsStr directly instead of converting.

  5. Double env lookup — yeah that's wasteful. I'll refactor so we only resolve the env var once and pass the result through.

  6. Missing getter — will add it.

  7. Test testing env support generally — you're right, I'll trim that test down to only cover prefix-specific behavior.

  8. Turbofish — good point, I'll remove it if type inference handles it.

  9. Derive tests — will add derive tests modeled after next_help_heading.

  10. Commit structure — understood, I'll restructure so the tests come in a prior commit showing current behavior, then the implementation commit ports them to the new API.

  11. Test testing env support — will remove the redundant env test from the prefix test file.

  12. Flatten cases — will add the missing flatten scenarios like next_help_heading has.

I'll rework the commits and push an updated version. appreciate the detailed review!

@veeceey veeceey force-pushed the feat/issue-3221-env-prefix branch from 05b4713 to 1a94daa Compare March 12, 2026 04:39
@veeceey
Copy link
Copy Markdown
Author

veeceey commented Mar 12, 2026

rebased and addressed the test feedback:

  • removed the env_prefix_cli_overrides_env builder test and command_next_env_prefix_cli_overrides derive test — both were just testing general env/cli override behavior, not prefix-specific logic
  • added flatten_field_with_env_prefix derive test (mirrors flatten_field_with_help_heading — prefix set on the flatten field itself, not inside the flattened struct)
  • restructured commits: first commit now has both builder and derive tests with hardcoded prefixed env names, second commit ports them to the new API
  • dropped the unrelated CI/toolchain changes that crept in from the fork being behind upstream

@veeceey veeceey force-pushed the feat/issue-3221-env-prefix branch from 1a94daa to 46986d6 Compare March 13, 2026 06:14
@veeceey
Copy link
Copy Markdown
Author

veeceey commented Mar 15, 2026

Thanks for the detailed review @epage. I see I'm missing several cases — the flatten scenarios that next_help_heading handles, derive tests, the getter, and the turbofish issue. Let me go through next_help_heading's implementation more carefully and make sure env_prefix covers the same set of cases. Will push an update with the missing flatten tests, a getter, and clean up the turbofish.

@veeceey
Copy link
Copy Markdown
Author

veeceey commented Mar 18, 2026

fixed the CI failures — the env::set_var calls in my test code weren't wrapped in unsafe blocks, which is required in the 2024 edition. all set_var calls now have the unsafe wrapper matching the existing test style in the repo.

Add env variable prefix support so that env variable names specified
via Arg::env can be automatically prefixed during build. This works
similarly to Command::next_help_heading - it's a stateful method that
affects subsequently added arguments.

- Command::next_env_prefix sets the prefix for all following args
- Arg::env_prefix sets per-arg prefix (takes precedence)
- Prefix and env name are joined with underscore
- Hand-written Debug impl updated to include env_prefix
- Getter get_env_prefix added for reflection
- Derive support via #[command(next_env_prefix = "...")]
- Works with flatten (prefix on Args struct or on flatten field)
- Requires both `env` and `string` features since the prefixed
  env name is dynamically constructed

Closes clap-rs#3221
@veeceey veeceey force-pushed the feat/issue-3221-env-prefix branch from 5ebc02f to d47433a Compare March 18, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an env_prefix derive option for a consistent prefix for environment variables

3 participants