Skip to content

Commit 1a94daa

Browse files
committed
feat(env): Add Command::next_env_prefix and Arg::env_prefix
Add env variable prefixing support as proposed in #3221. This follows the same pattern as next_help_heading / help_heading. - Command::next_env_prefix sets a prefix for all future args - Arg::env_prefix allows per-arg override, taking precedence - Arg::get_env_prefix getter for querying the prefix - Prefix is joined with '_' and applied during _build_self - Uses OsString operations for concatenation (no to_str) - Requires both 'env' and 'string' features - Includes env_prefix in Arg's hand-written Debug impl - Derive support via #[command(next_env_prefix = "...")] attribute - Port tests from hardcoded prefixes to the new API Closes #3221
1 parent c8a81b8 commit 1a94daa

8 files changed

Lines changed: 180 additions & 18 deletions

File tree

clap_builder/src/builder/arg.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ pub struct Arg {
8585
pub(crate) default_missing_vals: Vec<OsStr>,
8686
#[cfg(feature = "env")]
8787
pub(crate) env: Option<(OsStr, Option<OsString>)>,
88+
#[cfg(all(feature = "env", feature = "string"))]
89+
pub(crate) env_prefix: Option<Option<OsStr>>,
8890
pub(crate) terminator: Option<Str>,
8991
pub(crate) index: Option<usize>,
9092
pub(crate) help_heading: Option<Option<Str>>,
@@ -2212,6 +2214,41 @@ impl Arg {
22122214
pub fn env_os(self, name: impl Into<OsStr>) -> Self {
22132215
self.env(name)
22142216
}
2217+
2218+
/// Sets an env variable prefix for this argument.
2219+
///
2220+
/// When set, the env variable name specified via [`Arg::env`] will be
2221+
/// prefixed with this value (joined by `_`) during build.
2222+
///
2223+
/// An explicit `Arg::env_prefix` takes precedence over
2224+
/// [`Command::next_env_prefix`].
2225+
///
2226+
/// This can be reset with `None`.
2227+
///
2228+
/// # Examples
2229+
///
2230+
/// ```rust
2231+
/// # #[cfg(all(feature = "env", feature = "string"))] {
2232+
/// # use clap_builder as clap;
2233+
/// # use clap::{Command, Arg};
2234+
/// let cmd = Command::new("myapp")
2235+
/// .arg(Arg::new("config")
2236+
/// .long("config")
2237+
/// .env("CONFIG")
2238+
/// .env_prefix("MYAPP"));
2239+
/// // env var will be MYAPP_CONFIG
2240+
/// # }
2241+
/// ```
2242+
///
2243+
/// [`Arg::env`]: Arg::env()
2244+
/// [`Command::next_env_prefix`]: crate::Command::next_env_prefix()
2245+
#[cfg(all(feature = "env", feature = "string"))]
2246+
#[inline]
2247+
#[must_use]
2248+
pub fn env_prefix(mut self, prefix: impl IntoResettable<OsStr>) -> Self {
2249+
self.env_prefix = Some(prefix.into_resettable().into_option());
2250+
self
2251+
}
22152252
}
22162253

22172254
/// # Help
@@ -4398,6 +4435,18 @@ impl Arg {
43984435
self.env.as_ref().map(|x| x.0.as_os_str())
43994436
}
44004437

4438+
/// Get the env variable prefix for this argument, if any.
4439+
///
4440+
/// See [`Arg::env_prefix`].
4441+
#[cfg(all(feature = "env", feature = "string"))]
4442+
#[inline]
4443+
pub fn get_env_prefix(&self) -> Option<&std::ffi::OsStr> {
4444+
self.env_prefix
4445+
.as_ref()
4446+
.and_then(|p| p.as_ref())
4447+
.map(|p| p.as_os_str())
4448+
}
4449+
44014450
/// Get the default values specified for this argument, if any
44024451
///
44034452
/// # Examples
@@ -4820,6 +4869,10 @@ impl fmt::Debug for Arg {
48204869
{
48214870
ds = ds.field("env", &self.env);
48224871
}
4872+
#[cfg(all(feature = "env", feature = "string"))]
4873+
{
4874+
ds = ds.field("env_prefix", &self.env_prefix);
4875+
}
48234876

48244877
ds.finish()
48254878
}

clap_builder/src/builder/command.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use crate::builder::ext::Extension;
1515
use crate::builder::ext::Extensions;
1616
use crate::builder::ArgAction;
1717
use crate::builder::IntoResettable;
18+
#[cfg(all(feature = "env", feature = "string"))]
19+
use crate::builder::OsStr;
1820
use crate::builder::PossibleValue;
1921
use crate::builder::Str;
2022
use crate::builder::StyledStr;
@@ -101,6 +103,8 @@ pub struct Command {
101103
subcommands: Vec<Command>,
102104
groups: Vec<ArgGroup>,
103105
current_help_heading: Option<Str>,
106+
#[cfg(all(feature = "env", feature = "string"))]
107+
current_env_prefix: Option<OsStr>,
104108
current_disp_ord: Option<usize>,
105109
subcommand_value_name: Option<Str>,
106110
subcommand_heading: Option<Str>,
@@ -185,6 +189,11 @@ impl Command {
185189

186190
arg.help_heading
187191
.get_or_insert_with(|| self.current_help_heading.clone());
192+
#[cfg(all(feature = "env", feature = "string"))]
193+
{
194+
arg.env_prefix
195+
.get_or_insert_with(|| self.current_env_prefix.clone());
196+
}
188197
self.args.push(arg);
189198
}
190199

@@ -2376,6 +2385,41 @@ impl Command {
23762385
self
23772386
}
23782387

2388+
/// Sets a prefix to be prepended to the environment variable names of all
2389+
/// subsequent arguments added to this command.
2390+
///
2391+
/// This is a stateful method that affects all future [`Arg`]s added via
2392+
/// [`Command::arg`]. An explicit [`Arg::env_prefix`] on an argument takes
2393+
/// precedence over this.
2394+
///
2395+
/// The prefix and the argument's env name will be joined with `_`.
2396+
///
2397+
/// This is modeled after [`Command::next_help_heading`].
2398+
///
2399+
/// # Examples
2400+
///
2401+
/// ```rust
2402+
/// # #[cfg(all(feature = "env", feature = "string"))] {
2403+
/// # use clap_builder as clap;
2404+
/// # use clap::{Command, Arg};
2405+
/// let cmd = Command::new("myapp")
2406+
/// .next_env_prefix("MYAPP")
2407+
/// .arg(Arg::new("config").long("config").env("CONFIG"))
2408+
/// .arg(Arg::new("verbose").long("verbose"));
2409+
/// // config's env var will be MYAPP_CONFIG
2410+
/// # }
2411+
/// ```
2412+
///
2413+
/// [`Command::arg`]: Command::arg()
2414+
/// [`Arg::env_prefix`]: crate::Arg::env_prefix()
2415+
#[cfg(all(feature = "env", feature = "string"))]
2416+
#[inline]
2417+
#[must_use]
2418+
pub fn next_env_prefix(mut self, prefix: impl IntoResettable<OsStr>) -> Self {
2419+
self.current_env_prefix = prefix.into_resettable().into_option();
2420+
self
2421+
}
2422+
23792423
/// Change the starting value for assigning future display orders for args.
23802424
///
23812425
/// This will be used for any arg that hasn't had [`Arg::display_order`] called.
@@ -3832,6 +3876,13 @@ impl Command {
38323876
self.current_help_heading.as_deref()
38333877
}
38343878

3879+
/// Get the env prefix specified via [`Command::next_env_prefix`].
3880+
#[cfg(all(feature = "env", feature = "string"))]
3881+
#[inline]
3882+
pub fn get_next_env_prefix(&self) -> Option<&std::ffi::OsStr> {
3883+
self.current_env_prefix.as_ref().map(|s| s.as_os_str())
3884+
}
3885+
38353886
/// Iterate through the *visible* aliases for this subcommand.
38363887
#[inline]
38373888
pub fn get_visible_aliases(&self) -> impl Iterator<Item = &str> + '_ {
@@ -4436,6 +4487,21 @@ impl Command {
44364487
}
44374488
}
44384489

4490+
// Apply env prefix to env variable names
4491+
#[cfg(all(feature = "env", feature = "string"))]
4492+
if let Some(Some(ref prefix)) = a.env_prefix {
4493+
if let Some((ref env_name, _)) = a.env {
4494+
let mut prefixed = prefix.to_os_string();
4495+
prefixed.push("_");
4496+
prefixed.push(env_name.as_os_str());
4497+
let value = env::var_os(&prefixed);
4498+
a.env = Some((
4499+
OsStr::from_string(prefixed),
4500+
value,
4501+
));
4502+
}
4503+
}
4504+
44394505
// Figure out implied settings
44404506
a._build();
44414507
if hide_pv && a.is_takes_value_set() {
@@ -5216,6 +5282,8 @@ impl Default for Command {
52165282
subcommands: Default::default(),
52175283
groups: Default::default(),
52185284
current_help_heading: Default::default(),
5285+
#[cfg(all(feature = "env", feature = "string"))]
5286+
current_env_prefix: Default::default(),
52195287
current_disp_ord: Some(0),
52205288
subcommand_value_name: Default::default(),
52215289
subcommand_heading: Default::default(),

clap_derive/src/attr.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ impl Parse for ClapAttr {
8282
"skip" => Some(MagicAttrName::Skip),
8383
"next_display_order" => Some(MagicAttrName::NextDisplayOrder),
8484
"next_help_heading" => Some(MagicAttrName::NextHelpHeading),
85+
"next_env_prefix" => Some(MagicAttrName::NextEnvPrefix),
8586
"default_value_t" => Some(MagicAttrName::DefaultValueT),
8687
"default_values_t" => Some(MagicAttrName::DefaultValuesT),
8788
"default_value_os_t" => Some(MagicAttrName::DefaultValueOsT),
@@ -168,6 +169,7 @@ pub(crate) enum MagicAttrName {
168169
DefaultValuesOsT,
169170
NextDisplayOrder,
170171
NextHelpHeading,
172+
NextEnvPrefix,
171173
}
172174

173175
#[derive(Clone)]

clap_derive/src/derives/args.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ pub(crate) fn gen_augment(
227227
};
228228

229229
let next_help_heading = item.next_help_heading();
230+
let next_env_prefix = item.next_env_prefix();
230231
let next_display_order = item.next_display_order();
231232
let flatten_group_assert = if matches!(**ty, Ty::Option) {
232233
quote_spanned! { kind.span()=>
@@ -240,15 +241,17 @@ pub(crate) fn gen_augment(
240241
#flatten_group_assert
241242
let #app_var = #app_var
242243
#next_help_heading
243-
#next_display_order;
244+
#next_display_order
245+
#next_env_prefix;
244246
let #app_var = <#inner_type as clap::Args>::augment_args_for_update(#app_var);
245247
})
246248
} else {
247249
Some(quote_spanned! { kind.span()=>
248250
#flatten_group_assert
249251
let #app_var = #app_var
250252
#next_help_heading
251-
#next_display_order;
253+
#next_display_order
254+
#next_env_prefix;
252255
let #app_var = <#inner_type as clap::Args>::augment_args(#app_var);
253256
})
254257
}

clap_derive/src/derives/subcommand.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,21 +188,24 @@ fn gen_augment(
188188
quote!()
189189
};
190190
let next_help_heading = item.next_help_heading();
191+
let next_env_prefix = item.next_env_prefix();
191192
let next_display_order = item.next_display_order();
192193
let subcommand = if override_required {
193194
quote! {
194195
#deprecations
195196
let #app_var = #app_var
196197
#next_help_heading
197-
#next_display_order;
198+
#next_display_order
199+
#next_env_prefix;
198200
let #app_var = <#ty as clap::Subcommand>::augment_subcommands_for_update(#app_var);
199201
}
200202
} else {
201203
quote! {
202204
#deprecations
203205
let #app_var = #app_var
204206
#next_help_heading
205-
#next_display_order;
207+
#next_display_order
208+
#next_env_prefix;
206209
let #app_var = <#ty as clap::Subcommand>::augment_subcommands(#app_var);
207210
}
208211
};

clap_derive/src/item.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub(crate) struct Item {
4444
force_long_help: bool,
4545
next_display_order: Option<Method>,
4646
next_help_heading: Option<Method>,
47+
next_env_prefix: Option<Method>,
4748
is_enum: bool,
4849
is_positional: bool,
4950
skip_group: bool,
@@ -273,6 +274,7 @@ impl Item {
273274
force_long_help: false,
274275
next_display_order: None,
275276
next_help_heading: None,
277+
next_env_prefix: None,
276278
is_enum: false,
277279
is_positional: true,
278280
skip_group: false,
@@ -819,6 +821,13 @@ impl Item {
819821
self.next_help_heading = Some(Method::new(attr.name.clone(), quote!(#expr)));
820822
}
821823

824+
Some(MagicAttrName::NextEnvPrefix) => {
825+
assert_attr_kind(attr, &[AttrKind::Command])?;
826+
827+
let expr = attr.value_or_abort()?;
828+
self.next_env_prefix = Some(Method::new(attr.name.clone(), quote!(#expr)));
829+
}
830+
822831
Some(MagicAttrName::RenameAll) => {
823832
let lit = attr.lit_str_or_abort()?;
824833
self.casing = CasingStyle::from_lit(lit)?;
@@ -967,9 +976,11 @@ impl Item {
967976
pub(crate) fn initial_top_level_methods(&self) -> TokenStream {
968977
let next_display_order = self.next_display_order.as_ref().into_iter();
969978
let next_help_heading = self.next_help_heading.as_ref().into_iter();
979+
let next_env_prefix = self.next_env_prefix.as_ref().into_iter();
970980
quote!(
971981
#(#next_display_order)*
972982
#(#next_help_heading)*
983+
#(#next_env_prefix)*
973984
)
974985
}
975986

@@ -1011,6 +1022,11 @@ impl Item {
10111022
quote!( #(#next_help_heading)* )
10121023
}
10131024

1025+
pub(crate) fn next_env_prefix(&self) -> TokenStream {
1026+
let next_env_prefix = self.next_env_prefix.as_ref().into_iter();
1027+
quote!( #(#next_env_prefix)* )
1028+
}
1029+
10141030
pub(crate) fn id(&self) -> &Name {
10151031
&self.name
10161032
}

0 commit comments

Comments
 (0)