Skip to content

Commit 600edf5

Browse files
jdxclaude
andcommitted
fix(parse): handle variadic ellipsis inside brackets like [args...]
The spec parser was not recognizing `[args...]` or `<args...>` as variadic arguments. The ellipsis check only ran before bracket stripping, so `[args...]` (ending with `]` not `...`) was missed. Add a second ellipsis check after brackets are stripped so both `[args]...` and `[args...]` correctly set var=true. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 35bdd17 commit 600edf5

File tree

4 files changed

+150
-1
lines changed

4 files changed

+150
-1
lines changed

lib/src/parse.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,4 +1746,107 @@ mod tests {
17461746
// Should fail - env var is missing from both custom and process env
17471747
assert!(result.is_err());
17481748
}
1749+
1750+
#[test]
1751+
fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
1752+
let spec: Spec = r#"
1753+
flag "-v --verbose" var=#true
1754+
arg "[database]" default="myapp_dev"
1755+
arg "[args...]"
1756+
"#
1757+
.parse()
1758+
.unwrap();
1759+
let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
1760+
.into_iter()
1761+
.map(String::from)
1762+
.collect();
1763+
let parsed = parse(&spec, &input).unwrap();
1764+
let env = parsed.as_env();
1765+
assert_eq!(env.get("usage_database").unwrap(), "mydb");
1766+
assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
1767+
}
1768+
1769+
#[test]
1770+
fn test_variadic_arg_captures_unknown_flags() {
1771+
let cmd = SpecCommand::builder()
1772+
.name("test")
1773+
.flag(SpecFlag::builder().short('v').long("verbose").build())
1774+
.arg(SpecArg::builder().name("database").required(false).build())
1775+
.arg(
1776+
SpecArg::builder()
1777+
.name("args")
1778+
.required(false)
1779+
.var(true)
1780+
.build(),
1781+
)
1782+
.build();
1783+
let spec = Spec {
1784+
name: "test".to_string(),
1785+
bin: "test".to_string(),
1786+
cmd,
1787+
..Default::default()
1788+
};
1789+
1790+
// Unknown --host flag and its value should be captured by [args...]
1791+
let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
1792+
.into_iter()
1793+
.map(String::from)
1794+
.collect();
1795+
let parsed = parse(&spec, &input).unwrap();
1796+
assert_eq!(parsed.args.len(), 2);
1797+
let args_val = parsed
1798+
.args
1799+
.iter()
1800+
.find(|(a, _)| a.name == "args")
1801+
.unwrap()
1802+
.1;
1803+
match args_val {
1804+
ParseValue::MultiString(v) => {
1805+
assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
1806+
}
1807+
_ => panic!("Expected MultiString, got {:?}", args_val),
1808+
}
1809+
}
1810+
1811+
#[test]
1812+
fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
1813+
let cmd = SpecCommand::builder()
1814+
.name("test")
1815+
.flag(SpecFlag::builder().short('v').long("verbose").build())
1816+
.arg(SpecArg::builder().name("database").required(false).build())
1817+
.arg(
1818+
SpecArg::builder()
1819+
.name("args")
1820+
.required(false)
1821+
.var(true)
1822+
.build(),
1823+
)
1824+
.build();
1825+
let spec = Spec {
1826+
name: "test".to_string(),
1827+
bin: "test".to_string(),
1828+
cmd,
1829+
..Default::default()
1830+
};
1831+
1832+
// With explicit -- separator
1833+
let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
1834+
.into_iter()
1835+
.map(String::from)
1836+
.collect();
1837+
let parsed = parse(&spec, &input).unwrap();
1838+
assert_eq!(parsed.args.len(), 2);
1839+
let args_val = parsed
1840+
.args
1841+
.iter()
1842+
.find(|(a, _)| a.name == "args")
1843+
.unwrap()
1844+
.1;
1845+
match args_val {
1846+
ParseValue::MultiString(v) => {
1847+
assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
1848+
}
1849+
_ => panic!("Expected MultiString, got {:?}", args_val),
1850+
}
1851+
}
17491852
}

lib/src/spec/arg.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,17 @@ impl From<&str> for SpecArg {
256256
}
257257
_ => {}
258258
}
259+
// Also handle ellipsis inside brackets: "[args...]" or "<args...>"
260+
if !arg.var {
261+
if let Some(name) = arg
262+
.name
263+
.strip_suffix("...")
264+
.or_else(|| arg.name.strip_suffix("…"))
265+
{
266+
arg.var = true;
267+
arg.name = name.to_string();
268+
}
269+
}
259270
if let Some(name) = arg.name.strip_prefix("-- ") {
260271
arg.double_dash = SpecDoubleDashChoices::Required;
261272
arg.name = name.to_string();
@@ -425,5 +436,22 @@ arg "<output>" {
425436
assert_eq!(arg.name, "files");
426437
assert!(arg.var);
427438
assert!(!arg.required);
439+
440+
// Ellipsis inside brackets: [args...] and <args...>
441+
let arg: SpecArg = "[args...]".into();
442+
assert_eq!(arg.name, "args");
443+
assert!(arg.var);
444+
assert!(!arg.required);
445+
446+
let arg: SpecArg = "<args...>".into();
447+
assert_eq!(arg.name, "args");
448+
assert!(arg.var);
449+
assert!(arg.required);
450+
451+
// Unicode ellipsis inside brackets
452+
let arg: SpecArg = "[args…]".into();
453+
assert_eq!(arg.name, "args");
454+
assert!(arg.var);
455+
assert!(!arg.required);
428456
}
429457
}

lib/tests/dump.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,5 @@ tests_same! {
2626
choices bash fish zsh
2727
}"#,
2828

29-
double_dash: r#"arg "<-- shell...>""#,
29+
double_dash: r#"arg "<-- shell>…" var=#true"#,
3030
}

lib/tests/parse.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,24 @@ multi_flag:
240240
args="-v --verbose",
241241
expected=r#"{"usage_verbose": "2"}"#,
242242

243+
variadic_arg_captures_unknown_flags:
244+
spec=r#"
245+
flag "-v --verbose" var=#true
246+
arg "[database]" default="myapp_dev"
247+
arg "[args...]"
248+
"#,
249+
args="mydb --host localhost",
250+
expected=r#"{"usage_args": "--host localhost", "usage_database": "mydb"}"#,
251+
252+
variadic_arg_captures_unknown_short_flags:
253+
spec=r#"
254+
flag "-v --verbose" var=#true
255+
arg "[database]" default="myapp_dev"
256+
arg "[args...]"
257+
"#,
258+
args="mydb -x localhost",
259+
expected=r#"{"usage_args": "-x localhost", "usage_database": "mydb"}"#,
260+
243261
//shell_escape_arg:
244262
// spec=r#"
245263
// arg "<vars>" shell_escape=#true

0 commit comments

Comments
 (0)