diff --git a/lib/src/spec/arg.rs b/lib/src/spec/arg.rs index 6797d87ae..8c8375a18 100644 --- a/lib/src/spec/arg.rs +++ b/lib/src/spec/arg.rs @@ -334,6 +334,31 @@ impl FromStr for SpecArg { } } +/// A clap argument's defaults, as the spec has to record them. +/// +/// clap splits a value by the argument's `value_delimiter` before anyone sees it, defaults +/// included — so `default_value = "a,b,c"` with `value_delimiter = ','` is three values, not one. +/// The spec has no delimiter of its own; it has a list, which is the same statement. Recording the +/// joined string instead described a CLI whose default is a single value that its own `choices` +/// forbid, which is how mise's `--fs-events` reached the spec. +#[cfg(feature = "clap")] +pub(crate) fn default_values(arg: &clap::Arg) -> Vec { + let raw = arg + .get_default_values() + .iter() + .map(|v| v.to_string_lossy().to_string()); + match arg.get_value_delimiter() { + Some(delimiter) => raw + .flat_map(|v| { + v.split(delimiter) + .map(|part| part.to_string()) + .collect::>() + }) + .collect(), + None => raw.collect(), + } +} + #[cfg(feature = "clap")] impl From<&clap::Arg> for SpecArg { fn from(arg: &clap::Arg) -> Self { @@ -376,11 +401,7 @@ impl From<&clap::Arg> for SpecArg { var_max: None, var_min: None, hide, - default: arg - .get_default_values() - .iter() - .map(|v| v.to_string_lossy().to_string()) - .collect(), + default: default_values(arg), choices: None, effect: None, env: None, diff --git a/lib/src/spec/flag.rs b/lib/src/spec/flag.rs index dd2fdaa38..c43c6f8d6 100644 --- a/lib/src/spec/flag.rs +++ b/lib/src/spec/flag.rs @@ -535,11 +535,7 @@ impl From<&clap::Arg> for SpecFlag { c.get_action(), clap::ArgAction::Count | clap::ArgAction::Append ); - let default: Vec = c - .get_default_values() - .iter() - .map(|s| s.to_string_lossy().to_string()) - .collect(); + let default: Vec = crate::spec::arg::default_values(c); let short = c.get_short_and_visible_aliases().unwrap_or_default(); let long = c .get_long_and_visible_aliases() diff --git a/lib/src/spec/mod.rs b/lib/src/spec/mod.rs index 07d425195..c4e89e94b 100644 --- a/lib/src/spec/mod.rs +++ b/lib/src/spec/mod.rs @@ -688,6 +688,37 @@ source_code_link_template "https://github.com/jdx/mise/blob/main/src/cli/{{path} "#); } + #[test] + #[cfg(feature = "clap")] + fn a_delimited_default_becomes_the_values_clap_would_split_it_into() { + // clap splits by the delimiter before anyone sees a value, defaults included, so the + // joined string is not something the CLI ever holds. The spec has no delimiter — it has + // a list, which says the same thing. + // + // mise's `--fs-events` is why: `default_value = "create,remove,rename,modify,metadata"` + // beside `value_parser` listing those as its choices, so the recorded default was a + // single value its own spec forbade. + let cmd = clap::Command::new("test").arg( + clap::Arg::new("events") + .long("events") + .value_delimiter(',') + .action(clap::ArgAction::Append) + .value_parser(["a", "b", "c"]) + .default_value("a,b"), + ); + let spec = Spec::from(&cmd); + let flag = spec.cmd.flags.iter().find(|f| f.name == "events").unwrap(); + assert_eq!(flag.default, ["a", "b"]); + + // And without a delimiter the value is whatever was written, commas and all: a path list + // is not every CLI's idea of a separator, so splitting on speculation would be worse. + let cmd = clap::Command::new("test") + .arg(clap::Arg::new("events").long("events").default_value("a,b")); + let spec = Spec::from(&cmd); + let flag = spec.cmd.flags.iter().find(|f| f.name == "events").unwrap(); + assert_eq!(flag.default, ["a,b"]); + } + macro_rules! extract_usage_tests { ($($name:ident: $input:expr, $expected:expr,)*) => { $(