diff --git a/clap_builder/src/builder/arg.rs b/clap_builder/src/builder/arg.rs index 9ed75ece8c9..5ff28cfafba 100644 --- a/clap_builder/src/builder/arg.rs +++ b/clap_builder/src/builder/arg.rs @@ -2211,6 +2211,178 @@ impl Arg { pub fn env_os(self, name: impl Into) -> Self { self.env(name) } + + /// Read from `names` environment variable when argument is not present. + /// + /// If it is not present in the environment, then default + /// rules will apply. + /// + /// If user sets the argument in the environment: + /// - When [`Arg::action(ArgAction::Set)`] is not set, the flag is considered raised. + /// - When [`Arg::action(ArgAction::Set)`] is set, + /// [`ArgMatches::get_one`][crate::ArgMatches::get_one] will + /// return value of the environment variable. + /// + /// If user doesn't set the argument in the environment: + /// - When [`Arg::action(ArgAction::Set)`] is not set, the flag is considered off. + /// - When [`Arg::action(ArgAction::Set)`] is set, + /// [`ArgMatches::get_one`][crate::ArgMatches::get_one] will + /// return the default specified. + /// + /// Like with command-line values, this will be split by [`Arg::value_delimiter`]. + /// + /// # Examples + /// + /// In this example, we show the variable coming from the environment. + /// Note that because of the first `MY_FLAG` has a value, so the next environment variable is not valid again. + /// + /// ```rust + /// # use clap_builder as clap; + /// # use std::env; + /// # use clap::{Command, Arg, ArgAction}; + /// + /// env::set_var("MY_FLAG", "one"); + /// env::set_var("MY_SECOND_FLAG", "two"); + /// + /// let m = Command::new("prog") + /// .arg(Arg::new("flag") + /// .long("flag") + /// .envs(["MY_FLAG", "MY_SECOND_FLAG"]) + /// .action(ArgAction::Set)) + /// .get_matches_from(vec![ + /// "prog" + /// ]); + /// + /// assert_eq!(m.get_one::("flag").unwrap(), "one"); + /// ``` + /// + /// In this example, because `prog` is a flag that accepts an optional, case-insensitive + /// boolean literal. + /// + /// Note that the value parser controls how flags are parsed. In this case we've selected + /// [`FalseyValueParser`][crate::builder::FalseyValueParser]. A `false` literal is `n`, `no`, + /// `f`, `false`, `off` or `0`. An absent environment variable will also be considered as + /// `false`. Anything else will considered as `true`. + /// + /// ```rust + /// # use clap_builder as clap; + /// # use std::env; + /// # use clap::{Command, Arg, ArgAction}; + /// # use clap::builder::FalseyValueParser; + /// + /// env::set_var("TRUE_FLAG", "true"); + /// env::set_var("FALSE_FLAG", "0"); + /// + /// let m = Command::new("prog") + /// .arg(Arg::new("true_flag") + /// .long("true_flag") + /// .action(ArgAction::SetTrue) + /// .value_parser(FalseyValueParser::new()) + /// .envs(["TRUE_FLAG"])) + /// .arg(Arg::new("false_flag") + /// .long("false_flag") + /// .action(ArgAction::SetTrue) + /// .value_parser(FalseyValueParser::new()) + /// .envs(["FALSE_FLAG"])) + /// .arg(Arg::new("absent_flag") + /// .long("absent_flag") + /// .action(ArgAction::SetTrue) + /// .value_parser(FalseyValueParser::new()) + /// .envs(["ABSENT_FLAG"])) + /// .get_matches_from(vec![ + /// "prog" + /// ]); + /// + /// assert!(m.get_flag("true_flag")); + /// assert!(!m.get_flag("false_flag")); + /// assert!(!m.get_flag("absent_flag")); + /// ``` + /// + /// In this example, we show the variable coming from an option on the CLI: + /// + /// ```rust + /// # use clap_builder as clap; + /// # use std::env; + /// # use clap::{Command, Arg, ArgAction}; + /// + /// env::set_var("MY_FLAG", "env"); + /// + /// let m = Command::new("prog") + /// .arg(Arg::new("flag") + /// .long("flag") + /// .envs(["MY_FLAG"]) + /// .action(ArgAction::Set)) + /// .get_matches_from(vec![ + /// "prog", "--flag", "opt" + /// ]); + /// + /// assert_eq!(m.get_one::("flag").unwrap(), "opt"); + /// ``` + /// + /// In this example, we show the variable coming from the environment even with the + /// presence of a default: + /// + /// ```rust + /// # use clap_builder as clap; + /// # use std::env; + /// # use clap::{Command, Arg, ArgAction}; + /// + /// env::set_var("MY_FLAG", "env"); + /// + /// let m = Command::new("prog") + /// .arg(Arg::new("flag") + /// .long("flag") + /// .envs(["MY_FLAG"]) + /// .action(ArgAction::Set) + /// .default_value("default")) + /// .get_matches_from(vec![ + /// "prog" + /// ]); + /// + /// assert_eq!(m.get_one::("flag").unwrap(), "env"); + /// ``` + /// + /// In this example, we show the use of multiple values in a single environment variable: + /// + /// ```rust + /// # use clap_builder as clap; + /// # use std::env; + /// # use clap::{Command, Arg, ArgAction}; + /// + /// env::set_var("MY_FLAG_MULTI", "env1,env2"); + /// + /// let m = Command::new("prog") + /// .arg(Arg::new("flag") + /// .long("flag") + /// .envs(["MY_FLAG_MULTI"]) + /// .action(ArgAction::Set) + /// .num_args(1..) + /// .value_delimiter(',')) + /// .get_matches_from(vec![ + /// "prog" + /// ]); + /// + /// assert_eq!(m.get_many::("flag").unwrap().collect::>(), vec!["env1", "env2"]); + /// ``` + /// [`Arg::action(ArgAction::Set)`]: Arg::action() + /// [`Arg::value_delimiter(',')`]: Arg::value_delimiter() + #[cfg(feature = "env")] + #[inline] + #[must_use] + pub fn envs(mut self, names: impl IntoIterator>) -> Self { + for name in names { + if let Some(name) = name.into_resettable().into_option() { + if let Some(value) = env::var_os(&name) { + self.env = Some((name, Some(value))); + break; + } + } else { + self.env = None; + break; + } + } + self + } } /// # Help diff --git a/tests/builder/envs.rs b/tests/builder/envs.rs new file mode 100644 index 00000000000..e3c0e73b758 --- /dev/null +++ b/tests/builder/envs.rs @@ -0,0 +1,456 @@ +#![cfg(feature = "env")] + +use std::env; +use std::ffi::OsStr; + +use clap::{arg, builder::FalseyValueParser, Arg, ArgAction, Command}; + +#[test] +fn first_env() { + env::set_var("CLP_TEST_ENV_ONE", "one"); + env::set_var("CLP_TEST_ENV_TWO", "two"); + env::set_var("CLP_TEST_ENV_THREE", "three"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_ONE", "CLP_TEST_ENV_TWO", "CLP_TEST_ENV_THREE"]) + .action(ArgAction::Set), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.value_source("arg").unwrap(), + clap::parser::ValueSource::EnvVariable + ); + assert_eq!( + m.get_one::("arg").map(|v| v.as_str()).unwrap(), + "one" + ); +} + +#[test] +fn last_env() { + env::remove_var("CLP_TEST_ENV_ONE"); + env::remove_var("CLP_TEST_ENV_TWO"); + env::set_var("CLP_TEST_ENV_THREE", "three"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_ONE", "CLP_TEST_ENV_TWO", "CLP_TEST_ENV_THREE"]) + .action(ArgAction::Set), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.value_source("arg").unwrap(), + clap::parser::ValueSource::EnvVariable + ); + assert_eq!( + m.get_one::("arg").map(|v| v.as_str()).unwrap(), + "three" + ); +} + +#[test] +fn env_bool_literal() { + env::set_var("CLP_TEST_FLAG_TRUE", "On"); + env::set_var("CLP_TEST_FLAG_FALSE", "nO"); + + let r = Command::new("df") + .arg( + Arg::new("present") + .short('p') + .envs(["CLP_TEST_FLAG_TRUE"]) + .action(ArgAction::SetTrue) + .value_parser(FalseyValueParser::new()), + ) + .arg( + Arg::new("negated") + .short('n') + .envs(["CLP_TEST_FLAG_FALSE"]) + .action(ArgAction::SetTrue) + .value_parser(FalseyValueParser::new()), + ) + .arg( + Arg::new("absent") + .short('a') + .envs(["CLP_TEST_FLAG_ABSENT"]) + .action(ArgAction::SetTrue) + .value_parser(FalseyValueParser::new()), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(*m.get_one::("present").expect("defaulted by clap")); + assert!(!*m.get_one::("negated").expect("defaulted by clap")); + assert!(!*m.get_one::("absent").expect("defaulted by clap")); +} + +#[test] +fn env_os() { + env::set_var("CLP_TEST_ENV_OS_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs([OsStr::new("CLP_TEST_ENV_OS_ONE"), OsStr::new("CLP_TEST_ENV_OS_TWO")]) + .action(ArgAction::Set), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.get_one::("arg").map(|v| v.as_str()).unwrap(), + "env" + ); +} + +#[test] +fn no_env() { + // All the other tests use the presence of the Environment variable... + // we need another variable just in case one of the others is running at the same time... + env::remove_var("CLP_TEST_ENV_NONE_ONE"); + env::remove_var("CLP_TEST_ENV_NONE_TWO"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_NONE_ONE", "CLP_TEST_ENV_NONE_TWO"]) + .action(ArgAction::Set), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(!m.contains_id("arg")); + assert_eq!(m.value_source("arg"), None); + assert_eq!(m.get_one::("arg").map(|v| v.as_str()), None); +} + +#[test] +fn no_env_no_takes_value() { + // All the other tests use the presence of the Environment variable... + // we need another variable just in case one of the others is running at the same time... + env::remove_var("CLP_TEST_ENV_NONE_ONE"); + env::remove_var("CLP_TEST_ENV_NONE_TWO"); + + let r = Command::new("df") + .arg(arg!([arg] "some opt").envs(["CLP_TEST_ENV_NONE_ONE", "CLP_TEST_ENV_NONE_TWO"])) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(!m.contains_id("arg")); + assert_eq!(m.value_source("arg"), None); + assert_eq!(m.get_one::("arg").map(|v| v.as_str()), None); +} + +#[test] +fn with_default() { + env::set_var("CLP_TEST_ENV_WD_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_WD_ONE", "CLP_TEST_ENV_WD_TWO"]) + .action(ArgAction::Set) + .default_value("default"), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.value_source("arg").unwrap(), + clap::parser::ValueSource::EnvVariable + ); + assert_eq!( + m.get_one::("arg").map(|v| v.as_str()).unwrap(), + "env" + ); +} + +#[test] +fn opt_user_override() { + env::set_var("CLP_TEST_ENV_OR_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!(--arg [FILE] "some arg") + .envs(["CLP_TEST_ENV_OR_ONE", "CLP_TEST_ENV_OR_TWO"]) + .action(ArgAction::Set), + ) + .try_get_matches_from(vec!["", "--arg", "opt"]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.value_source("arg").unwrap(), + clap::parser::ValueSource::CommandLine + ); + assert_eq!( + m.get_one::("arg").map(|v| v.as_str()).unwrap(), + "opt" + ); + + // see https://github.com/clap-rs/clap/issues/1835 + let values: Vec<_> = m + .get_many::("arg") + .unwrap() + .map(|v| v.as_str()) + .collect(); + assert_eq!(values, vec!["opt"]); +} + +#[test] +fn positionals() { + env::set_var("CLP_TEST_ENV_P_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_P_ONE", "CLP_TEST_ENV_P_TWO"]) + .action(ArgAction::Set), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.value_source("arg").unwrap(), + clap::parser::ValueSource::EnvVariable + ); + assert_eq!( + m.get_one::("arg").map(|v| v.as_str()).unwrap(), + "env" + ); +} + +#[test] +fn positionals_user_override() { + env::set_var("CLP_TEST_ENV_POR_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_POR_ONE", "CLP_TEST_ENV_POR_TWO"]) + .action(ArgAction::Set), + ) + .try_get_matches_from(vec!["", "opt"]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.value_source("arg").unwrap(), + clap::parser::ValueSource::CommandLine + ); + assert_eq!( + m.get_one::("arg").map(|v| v.as_str()).unwrap(), + "opt" + ); + + // see https://github.com/clap-rs/clap/issues/1835 + let values: Vec<_> = m + .get_many::("arg") + .unwrap() + .map(|v| v.as_str()) + .collect(); + assert_eq!(values, vec!["opt"]); +} + +#[test] +fn multiple_one() { + env::set_var("CLP_TEST_ENV_MO_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_MO_ONE", "CLP_TEST_ENV_MO_TWO"]) + .action(ArgAction::Set) + .value_delimiter(',') + .num_args(1..), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.get_many::("arg") + .unwrap() + .map(|v| v.as_str()) + .collect::>(), + vec!["env"] + ); +} + +#[test] +fn multiple_three() { + env::set_var("CLP_TEST_ENV_MULTI1_ONE", "env1,env2,env3"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_MULTI1_ONE", "CLP_TEST_ENV_MULTI1_TWO"]) + .action(ArgAction::Set) + .value_delimiter(',') + .num_args(1..), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.get_many::("arg") + .unwrap() + .map(|v| v.as_str()) + .collect::>(), + vec!["env1", "env2", "env3"] + ); +} + +#[test] +fn multiple_no_delimiter() { + env::set_var("CLP_TEST_ENV_MULTI2_ONE", "env1 env2 env3"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_MULTI2_ONE", "CLP_TEST_ENV_MULTI2_TWO"]) + .action(ArgAction::Set) + .num_args(1..), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.get_many::("arg") + .unwrap() + .map(|v| v.as_str()) + .collect::>(), + vec!["env1 env2 env3"] + ); +} + +#[test] +fn possible_value() { + env::set_var("CLP_TEST_ENV_PV_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_PV_ONE", "CLP_TEST_ENV_PV_TWO"]) + .action(ArgAction::Set) + .value_parser(["env"]), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.get_one::("arg").map(|v| v.as_str()).unwrap(), + "env" + ); +} + +#[test] +fn not_possible_value() { + env::set_var("CLP_TEST_ENV_NPV_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_NPV_ONE", "CLP_TEST_ENV_NPV_TWO"]) + .action(ArgAction::Set) + .value_parser(["never"]), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_err()); +} + +#[test] +fn value_parser() { + env::set_var("CLP_TEST_ENV_VDOR_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_VDOR_ONE", "CLP_TEST_ENV_VDOR_TWO"]) + .action(ArgAction::Set) + .value_parser(|s: &str| -> Result { + if s == "env" { + Ok(s.to_owned()) + } else { + Err("not equal".to_owned()) + } + }), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_ok(), "{}", r.unwrap_err()); + let m = r.unwrap(); + assert!(m.contains_id("arg")); + assert_eq!( + m.get_one::("arg").map(|v| v.as_str()).unwrap(), + "env" + ); +} + +#[test] +fn value_parser_output() { + env::set_var("CLP_TEST_ENV_VO_ONE", "42"); + + let m = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_VO_ONE", "CLP_TEST_ENV_VO_TWO"]) + .action(ArgAction::Set) + .value_parser(clap::value_parser!(i32)), + ) + .try_get_matches_from(vec![""]) + .unwrap(); + + assert_eq!(*m.get_one::("arg").unwrap(), 42); +} + +#[test] +fn value_parser_invalid() { + env::set_var("CLP_TEST_ENV_IV_ONE", "env"); + + let r = Command::new("df") + .arg( + arg!([arg] "some opt") + .envs(["CLP_TEST_ENV_IV_ONE", "CLP_TEST_ENV_IV_TWO"]) + .action(ArgAction::Set) + .value_parser(|s: &str| -> Result { + if s != "env" { + Ok(s.to_owned()) + } else { + Err("is equal".to_string()) + } + }), + ) + .try_get_matches_from(vec![""]); + + assert!(r.is_err()); +}