Skip to content

Commit 46986d6

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 f61c103 commit 46986d6

8 files changed

Lines changed: 177 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>>,
@@ -2221,6 +2223,41 @@ impl Arg {
22212223
pub fn env_os(self, name: impl Into<OsStr>) -> Self {
22222224
self.env(name)
22232225
}
2226+
2227+
/// Sets an env variable prefix for this argument.
2228+
///
2229+
/// When set, the env variable name specified via [`Arg::env`] will be
2230+
/// prefixed with this value (joined by `_`) during build.
2231+
///
2232+
/// An explicit `Arg::env_prefix` takes precedence over
2233+
/// [`Command::next_env_prefix`].
2234+
///
2235+
/// This can be reset with `None`.
2236+
///
2237+
/// # Examples
2238+
///
2239+
/// ```rust
2240+
/// # #[cfg(all(feature = "env", feature = "string"))] {
2241+
/// # use clap_builder as clap;
2242+
/// # use clap::{Command, Arg};
2243+
/// let cmd = Command::new("myapp")
2244+
/// .arg(Arg::new("config")
2245+
/// .long("config")
2246+
/// .env("CONFIG")
2247+
/// .env_prefix("MYAPP"));
2248+
/// // env var will be MYAPP_CONFIG
2249+
/// # }
2250+
/// ```
2251+
///
2252+
/// [`Arg::env`]: Arg::env()
2253+
/// [`Command::next_env_prefix`]: crate::Command::next_env_prefix()
2254+
#[cfg(all(feature = "env", feature = "string"))]
2255+
#[inline]
2256+
#[must_use]
2257+
pub fn env_prefix(mut self, prefix: impl IntoResettable<OsStr>) -> Self {
2258+
self.env_prefix = Some(prefix.into_resettable().into_option());
2259+
self
2260+
}
22242261
}
22252262

22262263
/// # Help
@@ -4407,6 +4444,18 @@ impl Arg {
44074444
self.env.as_ref().map(|x| x.0.as_os_str())
44084445
}
44094446

4447+
/// Get the env variable prefix for this argument, if any.
4448+
///
4449+
/// See [`Arg::env_prefix`].
4450+
#[cfg(all(feature = "env", feature = "string"))]
4451+
#[inline]
4452+
pub fn get_env_prefix(&self) -> Option<&std::ffi::OsStr> {
4453+
self.env_prefix
4454+
.as_ref()
4455+
.and_then(|p| p.as_ref())
4456+
.map(|p| p.as_os_str())
4457+
}
4458+
44104459
/// Get the default values specified for this argument, if any
44114460
///
44124461
/// # Examples
@@ -4829,6 +4878,10 @@ impl fmt::Debug for Arg {
48294878
{
48304879
ds = ds.field("env", &self.env);
48314880
}
4881+
#[cfg(all(feature = "env", feature = "string"))]
4882+
{
4883+
ds = ds.field("env_prefix", &self.env_prefix);
4884+
}
48324885

48334886
ds.finish()
48344887
}

clap_builder/src/builder/command.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use std::path::Path;
1111
// Internal
1212
use crate::builder::ArgAction;
1313
use crate::builder::IntoResettable;
14+
#[cfg(all(feature = "env", feature = "string"))]
15+
use crate::builder::OsStr;
1416
use crate::builder::PossibleValue;
1517
use crate::builder::Str;
1618
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

@@ -2378,6 +2387,41 @@ impl Command {
23782387
self
23792388
}
23802389

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

3881+
/// Get the env prefix specified via [`Command::next_env_prefix`].
3882+
#[cfg(all(feature = "env", feature = "string"))]
3883+
#[inline]
3884+
pub fn get_next_env_prefix(&self) -> Option<&std::ffi::OsStr> {
3885+
self.current_env_prefix.as_ref().map(|s| s.as_os_str())
3886+
}
3887+
38373888
/// Iterate through the *visible* aliases for this subcommand.
38383889
#[inline]
38393890
pub fn get_visible_aliases(&self) -> impl Iterator<Item = &str> + '_ {
@@ -4440,6 +4491,18 @@ impl Command {
44404491
}
44414492
}
44424493

4494+
// Apply env prefix to env variable names
4495+
#[cfg(all(feature = "env", feature = "string"))]
4496+
if let Some(Some(ref prefix)) = a.env_prefix {
4497+
if let Some((ref env_name, _)) = a.env {
4498+
let mut prefixed = prefix.to_os_string();
4499+
prefixed.push("_");
4500+
prefixed.push(env_name.as_os_str());
4501+
let value = env::var_os(&prefixed);
4502+
a.env = Some((OsStr::from_string(prefixed), value));
4503+
}
4504+
}
4505+
44434506
// Figure out implied settings
44444507
a._build();
44454508
if hide_pv && a.is_takes_value_set() {
@@ -5221,6 +5284,8 @@ impl Default for Command {
52215284
subcommands: Default::default(),
52225285
groups: Default::default(),
52235286
current_help_heading: Default::default(),
5287+
#[cfg(all(feature = "env", feature = "string"))]
5288+
current_env_prefix: Default::default(),
52245289
current_disp_ord: Some(0),
52255290
subcommand_value_name: Default::default(),
52265291
subcommand_heading: Default::default(),

clap_derive/src/attr.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ impl Parse for ClapAttr {
8181
"skip" => Some(MagicAttrName::Skip),
8282
"next_display_order" => Some(MagicAttrName::NextDisplayOrder),
8383
"next_help_heading" => Some(MagicAttrName::NextHelpHeading),
84+
"next_env_prefix" => Some(MagicAttrName::NextEnvPrefix),
8485
"default_value_t" => Some(MagicAttrName::DefaultValueT),
8586
"default_values_t" => Some(MagicAttrName::DefaultValuesT),
8687
"default_value_os_t" => Some(MagicAttrName::DefaultValueOsT),
@@ -167,6 +168,7 @@ pub(crate) enum MagicAttrName {
167168
DefaultValuesOsT,
168169
NextDisplayOrder,
169170
NextHelpHeading,
171+
NextEnvPrefix,
170172
}
171173

172174
#[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)