Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/usage.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ This is used internally by shell completion scripts to provide intelligent compl
flag "-s --spec" help="Raw string spec input" {
arg <SPEC>
}
flag --cword help="Current word index" {
flag --cword help="Current word index" allow_hyphen_values=#true {
arg <CWORD>
}
flag --shell default=bash {
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/reference/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
"name": "CWORD",
"usage": "<CWORD>",
"required": true,
"double_dash": "Optional",
"double_dash": "Automatic",
"hide": false
}
},
Expand Down
178 changes: 153 additions & 25 deletions lib/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,27 @@ fn parse_partial_with_env(
}
}

if w.starts_with('-')
&& out
.flag_awaiting_value
.last()
.is_some_and(|flag| flag.allow_hyphen_values())
{
let should_return = drain_pending_flag_values(
spec,
&out.cmd,
&mut out.errors,
&mut out.flags,
&mut out.flag_awaiting_value,
&mut w,
custom_env,
)?;
if should_return {
return Ok(out);
}
continue;
}

// long flags
if enable_flags && w.starts_with("--") {
grouped_flag = false;
Expand Down Expand Up @@ -633,31 +654,17 @@ fn parse_partial_with_env(
}

if !out.flag_awaiting_value.is_empty() {
while let Some(flag) = out.flag_awaiting_value.pop() {
let arg = flag.arg.as_ref().unwrap();
if validate_choices(
spec,
&out.cmd,
&mut out.errors,
ChoiceTarget::option(&flag),
&w,
arg.choices.as_ref(),
custom_env,
)? {
return Ok(out);
}
if flag.var {
let arr = out
.flags
.entry(flag)
.or_insert_with(|| ParseValue::MultiString(vec![]))
.try_as_multi_string_mut()
.unwrap();
arr.push(w);
} else {
out.flags.insert(flag, ParseValue::String(w));
}
w = "".to_string();
let should_return = drain_pending_flag_values(
spec,
&out.cmd,
&mut out.errors,
&mut out.flags,
&mut out.flag_awaiting_value,
&mut w,
custom_env,
)?;
if should_return {
return Ok(out);
}
continue;
}
Expand Down Expand Up @@ -818,6 +825,43 @@ impl<'a> ChoiceTarget<'a> {
}
}

fn drain_pending_flag_values(
spec: &Spec,
cmd: &SpecCommand,
errors: &mut Vec<UsageErr>,
flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
word: &mut String,
custom_env: Option<&HashMap<String, String>>,
) -> miette::Result<bool> {
while let Some(flag) = flag_awaiting_value.pop() {
let arg = flag.arg.as_ref().unwrap();
if validate_choices(
spec,
cmd,
errors,
ChoiceTarget::option(&flag),
word,
arg.choices.as_ref(),
custom_env,
)? {
return Ok(true);
}
let value = std::mem::take(word);
if flag.var {
let arr = flags
.entry(flag)
.or_insert_with(|| ParseValue::MultiString(vec![]))
.try_as_multi_string_mut()
.unwrap();
arr.push(value);
} else {
flags.insert(flag, ParseValue::String(value));
}
}
Ok(false)
}

fn choice_error(
target: ChoiceTarget<'_>,
value: &str,
Expand Down Expand Up @@ -1020,6 +1064,22 @@ mod tests {
panic!("expected first parsed value to be ParseValue::String");
}

fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
let flag = parsed
.flags
.keys()
.find(|flag| flag.name == name)
.unwrap_or_else(|| panic!("expected flag {name}"));
let value = parsed
.flags
.get(flag)
.unwrap_or_else(|| panic!("expected value for flag {name}"));
match value {
ParseValue::String(value) => value,
_ => panic!("expected flag {name} to be ParseValue::String"),
}
}

fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
let err = result.expect_err("expected parser error");
assert_eq!(format!("{err}"), expected);
Expand Down Expand Up @@ -2688,4 +2748,72 @@ mod tests {
env3.get("usage_other_args"),
);
}

#[test]
fn test_allow_hyphen_values_consumes_short_flag_collision() {
let spec = r#"
flag "-d --working-dir <DIR>"
flag "-a --args <ARGS>" allow_hyphen_values=#true
"#
.parse::<Spec>()
.unwrap();

let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();

assert_eq!(parsed.flags.len(), 1);
assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
}

#[test]
fn test_allow_hyphen_values_consumes_embedded_long_value() {
let spec = r#"
flag "-d --working-dir <DIR>"
flag "-a --args <ARGS>" allow_hyphen_values=#true
"#
.parse::<Spec>()
.unwrap();

let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();

assert_eq!(parsed.flags.len(), 1);
assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
}

#[test]
fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
let spec = r#"
flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
"#
.parse::<Spec>()
.unwrap();

let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();

let flag = parsed
.flags
.keys()
.find(|flag| flag.name == "args")
.expect("expected args flag");
let value = parsed.flags.get(flag).expect("expected args value");
match value {
ParseValue::MultiString(values) => {
assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
}
_ => panic!("expected MultiString, got {value:?}"),
}
}

#[test]
fn test_hyphen_values_still_default_to_short_flag_parsing() {
let spec = r#"
flag "-d --working-dir <DIR>"
flag "-a --args <ARGS>"
"#
.parse::<Spec>()
.unwrap();

let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();

assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
}
}
24 changes: 24 additions & 0 deletions lib/src/spec/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use crate::{spec::arg::SpecDoubleDashChoices, SpecArg, SpecChoices, SpecCommand,
#[derive(Debug, Default, Clone)]
pub struct SpecFlagBuilder {
inner: SpecFlag,
allow_hyphen_values: bool,
}

impl SpecFlagBuilder {
Expand Down Expand Up @@ -161,9 +162,27 @@ impl SpecFlagBuilder {
self
}

/// Allow this flag's value to start with `-`
pub fn allow_hyphen_values(mut self, allow: bool) -> Self {
self.allow_hyphen_values = allow;
if let Some(arg) = &mut self.inner.arg {
arg.double_dash = if allow {
crate::spec::arg::SpecDoubleDashChoices::Automatic
} else {
crate::spec::arg::SpecDoubleDashChoices::Optional
};
}
self
}

/// Set the argument spec for flags that take values
pub fn arg(mut self, arg: SpecArg) -> Self {
self.inner.arg = Some(arg);
if self.allow_hyphen_values {
if let Some(arg) = &mut self.inner.arg {
arg.double_dash = crate::spec::arg::SpecDoubleDashChoices::Automatic;
}
}
self
}

Expand All @@ -188,6 +207,11 @@ impl SpecFlagBuilder {
/// Build the final SpecFlag
#[must_use]
pub fn build(mut self) -> SpecFlag {
if self.allow_hyphen_values {
if let Some(arg) = &mut self.inner.arg {
arg.double_dash = crate::spec::arg::SpecDoubleDashChoices::Automatic;
}
}
self.inner.usage = self.inner.usage();
if self.inner.name.is_empty() {
// Derive name from long or short flags
Expand Down
Loading