From 616fcfc3061957adc5ebddb05226ca97ae78f71f Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Thu, 20 Aug 2026 01:27:26 +0000 Subject: [PATCH] feat(parse): preserve trailing delimiters --- PLAN.md | 9 +- argv/src/diagnostic.rs | 2 +- argv/src/lib.rs | 124 +++++++--- argv/src/spec.rs | 6 + clap_usage/src/report.rs | 6 - clap_usage/tests/fidelity_report.rs | 10 + conformance/src/argv.rs | 19 +- conformance/src/tables.rs | 1 + conformance/tests/trailing_delimiter.rs | 57 +++++ corpus/06-double-dash.json | 13 + derive/src/codegen.rs | 113 ++++++--- derive/src/lib.rs | 1 + derive/src/model.rs | 8 +- docs/rust/clap-compatibility.md | 2 +- docs/spec/reference/cmd.md | 14 ++ go/argv/argv.go | 27 ++ go/argv/parser.go | 44 +++- go/conformance/conformance_test.go | 4 +- go/internal/shadow/mise/tables.go | 233 +++++++++--------- go/internal/spec/spec.go | 55 +++-- lib/src/docs/models.rs | 1 + lib/src/go/mod.rs | 12 + ...fault_subcommand_points_into_the_tree.snap | 7 +- .../usage__go__tests__a_whole_cli.snap | 9 +- ...liding_names_get_distinct_identifiers.snap | 5 +- ...n_flags_are_inherited_and_overridable.snap | 5 +- lib/src/go/structs.rs | 9 +- lib/src/parse.rs | 11 +- lib/src/spec/cmd.rs | 19 ++ lib/src/spec/mod.rs | 8 + 30 files changed, 577 insertions(+), 257 deletions(-) create mode 100644 conformance/tests/trailing_delimiter.rs diff --git a/PLAN.md b/PLAN.md index 3aab0277e..7abf42619 100644 --- a/PLAN.md +++ b/PLAN.md @@ -412,10 +412,11 @@ Groups are the opposite case: `Command::get_groups`, `ArgGroup::get_args` and metadata, usage-lib, generated Go, and the clap bridge. The first accepts only negative numeric tokens rather than every dash-word; the second ends a variadic owner without binding the terminator. -- [ ] **Trailing delimiter policy** — `dont_delimit_trailing_values` still has no - portable spelling. It is command-wide in clap and suppresses delimiter splitting - only after the final positional begins, so it needs an explicit command/argument - boundary rather than changing `delimiter` globally. +- [x] **Trailing delimiter policy** — `dont_delimit_trailing_values` is a + command-wide, inherited setting in KDL, the typed derive, usage-lib, + usage-argv, generated Go, and the clap bridge. It suppresses delimiter + splitting only after `--` or once a `double_dash="automatic"` + positional begins; the same argument still splits ordinary values. - [ ] **Fixed arity and distinct value names** — clap can say `num_args(2)` with ` `. `var_min` / `var_max` express the bound, and the clap bridge now preserves it for positionals and non-repeatable value diff --git a/argv/src/diagnostic.rs b/argv/src/diagnostic.rs index b79f6b214..ecd83bdbe 100644 --- a/argv/src/diagnostic.rs +++ b/argv/src/diagnostic.rs @@ -339,7 +339,7 @@ fn value_bound_to( let mut last = None; while let Some(event) = parser.next_event() { let value = match event { - Ok(crate::Event::Arg { arg, value }) if arg.name == name => value, + Ok(crate::Event::Arg { arg, value, .. }) if arg.name == name => value, Ok(crate::Event::Flag { flag, value: Some(value), diff --git a/argv/src/lib.rs b/argv/src/lib.rs index e0ff502a2..3e17059fe 100644 --- a/argv/src/lib.rs +++ b/argv/src/lib.rs @@ -189,6 +189,9 @@ pub struct Command<'a> { /// bound values: an environment variable or default may fill a field, but neither means /// the user supplied an argument to this invocation. pub arg_required_else_help: bool, + /// Disable delimiter splitting for positional values after `--` or on an + /// automatic trailing argument. Inherited by subcommands. + pub dont_delimit_trailing_values: bool, /// What an unrecognized flag-like token means here, or `None` to keep whatever the /// enclosing command said. See [`UnknownFlags`]. /// @@ -231,6 +234,7 @@ impl Command<'_> { default_subcommand: ::core::option::Option::None, external_subcommand: false, arg_required_else_help: false, + dont_delimit_trailing_values: false, unknown_flags: ::core::option::Option::None, version: false, key: 0, @@ -474,7 +478,12 @@ pub enum Event<'t, 'a, 'v> { }, /// A word was bound to a positional argument. A variadic argument produces /// one event per value. - Arg { arg: &'t Arg<'t>, value: &'v [u8] }, + Arg { + arg: &'t Arg<'t>, + value: &'v [u8], + /// Whether this value should be split by the argument's declared delimiter. + delimit: bool, + }, /// An unmatched word was forwarded as an external command: the name, then /// every remaining token, including flags. External { values: &'a [&'v OsStr] }, @@ -1022,6 +1031,8 @@ pub struct Parser<'t, 'a, 'v> { /// nothing keeps what the enclosing one said, and walking back up the ancestors on /// every unrecognized token would pay for the inheritance at the wrong moment. unknown_flags: UnknownFlags, + /// Effective inherited trailing-delimiter policy. + dont_delimit_trailing_values: bool, /// The chain above `cmd`, used to find inherited global flags. Fixed size so /// that nothing is allocated. ancestors: [Option<&'t Command<'t>>; MAX_DEPTH], @@ -1088,6 +1099,7 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { // Nothing above the root to inherit from, so the default stands. ::core::option::Option::None => UnknownFlags::Value, }, + dont_delimit_trailing_values: root.dont_delimit_trailing_values, ancestors: [None; MAX_DEPTH], depth: 0, bundle: &[], @@ -1611,6 +1623,8 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { self.arg_filled = true; // An `automatic` argument stops flag interpretation from here on, as // though the caller had typed the separator themselves. + let trailing_value = self.separator_seen || arg.double_dash == DoubleDash::Automatic; + let delimit = !(self.dont_delimit_trailing_values && trailing_value); if arg.double_dash == DoubleDash::Automatic { self.flags_stopped = true; } @@ -1618,7 +1632,7 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { // bound, at which point the words after it belong to whatever comes next. That is // what makes `[a]… [b]` expressible at all. if arg.var { - self.arg_taken += values_in(token, arg.delimiter); + self.arg_taken += values_in(token, delimit.then_some(arg.delimiter).flatten()); // Before advancing, which resets the count: as with a variadic flag, reaching // the bound and passing it are the same event once a word can carry several // values, and only the second is a mistake. @@ -1635,7 +1649,11 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { } else { self.advance_arg(); } - Ok(Event::Arg { arg, value: token }) + Ok(Event::Arg { + arg, + value: token, + delimit, + }) } fn descend(&mut self, sub: &'t Command<'t>) -> Result<(), Error<'t, 'v>> { @@ -1650,6 +1668,7 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { if let ::core::option::Option::Some(mode) = sub.unknown_flags { self.unknown_flags = mode; } + self.dont_delimit_trailing_values |= sub.dont_delimit_trailing_values; // Where this command's own words start, which is what lets a completion hand a callback // the half-parsed struct of the command it was declared on rather than of the root. self.cmd_start = self.pos; @@ -2077,7 +2096,8 @@ mod tests { }, Event::Arg { arg: &FILE, - value: b"input" + value: b"input", + delimit: true, } ] ); @@ -2091,7 +2111,8 @@ mod tests { parse(&ROOT, &a).unwrap(), vec![Event::Arg { arg: &FILE, - value: b"--forc" + value: b"--forc", + delimit: true, }] ); @@ -2113,11 +2134,13 @@ mod tests { vec![ Event::Arg { arg: &FILE, - value: b"--wat" + value: b"--wat", + delimit: true, }, Event::Arg { arg: &REST, - value: b"keep" + value: b"keep", + delimit: true, }, ] ); @@ -2187,7 +2210,8 @@ mod tests { parse(&ROOT, &a).unwrap(), vec![Event::Arg { arg: &FILE, - value: b"-" + value: b"-", + delimit: true, }] ); } @@ -2200,15 +2224,18 @@ mod tests { vec![ Event::Arg { arg: &FILE, - value: b"one" + value: b"one", + delimit: true, }, Event::Arg { arg: &REST, - value: b"two" + value: b"two", + delimit: true, }, Event::Arg { arg: &REST, - value: b"three" + value: b"three", + delimit: true, }, ] ); @@ -2234,11 +2261,13 @@ mod tests { vec![ Event::Arg { arg: &FILE, - value: b"other" + value: b"other", + delimit: true, }, Event::Arg { arg: &REST, - value: b"install" + value: b"install", + delimit: true, }, ] ); @@ -2256,7 +2285,8 @@ mod tests { Event::Command(&RUN), Event::Arg { arg: &RUN_TASK, - value: b"build" + value: b"build", + delimit: true, }, ] ); @@ -2328,11 +2358,13 @@ mod tests { }, Event::Arg { arg: &SHARED_WHAT, - value: b"one" + value: b"one", + delimit: true, }, Event::Arg { arg: &REST, - value: b"two" + value: b"two", + delimit: true, }, Event::Flag { flag: &FORCE, @@ -2356,7 +2388,8 @@ mod tests { parse(&DEFAULTING, &a).unwrap(), vec![Event::Arg { arg: &TASK, - value: token.as_bytes() + value: token.as_bytes(), + delimit: true, }], "{token} should bind where it was typed, not in the default subcommand" ); @@ -2436,7 +2469,8 @@ mod tests { Event::Command(&RUN), Event::Arg { arg: &RUN_TASK, - value: b"build" + value: b"build", + delimit: true, } ] ); @@ -2452,7 +2486,8 @@ mod tests { parser.next_event(), Some(Ok(Event::Arg { arg: &RUN_TASK, - value: b"build" + value: b"build", + delimit: true, })) ); } @@ -2572,7 +2607,8 @@ mod tests { Event::Command(&DEEP), Event::Arg { arg: &RUN_TASK, - value: b"zzz" + value: b"zzz", + delimit: true, }, ] ); @@ -2594,7 +2630,8 @@ mod tests { Event::Command(&RUN), Event::Arg { arg: &RUN_TASK, - value: b"build" + value: b"build", + delimit: true, }, ] ); @@ -2609,7 +2646,8 @@ mod tests { parse(&DEFAULTING, &a).unwrap(), vec![Event::Arg { arg: &TASK, - value: b"build" + value: b"build", + delimit: true, }] ); } @@ -2649,11 +2687,13 @@ mod tests { vec![ Event::Arg { arg: &FILE, - value: b"--force" + value: b"--force", + delimit: true, }, Event::Arg { arg: &REST, - value: b"-x" + value: b"-x", + delimit: true, }, ] ); @@ -2719,7 +2759,8 @@ mod tests { }, Event::Arg { arg: &REST, - value: b"-x" + value: b"-x", + delimit: true, }, ] ); @@ -2896,7 +2937,8 @@ mod tests { }, Event::Arg { arg: &REST, - value: b"80" + value: b"80", + delimit: true, }, ] ); @@ -3000,6 +3042,7 @@ mod tests { Event::Arg { arg: &AFTER, value: b"tail", + delimit: true, }, ] ); @@ -3011,10 +3054,12 @@ mod tests { Event::Arg { arg: &ITEMS, value: b"a", + delimit: true, }, Event::Arg { arg: &AFTER, value: b"tail", + delimit: true, }, ] ); @@ -3035,7 +3080,8 @@ mod tests { }, Event::Arg { arg: &FILE, - value: b"keep-me" + value: b"keep-me", + delimit: true, }, ] ); @@ -3094,11 +3140,13 @@ mod tests { vec![ Event::Arg { arg: &ARGS, - value: b"--help" + value: b"--help", + delimit: true, }, Event::Arg { arg: &ARGS, - value: b"-h" + value: b"-h", + delimit: true, }, ] ); @@ -3123,11 +3171,13 @@ mod tests { vec![ Event::Arg { arg: &AUTO_ARGS, - value: b"node" + value: b"node", + delimit: true, }, Event::Arg { arg: &AUTO_ARGS, - value: b"--help" + value: b"--help", + delimit: true, }, ] ); @@ -3164,7 +3214,8 @@ mod tests { parse(&EXEC, &a).unwrap(), vec![Event::Arg { arg: &CMD, - value: b"ls" + value: b"ls", + delimit: true, }] ); @@ -3229,11 +3280,13 @@ mod tests { }, Event::Arg { arg: &FILES, - value: b"one" + value: b"one", + delimit: true, }, Event::Arg { arg: &FILES, - value: b"--force" + value: b"--force", + delimit: true, }, ] ); @@ -3271,7 +3324,8 @@ mod tests { parse(&ROOT, &a).unwrap(), vec![Event::Arg { arg: &FILE, - value: b"-fz" + value: b"-fz", + delimit: true, }] ); } diff --git a/argv/src/spec.rs b/argv/src/spec.rs index 4b8c5a852..d150312a4 100644 --- a/argv/src/spec.rs +++ b/argv/src/spec.rs @@ -1085,6 +1085,9 @@ impl Spec<'_> { if self.root.cmd.arg_required_else_help { writeln!(out, "arg_required_else_help #true")?; } + if self.root.cmd.dont_delimit_trailing_values { + writeln!(out, "dont_delimit_trailing_values #true")?; + } // A `complete` block for every completer this CLI declares, naming the command that // asks the binary itself. Written rather than declared, so there is one place a // completer is said to exist: the Rust function. Everything that reads a spec — the @@ -1328,6 +1331,9 @@ fn write_command<'a>( if meta.cmd.arg_required_else_help { out.push_str(" arg_required_else_help=#true"); } + if meta.cmd.dont_delimit_trailing_values { + out.push_str(" dont_delimit_trailing_values=#true"); + } out.push_str(" {\n"); let inner = depth + 1; diff --git a/clap_usage/src/report.rs b/clap_usage/src/report.rs index 95f4315ff..ae6a6b738 100644 --- a/clap_usage/src/report.rs +++ b/clap_usage/src/report.rs @@ -13,7 +13,6 @@ pub enum FidelityFeature { ValueArity, DistinctValueNames, GranularHide, - DontDelimitTrailingValues, AllowMissingPositional, ArgsConflictWithSubcommands, ArgsOverrideSelf, @@ -84,11 +83,6 @@ fn visit(cmd: &Command, ancestors: &[String], losses: &mut BTreeSet Outcome { } (Some(Multi::Var), Some(v)) => { match flags.entry(name).or_insert(Value::Strs(vec![])) { - Value::Strs(list) => list.push(string(v)), + Value::Strs(list) => list.extend(split_value(v, flag.delimiter, true)), _ => unreachable!("var flags only ever hold strings"), } } @@ -143,11 +143,17 @@ pub fn run(vector: &Vector) -> Outcome { } } } - Ok(Event::Arg { arg, value }) => { + Ok(Event::Arg { + arg, + value, + delimit, + }) => { let name = arg.name.to_string(); if arg.var { match args.entry(name).or_insert(Value::Strs(vec![])) { - Value::Strs(list) => list.push(string(value)), + Value::Strs(list) => { + list.extend(split_value(value, arg.delimiter, delimit)) + } _ => unreachable!("var args only ever hold strings"), } } else { @@ -177,6 +183,13 @@ pub fn run(vector: &Vector) -> Outcome { }) } +fn split_value(value: &[u8], delimiter: Option, delimit: bool) -> Vec { + match delimiter.filter(|_| delimit) { + Some(delimiter) => value.split(|byte| *byte == delimiter).map(string).collect(), + None => vec![string(value)], + } +} + fn string(value: &[u8]) -> String { usage_argv::as_str(value) .expect("corpus values are UTF-8") diff --git a/conformance/src/tables.rs b/conformance/src/tables.rs index 6467b7f5f..a488e0b12 100644 --- a/conformance/src/tables.rs +++ b/conformance/src/tables.rs @@ -110,6 +110,7 @@ pub fn build( unknown_flags, external_subcommand: cmd.external_subcommand, arg_required_else_help: cmd.arg_required_else_help, + dont_delimit_trailing_values: cmd.dont_delimit_trailing_values, key: 0, })); diff --git a/conformance/tests/trailing_delimiter.rs b/conformance/tests/trailing_delimiter.rs new file mode 100644 index 000000000..48ba2435f --- /dev/null +++ b/conformance/tests/trailing_delimiter.rs @@ -0,0 +1,57 @@ +use std::ffi::OsStr; + +use usage::Spec; +use usage_derive::{Args, Cli, Subcommands}; + +fn argv(tokens: [&str; N]) -> [&OsStr; N] { + tokens.map(OsStr::new) +} + +#[derive(Cli)] +#[usage(bin = "ex", dont_delimit_trailing_values)] +struct Mixed { + #[usage(arg, delimiter = ',')] + values: Vec, +} + +#[derive(Cli)] +#[usage(bin = "ex", dont_delimit_trailing_values)] +struct Nested { + #[usage(subcommand)] + command: NestedCommands, +} + +#[derive(Subcommands)] +enum NestedCommands { + Run(Run), +} + +#[derive(Args)] +struct Run { + #[usage(arg, delimiter = ',')] + values: Vec, +} + +#[test] +fn only_values_past_the_trailing_boundary_keep_the_delimiter() { + let parsed = Mixed::parse_from(&argv(["a,b", "--", "c,d"])).expect("should parse"); + assert_eq!(parsed.values, ["a", "b", "c,d"]); +} + +#[test] +fn the_command_policy_is_inherited_by_subcommands() { + let parsed = Nested::parse_from(&argv(["run", "--", "a,b"])).expect("should parse"); + let NestedCommands::Run(run) = parsed.command; + assert_eq!(run.values, ["a,b"]); +} + +#[test] +fn the_policy_round_trips() { + let emitted = Mixed::to_kdl(); + assert!( + emitted.contains("dont_delimit_trailing_values #true"), + "{emitted}" + ); + let spec: Spec = emitted.parse().expect("valid emitted spec"); + assert!(spec.cmd.dont_delimit_trailing_values); +} diff --git a/corpus/06-double-dash.json b/corpus/06-double-dash.json index 1070fe5d9..4c872b2a5 100644 --- a/corpus/06-double-dash.json +++ b/corpus/06-double-dash.json @@ -125,6 +125,19 @@ } } }, + { + "id": "dd-trailing-values-can-keep-delimiters", + "doc": "`dont_delimit_trailing_values` splits ordinary positional values but preserves the same delimiter after `--`, matching clap's command-wide trailing policy.", + "spec": "name \"ex\"\nbin \"ex\"\ndont_delimit_trailing_values #true\narg \"...\" delimiter=\",\"\n", + "argv": ["a,b", "--", "c,d"], + "expect": { + "ok": { + "args": { + "values": ["a", "b", "c,d"] + } + } + } + }, { "id": "dd-automatic-behaves-as-if-separated", "doc": "With `double_dash=\"automatic\"`, the first value an argument takes stops flag interpretation for everything after it, so a wrapper can forward flags without the caller typing `--`.", diff --git a/derive/src/codegen.rs b/derive/src/codegen.rs index 24c09597b..d3234d036 100644 --- a/derive/src/codegen.rs +++ b/derive/src/codegen.rs @@ -139,6 +139,7 @@ pub fn emit(cli: &Cli) -> TokenStream { let multicall = cli.multicall; let no_binary_name = cli.no_binary_name; let arg_required_else_help = cli.arg_required_else_help; + let dont_delimit_trailing_values = cli.dont_delimit_trailing_values; let usage = option_str(cli.usage.as_deref()); let restart_token = option_str(cli.restart_token.as_deref()); let mount = option_str(cli.mount.as_deref()); @@ -420,6 +421,7 @@ pub fn emit(cli: &Cli) -> TokenStream { version: #has_version, unknown_flags: #unknown_flags, arg_required_else_help: #arg_required_else_help, + dont_delimit_trailing_values: #dont_delimit_trailing_values, name: #name, key: #root_key, flags: #flag_table_ref, @@ -1699,7 +1701,19 @@ fn flag_arm(cli: &Cli, i: usize, field: &Field) -> TokenStream { partial.#ident = ::std::option::Option::Some(__usage_value_text(value)); }, Shape::Required => quote!(partial.#ident = __usage_value_text(value);), - Shape::Many => quote!(partial.#ident.push(__usage_value_text(value));), + Shape::Many => match field.delimiter { + Some(delimiter) => { + let byte = u8::try_from(u32::from(delimiter)) + .expect("the model rejects non-ASCII delimiters"); + quote! { + let value = __usage_value_text(value); + for part in value.split(|b| *b == #byte) { + partial.#ident.push(part.to_vec()); + } + } + } + None => quote!(partial.#ident.push(__usage_value_text(value));), + }, }; let table = format_ident!("FLAG_{i}"); quote! { @@ -1865,7 +1879,22 @@ fn arg_arm(i: usize, field: &Field) -> TokenStream { let ident = &field.ident; let given = format_ident!("__given_{}", ident); let body = match field.shape { - Shape::Many => quote!(partial.#ident.push(__usage_text(value));), + Shape::Many => match field.delimiter { + Some(delimiter) => { + let byte = u8::try_from(u32::from(delimiter)) + .expect("the model rejects non-ASCII delimiters"); + quote! { + if delimit { + for part in value.split(|b| *b == #byte) { + partial.#ident.push(__usage_text(part)); + } + } else { + partial.#ident.push(__usage_text(value)); + } + } + } + None => quote!(partial.#ident.push(__usage_text(value));), + }, Shape::Optional => quote! { partial.#ident = ::std::option::Option::Some(__usage_text(value)); }, @@ -2962,9 +2991,23 @@ fn assign_literal(field: &Field, first: &str, all: &[String]) -> TokenStream { Shape::Count => quote!(partial.#ident = ::std::default::Default::default();), Shape::Many => { let defaults = all; - quote! { - #cleared - #(partial.#ident.push(#defaults.as_bytes().to_vec());)* + match field.delimiter { + Some(delimiter) => { + let byte = u8::try_from(u32::from(delimiter)) + .expect("the model rejects non-ASCII delimiters"); + quote! { + #cleared + #( + for part in #defaults.as_bytes().split(|b| *b == #byte) { + partial.#ident.push(part.to_vec()); + } + )* + } + } + None => quote! { + #cleared + #(partial.#ident.push(#defaults.as_bytes().to_vec());)* + }, } } } @@ -3092,9 +3135,13 @@ fn apply_fn(cli: &Cli) -> TokenStream { _ => false, } } - Event::Arg { arg, value } => { - let value = *value; - let _ = value; + Event::Arg { + arg, + value, + delimit, + } => { + let (value, delimit) = (*value, *delimit); + let _ = (value, delimit); match arg.key { #(#arg_arms)* _ => false, @@ -3325,6 +3372,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream { }); let unknown_flags = unknown_flags_tokens(cli); let arg_required_else_help = cli.arg_required_else_help; + let dont_delimit_trailing_values = cli.dont_delimit_trailing_values; let before_help = option_expr(cli.before_help.as_ref()); let before_long_help = option_expr(cli.before_long_help.as_ref()); let after_help = option_expr(cli.after_help.as_ref()); @@ -3435,6 +3483,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream { key: #command_key, unknown_flags: #unknown_flags, arg_required_else_help: #arg_required_else_help, + dont_delimit_trailing_values: #dont_delimit_trailing_values, flags: #flag_table_ref, args: #arg_table_ref, #sub_commands @@ -4493,9 +4542,21 @@ fn env_fallbacks(cli: &Cli) -> TokenStream { Shape::Required => quote!(partial.#ident = value.into_bytes();), // Cleared first, so the environment *replaces* a declared default instead of // adding to it — which is what every other shape does by assigning. - Shape::Many => quote! { - partial.#ident.clear(); - partial.#ident.push(value.into_bytes()); + Shape::Many => match f.delimiter { + Some(delimiter) => { + let byte = u8::try_from(u32::from(delimiter)) + .expect("the model rejects non-ASCII delimiters"); + quote! { + partial.#ident.clear(); + for part in value.as_bytes().split(|b| *b == #byte) { + partial.#ident.push(part.to_vec()); + } + } + } + None => quote! { + partial.#ident.clear(); + partial.#ident.push(value.into_bytes()); + }, }, Shape::Bool => quote! { partial.#ident = !matches!( @@ -4641,32 +4702,6 @@ fn post_binding(cli: &Cli) -> TokenStream { let declared_defaults = declared_defaults(cli); let env_fallbacks = env_fallbacks(cli); - // One word becomes several values, before anything judges them. Run after the - // environment fills what argv left out — a `TAGS=a,b` is one word too — and before - // every check, so `choices` sees each value rather than the word that carried them, - // and `var_min`/`var_max` count what the user meant rather than what they typed. - // - // On the cold path deliberately: the binder collects words and knows nothing about - // what a value *is*, which is what keeps it free of everything in this function. - let delimiter_splits = cli.fields.iter().filter_map(|f| { - let delimiter = f.delimiter?; - let ident = &f.ident; - // A one-byte separator, which every delimiter anyone writes is. Refused at the - // attribute otherwise, rather than splitting on half a character. - let byte = u8::try_from(u32::from(delimiter)).ok()?; - Some(quote! { - if !partial.#ident.is_empty() { - let mut __usage_split: ::std::vec::Vec<::std::vec::Vec> = - ::std::vec::Vec::with_capacity(partial.#ident.len()); - for value in &partial.#ident { - for part in value.split(|b| *b == #byte) { - __usage_split.push(part.to_vec()); - } - } - partial.#ident = __usage_split; - } - }) - }); let required_checks = cli.fields.iter().filter_map(|f| { // A `String` has nowhere to put "absent", so the type is the declaration; a collection @@ -5309,10 +5344,6 @@ fn post_binding(cli: &Cli) -> TokenStream { // env, and so the environment still overrides an unconditional default. #env_fallbacks #declared_defaults - // Splitting before any check that counts or judges values, so `choices` sees a - // value rather than the word that carried several, and the bounds count what the - // user meant. After the environment, since `TAGS=a,b` is one word too. - #(#delimiter_splits)* let __usage_exclusive_present = #exclusive_present; #(#duplicate_checks)* // Before required-ness: "you gave two flags that cannot go together" is the diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 82d43fc1d..11f8f30db 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -203,6 +203,7 @@ //! `verbatim_doc_comment` — preserve doc-comment line breaks and whitespace — //! `default_subcommand`, `multicall` — argv[0]'s basename selects a subcommand — //! `arg_required_else_help` — a selected command with no argv of its own shows short help — +//! `dont_delimit_trailing_values` — preserve delimiters after the trailing boundary — //! `min_usage_version` — the oldest `usage` that can read the emitted //! spec, declared rather than worked out — `effect` — what running this command does to the world, on an `Args` //! rather than on the root, which does nothing itself — `completion`, which adds the hidden command a generated shell diff --git a/derive/src/model.rs b/derive/src/model.rs index 9c2775dbe..c8cd2e31f 100644 --- a/derive/src/model.rs +++ b/derive/src/model.rs @@ -108,6 +108,8 @@ pub struct Cli { pub no_binary_name: bool, /// Show help when no argv token follows this command's name. pub arg_required_else_help: bool, + /// Disable delimiter splitting after `--` and for automatic trailing args. + pub dont_delimit_trailing_values: bool, /// Declared descriptions, for the case a doc comment cannot express: a long form that does /// not contain the short one. pub about_attr: Option, @@ -532,6 +534,7 @@ impl Cli { multicall: false, no_binary_name: false, arg_required_else_help: false, + dont_delimit_trailing_values: false, about_attr: None, long_about_attr: None, before_help: None, @@ -670,6 +673,9 @@ impl Cli { "multicall" => cli.multicall = flag_value(&meta)?, "no_binary_name" => cli.no_binary_name = flag_value(&meta)?, "arg_required_else_help" => cli.arg_required_else_help = flag_value(&meta)?, + "dont_delimit_trailing_values" => { + cli.dont_delimit_trailing_values = flag_value(&meta)? + } "restart_token" => cli.restart_token = Some(string_value(&meta)?), "mount" => cli.mount = Some(string_value(&meta)?), "group" => cli.groups.push(group_decl(&meta)?), @@ -681,7 +687,7 @@ impl Cli { format!( "unknown option `{other}` on a struct; usage::Cli takes \ `name`, `name_spec`, `bin`, `bin_spec`, `version`, `version_spec`, `usage`, `verbatim_doc_comment`, `unknown_flags`, \ - `default_subcommand`, `multicall`, `no_binary_name`, `arg_required_else_help`, \ + `default_subcommand`, `multicall`, `no_binary_name`, `arg_required_else_help`, `dont_delimit_trailing_values`, \ `next_help_heading`, `restart_token`, `mount` and \ `group` here, and the description comes from the doc \ comment" diff --git a/docs/rust/clap-compatibility.md b/docs/rust/clap-compatibility.md index 01e864fc2..df30a9260 100644 --- a/docs/rust/clap-compatibility.md +++ b/docs/rust/clap-compatibility.md @@ -71,7 +71,7 @@ the Rust declaration, not only from generated KDL, wherever the bridge column sa | `require_equals` | yes | yes | yes | yes | yes | yes | Detached values are refused. | | `value_terminator` | yes | yes | yes | yes | yes | yes | Ends a variadic value owner without binding the terminator token. | | `trailing_var_arg` / `last` | yes | yes | yes | yes | yes | lossy | `double_dash` carries automatic/required/optional; clap shadow generation still drops automatic mode. | -| `dont_delimit_trailing_values` | no | no | no | no | no | no | No portable spelling yet. | +| `dont_delimit_trailing_values` | yes | yes | yes | yes | yes | yes | Preserves delimiters after `--` and on automatic trailing positionals while ordinary values still split. | | possible-values parser | yes | yes | yes | yes | yes | yes | Use `ValueEnum` or `choices`. | | arbitrary `value_parser` callbacks | usage-only | yes | lossy | yes | yes | no | `FromStr` handles typed conversion and portable `validate` expressions handle declarative rules; Rust callbacks cannot enter KDL. | | `ValueHint::{FilePath,DirPath}` | yes | yes | yes | yes | yes | lossy | Shell-native path completion is supported directly; `clap_usage` does not yet lower hints into completion nodes. | diff --git a/docs/spec/reference/cmd.md b/docs/spec/reference/cmd.md index 2ac0fe7a1..9194ff9dd 100644 --- a/docs/spec/reference/cmd.md +++ b/docs/spec/reference/cmd.md @@ -169,6 +169,20 @@ The policy observes argv belonging to the selected command. A global flag before subcommand does not count as that subcommand's argument, and values supplied later by an environment variable or default do not suppress help. +### Preserve delimiters in trailing values + +`dont_delimit_trailing_values` keeps a positional token whole after `--`, or once a +`double_dash="automatic"` positional begins. The same argument still applies its +`delimiter` before that boundary: + +```kdl +dont_delimit_trailing_values #true +arg "..." delimiter="," +``` + +`ex a,b -- c,d` binds `a`, `b`, and `c,d`. The policy is inherited by +subcommands, matching clap's command-wide setting. + ### Global flags and mounted commands A mounted command describes a different program, so the flags of the commands it is mounted under diff --git a/go/argv/argv.go b/go/argv/argv.go index ea53a95ce..04262c808 100644 --- a/go/argv/argv.go +++ b/go/argv/argv.go @@ -70,6 +70,9 @@ type Command struct { ExternalSubcommand bool // ArgRequiredElseHelp shows this command's help when no argv token follows its name. ArgRequiredElseHelp bool + // DontDelimitTrailingValues disables delimiter splitting after -- and for + // automatic trailing arguments. It is inherited by subcommands. + DontDelimitTrailingValues bool // UnknownFlags is what an unrecognized flag-like token means here. Already // resolved: inheritance is a question for whoever builds the tables. UnknownFlags UnknownFlags @@ -145,6 +148,8 @@ type Flag struct { AllowNegativeNumbers bool // ValueTerminator ends one variadic occurrence without becoming a value. ValueTerminator string + // Delimiter splits one token into several values. Zero disables splitting. + Delimiter byte // RequireEquals is whether the value must be attached with `=`. // // `--flag=value` is accepted and `--flag value` is not, which is clap's @@ -185,6 +190,8 @@ type Arg struct { AllowNegativeNumbers bool // ValueTerminator ends this variadic positional without becoming a value. ValueTerminator string + // Delimiter splits one token into several values. Zero disables splitting. + Delimiter byte // DoubleDash is this argument's relationship to the -- separator. DoubleDash DoubleDash } @@ -266,12 +273,32 @@ type Event struct { HasValue bool // Negated is true when a flag was set through its Negate form. Negated bool + // Delimit says whether a positional value should honor Arg.Delimiter. + Delimit bool // Values is the remaining argv when Kind is KindExternal: the unmatched name // first, then every token after it. Shares memory with the argv the parser // was given. Values []string } +// SplitValue applies a generated table's ASCII delimiter to one bound value. +// Delimit is false for trailing positional values protected by +// DontDelimitTrailingValues. +func SplitValue(value string, delimiter byte, delimit bool) []string { + if delimiter == 0 || !delimit { + return []string{value} + } + out := make([]string, 0, valuesIn(value, delimiter)) + start := 0 + for i := 0; i < len(value); i++ { + if value[i] == delimiter { + out = append(out, value[start:i]) + start = i + 1 + } + } + return append(out, value[start:]) +} + // Code is a class of binding failure. // // The grammar specifies the class, not the wording: diagnostics are a diff --git a/go/argv/parser.go b/go/argv/parser.go index 38e780365..66e960687 100644 --- a/go/argv/parser.go +++ b/go/argv/parser.go @@ -35,6 +35,8 @@ type Parser struct { pos int // cmd is the command currently in scope. cmd *Command + // Effective inherited trailing-delimiter policy. + dontDelimitTrailingValues bool // ancestors is the chain above cmd, used to find inherited global flags. // Fixed size so that nothing is allocated. ancestors [MaxDepth]*Command @@ -87,7 +89,11 @@ type Parser struct { // New begins parsing argv against root. argv excludes the program name. func New(root *Command, argv []string) Parser { - return Parser{argv: argv, cmd: root} + return Parser{ + argv: argv, + cmd: root, + dontDelimitTrailingValues: root.DontDelimitTrailingValues, + } } // Next reads the next event, reporting false when argv is exhausted or the parse @@ -204,7 +210,7 @@ func (p *Parser) step() bool { } if (!isFlagLike(next) || (flag.AllowNegativeNumbers && isNegativeNumber(next))) && next != "--" { p.pos++ - p.collected++ + p.collected += valuesIn(next, flag.Delimiter) // Same rule as a positional: a bounded occurrence takes that many and // leaves the rest to whatever follows. if flag.VarMax != 0 && p.collected >= flag.VarMax { @@ -326,7 +332,7 @@ func (p *Parser) longFlag(token string) bool { } } if flag.Variadic { - p.startCollecting(flag) + p.startCollecting(flag, value) } return p.emit(Event{Kind: KindFlag, Flag: flag, Value: value, HasValue: hasValue}) } @@ -412,7 +418,7 @@ func (p *Parser) shortFlag() bool { value = rest } if flag.Variadic { - p.startCollecting(flag) + p.startCollecting(flag, value) } return p.emit(Event{Kind: KindFlag, Flag: flag, Value: value, HasValue: true}) } @@ -547,6 +553,8 @@ func (p *Parser) word(token string) bool { } p.argFilled = true + trailingValue := p.separatorSeen || arg.DoubleDash == DoubleDashAutomatic + delimit := !(p.dontDelimitTrailingValues && trailingValue) // An automatic argument stops flag interpretation from here on, as though the // caller had typed the separator themselves. if arg.DoubleDash == DoubleDashAutomatic { @@ -556,14 +564,20 @@ func (p *Parser) word(token string) bool { // its bound, at which point the words after it belong to whatever comes next. // That is what makes `[a]… [b]` expressible at all. if arg.Var { - p.argTaken++ + delimiter := arg.Delimiter + if !delimit { + delimiter = 0 + } + p.argTaken += valuesIn(token, delimiter) if arg.VarMax != 0 && p.argTaken >= arg.VarMax { p.advanceArg() } } else { p.advanceArg() } - return p.emit(Event{Kind: KindArg, Arg: arg, Value: token, HasValue: true}) + return p.emit(Event{ + Kind: KindArg, Arg: arg, Value: token, HasValue: true, Delimit: delimit, + }) } func (p *Parser) descend(sub *Command) bool { @@ -574,6 +588,7 @@ func (p *Parser) descend(sub *Command) bool { p.starts[p.depth] = p.cmdStart p.depth++ p.cmd = sub + p.dontDelimitTrailingValues = p.dontDelimitTrailingValues || sub.DontDelimitTrailingValues // Where this command's own words start, which is what lets a completion hand a // callback the half-parsed struct of the command it was declared on rather than // of the root. @@ -594,8 +609,8 @@ func (p *Parser) advanceArg() { // // The value it was given on the same token counts, which is why this starts at // one: `--include a b` with VarMax 2 takes a and b, not three words. -func (p *Parser) startCollecting(flag *Flag) { - p.collected = 1 +func (p *Parser) startCollecting(flag *Flag, first string) { + p.collected = valuesIn(first, flag.Delimiter) if flag.VarMax != 0 && flag.VarMax <= 1 { p.collecting = nil } else { @@ -603,6 +618,19 @@ func (p *Parser) startCollecting(flag *Flag) { } } +func valuesIn(value string, delimiter byte) uint32 { + count := uint32(1) + if delimiter == 0 { + return count + } + for i := 0; i < len(value); i++ { + if value[i] == delimiter { + count++ + } + } + return count +} + func (p *Parser) nextArg() *Arg { if p.argPos < len(p.cmd.Args) { return p.cmd.Args[p.argPos] diff --git a/go/conformance/conformance_test.go b/go/conformance/conformance_test.go index d216cb5d0..8af8409f0 100644 --- a/go/conformance/conformance_test.go +++ b/go/conformance/conformance_test.go @@ -226,14 +226,14 @@ func run(s *spec.Spec, args []string, argv0 *string, env map[string]string) (*Pa b.negated = ev.Negated b.at = seen if ev.HasValue { - b.values = append(b.values, ev.Value) + b.values = append(b.values, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case argv.KindArg: seen++ b := entry(ev.Arg.Key) b.occurrences++ b.at = seen - b.values = append(b.values, ev.Value) + b.values = append(b.values, argv.SplitValue(ev.Value, ev.Arg.Delimiter, ev.Delimit)...) case argv.KindExternal: external = append(external, ev.Values...) } diff --git a/go/internal/shadow/mise/tables.go b/go/internal/shadow/mise/tables.go index d5c46a939..51600f853 100644 --- a/go/internal/shadow/mise/tables.go +++ b/go/internal/shadow/mise/tables.go @@ -8659,7 +8659,7 @@ func Parse(args []string) (*Cli, error) { case argv.KindFlag: seen[ev.Flag.Key]++ if ev.HasValue { - given[ev.Flag.Key] = append(given[ev.Flag.Key], ev.Value) + given[ev.Flag.Key] = append(given[ev.Flag.Key], argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } else if given[ev.Flag.Key] == nil { // Given without a value is still given, and nil would read as // absent when the fallbacks are applied. @@ -8672,7 +8672,7 @@ func Parse(args []string) (*Cli, error) { out.Cd = ev.Value case FlagEnv: if ev.HasValue { - out.Env = append(out.Env, ev.Value) + out.Env = append(out.Env, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagForce: out.Force = !ev.Negated @@ -8682,7 +8682,7 @@ func Parse(args []string) (*Cli, error) { out.DryRun = !ev.Negated case FlagProfile: if ev.HasValue { - out.Profile = append(out.Profile, ev.Value) + out.Profile = append(out.Profile, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagQuiet: out.Quiet = !ev.Negated @@ -8690,7 +8690,7 @@ func Parse(args []string) (*Cli, error) { out.Shell = ev.Value case FlagTool: if ev.HasValue { - out.Tool = append(out.Tool, ev.Value) + out.Tool = append(out.Tool, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagVerbose: out.Verbose++ @@ -8750,13 +8750,13 @@ func Parse(args []string) (*Cli, error) { cmdBootstrapV.ForceDotfiles = !ev.Negated case FlagBootstrapOnly: if ev.HasValue { - cmdBootstrapV.Only = append(cmdBootstrapV.Only, ev.Value) + cmdBootstrapV.Only = append(cmdBootstrapV.Only, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagBootstrapPromptSecrets: cmdBootstrapV.PromptSecrets = !ev.Negated case FlagBootstrapSkip: if ev.HasValue { - cmdBootstrapV.Skip = append(cmdBootstrapV.Skip, ev.Value) + cmdBootstrapV.Skip = append(cmdBootstrapV.Skip, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagBootstrapUpdate: cmdBootstrapV.Update = !ev.Negated @@ -8962,7 +8962,7 @@ func Parse(args []string) (*Cli, error) { cmdBootstrapRemoteV.ConnectTimeout = ev.Value case FlagBootstrapRemoteExclude: if ev.HasValue { - cmdBootstrapRemoteV.Exclude = append(cmdBootstrapRemoteV.Exclude, ev.Value) + cmdBootstrapRemoteV.Exclude = append(cmdBootstrapRemoteV.Exclude, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagBootstrapRemoteFailFast: cmdBootstrapRemoteV.FailFast = !ev.Negated @@ -8970,7 +8970,7 @@ func Parse(args []string) (*Cli, error) { cmdBootstrapRemoteV.ForceDotfiles = !ev.Negated case FlagBootstrapRemoteHost: if ev.HasValue { - cmdBootstrapRemoteV.Host = append(cmdBootstrapRemoteV.Host, ev.Value) + cmdBootstrapRemoteV.Host = append(cmdBootstrapRemoteV.Host, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagBootstrapRemoteIdentityFile: cmdBootstrapRemoteV.IdentityFile = ev.Value @@ -8982,7 +8982,7 @@ func Parse(args []string) (*Cli, error) { cmdBootstrapRemoteV.MiseBin = ev.Value case FlagBootstrapRemoteOnly: if ev.HasValue { - cmdBootstrapRemoteV.Only = append(cmdBootstrapRemoteV.Only, ev.Value) + cmdBootstrapRemoteV.Only = append(cmdBootstrapRemoteV.Only, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagBootstrapRemotePort: cmdBootstrapRemoteV.Port = ev.Value @@ -8992,17 +8992,17 @@ func Parse(args []string) (*Cli, error) { cmdBootstrapRemoteV.RemoteMise = ev.Value case FlagBootstrapRemoteSkip: if ev.HasValue { - cmdBootstrapRemoteV.Skip = append(cmdBootstrapRemoteV.Skip, ev.Value) + cmdBootstrapRemoteV.Skip = append(cmdBootstrapRemoteV.Skip, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagBootstrapRemoteSource: cmdBootstrapRemoteV.Source = ev.Value case FlagBootstrapRemoteSshOption: if ev.HasValue { - cmdBootstrapRemoteV.SshOption = append(cmdBootstrapRemoteV.SshOption, ev.Value) + cmdBootstrapRemoteV.SshOption = append(cmdBootstrapRemoteV.SshOption, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagBootstrapRemoteTag: if ev.HasValue { - cmdBootstrapRemoteV.Tag = append(cmdBootstrapRemoteV.Tag, ev.Value) + cmdBootstrapRemoteV.Tag = append(cmdBootstrapRemoteV.Tag, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagBootstrapRemoteUpdate: cmdBootstrapRemoteV.Update = !ev.Negated @@ -9158,19 +9158,19 @@ func Parse(args []string) (*Cli, error) { cmdExecV.Jobs = ev.Value case FlagExecAllowEnv: if ev.HasValue { - cmdExecV.AllowEnv = append(cmdExecV.AllowEnv, ev.Value) + cmdExecV.AllowEnv = append(cmdExecV.AllowEnv, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagExecAllowNet: if ev.HasValue { - cmdExecV.AllowNet = append(cmdExecV.AllowNet, ev.Value) + cmdExecV.AllowNet = append(cmdExecV.AllowNet, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagExecAllowRead: if ev.HasValue { - cmdExecV.AllowRead = append(cmdExecV.AllowRead, ev.Value) + cmdExecV.AllowRead = append(cmdExecV.AllowRead, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagExecAllowWrite: if ev.HasValue { - cmdExecV.AllowWrite = append(cmdExecV.AllowWrite, ev.Value) + cmdExecV.AllowWrite = append(cmdExecV.AllowWrite, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagExecDenyAll: cmdExecV.DenyAll = !ev.Negated @@ -9258,11 +9258,11 @@ func Parse(args []string) (*Cli, error) { cmdGenerateToolStubV.Lock = !ev.Negated case FlagGenerateToolStubPlatformBin: if ev.HasValue { - cmdGenerateToolStubV.PlatformBin = append(cmdGenerateToolStubV.PlatformBin, ev.Value) + cmdGenerateToolStubV.PlatformBin = append(cmdGenerateToolStubV.PlatformBin, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagGenerateToolStubPlatformUrl: if ev.HasValue { - cmdGenerateToolStubV.PlatformUrl = append(cmdGenerateToolStubV.PlatformUrl, ev.Value) + cmdGenerateToolStubV.PlatformUrl = append(cmdGenerateToolStubV.PlatformUrl, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagGenerateToolStubSkipDownload: cmdGenerateToolStubV.SkipDownload = !ev.Negated @@ -9286,7 +9286,7 @@ func Parse(args []string) (*Cli, error) { cmdGlobalV.Pin = !ev.Negated case FlagGlobalRemove: if ev.HasValue { - cmdGlobalV.Remove = append(cmdGlobalV.Remove, ev.Value) + cmdGlobalV.Remove = append(cmdGlobalV.Remove, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagHookEnvForce: cmdHookEnvV.Force = !ev.Negated @@ -9346,7 +9346,7 @@ func Parse(args []string) (*Cli, error) { cmdLocalV.Pin = !ev.Negated case FlagLocalRemove: if ev.HasValue { - cmdLocalV.Remove = append(cmdLocalV.Remove, ev.Value) + cmdLocalV.Remove = append(cmdLocalV.Remove, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagLockGlobal: cmdLockV.Global = !ev.Negated @@ -9356,7 +9356,7 @@ func Parse(args []string) (*Cli, error) { cmdLockV.DryRun = !ev.Negated case FlagLockPlatform: if ev.HasValue { - cmdLockV.Platform = append(cmdLockV.Platform, ev.Value) + cmdLockV.Platform = append(cmdLockV.Platform, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagLockBump: cmdLockV.Bump = !ev.Negated @@ -9408,7 +9408,7 @@ func Parse(args []string) (*Cli, error) { cmdLsRemoteV.StrictMetadata = !ev.Negated case FlagOciBuildCopy: if ev.HasValue { - cmdOciBuildV.Copy = append(cmdOciBuildV.Copy, ev.Value) + cmdOciBuildV.Copy = append(cmdOciBuildV.Copy, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagOciBuildOutput: cmdOciBuildV.Output = ev.Value @@ -9460,11 +9460,11 @@ func Parse(args []string) (*Cli, error) { cmdOciRunV.Owner = ev.Value case FlagOciRunVolume: if ev.HasValue { - cmdOciRunV.Volume = append(cmdOciRunV.Volume, ev.Value) + cmdOciRunV.Volume = append(cmdOciRunV.Volume, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagOciRunEnv: if ev.HasValue { - cmdOciRunV.Env = append(cmdOciRunV.Env, ev.Value) + cmdOciRunV.Env = append(cmdOciRunV.Env, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagOciRunInteractive: cmdOciRunV.Interactive = !ev.Negated @@ -9542,11 +9542,11 @@ func Parse(args []string) (*Cli, error) { cmdDepsV.Monorepo = !ev.Negated case FlagDepsOnly: if ev.HasValue { - cmdDepsV.Only = append(cmdDepsV.Only, ev.Value) + cmdDepsV.Only = append(cmdDepsV.Only, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagDepsSkip: if ev.HasValue { - cmdDepsV.Skip = append(cmdDepsV.Skip, ev.Value) + cmdDepsV.Skip = append(cmdDepsV.Skip, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagDepsAddDev: cmdDepsAddV.Dev = !ev.Negated @@ -9562,11 +9562,11 @@ func Parse(args []string) (*Cli, error) { cmdDepsInstallV.Monorepo = !ev.Negated case FlagDepsInstallOnly: if ev.HasValue { - cmdDepsInstallV.Only = append(cmdDepsInstallV.Only, ev.Value) + cmdDepsInstallV.Only = append(cmdDepsInstallV.Only, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagDepsInstallSkip: if ev.HasValue { - cmdDepsInstallV.Skip = append(cmdDepsInstallV.Skip, ev.Value) + cmdDepsInstallV.Skip = append(cmdDepsInstallV.Skip, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagPruneDryRun: cmdPruneV.DryRun = !ev.Negated @@ -9622,23 +9622,23 @@ func Parse(args []string) (*Cli, error) { cmdRunV.Silent = !ev.Negated case FlagRunTool: if ev.HasValue { - cmdRunV.Tool = append(cmdRunV.Tool, ev.Value) + cmdRunV.Tool = append(cmdRunV.Tool, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagRunAllowEnv: if ev.HasValue { - cmdRunV.AllowEnv = append(cmdRunV.AllowEnv, ev.Value) + cmdRunV.AllowEnv = append(cmdRunV.AllowEnv, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagRunAllowNet: if ev.HasValue { - cmdRunV.AllowNet = append(cmdRunV.AllowNet, ev.Value) + cmdRunV.AllowNet = append(cmdRunV.AllowNet, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagRunAllowRead: if ev.HasValue { - cmdRunV.AllowRead = append(cmdRunV.AllowRead, ev.Value) + cmdRunV.AllowRead = append(cmdRunV.AllowRead, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagRunAllowWrite: if ev.HasValue { - cmdRunV.AllowWrite = append(cmdRunV.AllowWrite, ev.Value) + cmdRunV.AllowWrite = append(cmdRunV.AllowWrite, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagRunDenyAll: cmdRunV.DenyAll = !ev.Negated @@ -9696,11 +9696,11 @@ func Parse(args []string) (*Cli, error) { cmdSetV.AgeKeyFile = ev.Value case FlagSetAgeRecipient: if ev.HasValue { - cmdSetV.AgeRecipient = append(cmdSetV.AgeRecipient, ev.Value) + cmdSetV.AgeRecipient = append(cmdSetV.AgeRecipient, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagSetAgeSshRecipient: if ev.HasValue { - cmdSetV.AgeSshRecipient = append(cmdSetV.AgeSshRecipient, ev.Value) + cmdSetV.AgeSshRecipient = append(cmdSetV.AgeSshRecipient, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagSetComplete: cmdSetV.Complete = !ev.Negated @@ -9712,7 +9712,7 @@ func Parse(args []string) (*Cli, error) { cmdSetV.Prompt = !ev.Negated case FlagSetRemove: if ev.HasValue { - cmdSetV.Remove = append(cmdSetV.Remove, ev.Value) + cmdSetV.Remove = append(cmdSetV.Remove, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagSetStdin: cmdSetV.Stdin = !ev.Negated @@ -9796,11 +9796,11 @@ func Parse(args []string) (*Cli, error) { cmdTasksV.Usage = !ev.Negated case FlagTasksAddAlias: if ev.HasValue { - cmdTasksAddV.Alias = append(cmdTasksAddV.Alias, ev.Value) + cmdTasksAddV.Alias = append(cmdTasksAddV.Alias, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksAddDepends: if ev.HasValue { - cmdTasksAddV.Depends = append(cmdTasksAddV.Depends, ev.Value) + cmdTasksAddV.Depends = append(cmdTasksAddV.Depends, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksAddDir: cmdTasksAddV.Dir = ev.Value @@ -9814,21 +9814,21 @@ func Parse(args []string) (*Cli, error) { cmdTasksAddV.Raw = !ev.Negated case FlagTasksAddSources: if ev.HasValue { - cmdTasksAddV.Sources = append(cmdTasksAddV.Sources, ev.Value) + cmdTasksAddV.Sources = append(cmdTasksAddV.Sources, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksAddWaitFor: if ev.HasValue { - cmdTasksAddV.WaitFor = append(cmdTasksAddV.WaitFor, ev.Value) + cmdTasksAddV.WaitFor = append(cmdTasksAddV.WaitFor, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksAddDependsPost: if ev.HasValue { - cmdTasksAddV.DependsPost = append(cmdTasksAddV.DependsPost, ev.Value) + cmdTasksAddV.DependsPost = append(cmdTasksAddV.DependsPost, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksAddDescription: cmdTasksAddV.Description = ev.Value case FlagTasksAddOutputs: if ev.HasValue { - cmdTasksAddV.Outputs = append(cmdTasksAddV.Outputs, ev.Value) + cmdTasksAddV.Outputs = append(cmdTasksAddV.Outputs, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksAddRunWindows: cmdTasksAddV.RunWindows = ev.Value @@ -9908,23 +9908,23 @@ func Parse(args []string) (*Cli, error) { cmdTasksRunV.Silent = !ev.Negated case FlagTasksRunTool: if ev.HasValue { - cmdTasksRunV.Tool = append(cmdTasksRunV.Tool, ev.Value) + cmdTasksRunV.Tool = append(cmdTasksRunV.Tool, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksRunAllowEnv: if ev.HasValue { - cmdTasksRunV.AllowEnv = append(cmdTasksRunV.AllowEnv, ev.Value) + cmdTasksRunV.AllowEnv = append(cmdTasksRunV.AllowEnv, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksRunAllowNet: if ev.HasValue { - cmdTasksRunV.AllowNet = append(cmdTasksRunV.AllowNet, ev.Value) + cmdTasksRunV.AllowNet = append(cmdTasksRunV.AllowNet, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksRunAllowRead: if ev.HasValue { - cmdTasksRunV.AllowRead = append(cmdTasksRunV.AllowRead, ev.Value) + cmdTasksRunV.AllowRead = append(cmdTasksRunV.AllowRead, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksRunAllowWrite: if ev.HasValue { - cmdTasksRunV.AllowWrite = append(cmdTasksRunV.AllowWrite, ev.Value) + cmdTasksRunV.AllowWrite = append(cmdTasksRunV.AllowWrite, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagTasksRunDenyAll: cmdTasksRunV.DenyAll = !ev.Negated @@ -10038,7 +10038,7 @@ func Parse(args []string) (*Cli, error) { cmdUpgradeV.DryRun = !ev.Negated case FlagUpgradeExclude: if ev.HasValue { - cmdUpgradeV.Exclude = append(cmdUpgradeV.Exclude, ev.Value) + cmdUpgradeV.Exclude = append(cmdUpgradeV.Exclude, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagUpgradeDryRunCode: cmdUpgradeV.DryRunCode = !ev.Negated @@ -10078,27 +10078,27 @@ func Parse(args []string) (*Cli, error) { cmdUseV.Raw = !ev.Negated case FlagUseRemove: if ev.HasValue { - cmdUseV.Remove = append(cmdUseV.Remove, ev.Value) + cmdUseV.Remove = append(cmdUseV.Remove, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagVersionJson: cmdVersionV.Json = !ev.Negated case FlagWatchTaskFlag: if ev.HasValue { - cmdWatchV.TaskFlag = append(cmdWatchV.TaskFlag, ev.Value) + cmdWatchV.TaskFlag = append(cmdWatchV.TaskFlag, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchGlob: if ev.HasValue { - cmdWatchV.Glob = append(cmdWatchV.Glob, ev.Value) + cmdWatchV.Glob = append(cmdWatchV.Glob, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchSkipDeps: cmdWatchV.SkipDeps = !ev.Negated case FlagWatchWatch: if ev.HasValue { - cmdWatchV.Watch = append(cmdWatchV.Watch, ev.Value) + cmdWatchV.Watch = append(cmdWatchV.Watch, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchWatchNonRecursive: if ev.HasValue { - cmdWatchV.WatchNonRecursive = append(cmdWatchV.WatchNonRecursive, ev.Value) + cmdWatchV.WatchNonRecursive = append(cmdWatchV.WatchNonRecursive, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchWatchFile: cmdWatchV.WatchFile = ev.Value @@ -10116,7 +10116,7 @@ func Parse(args []string) (*Cli, error) { cmdWatchV.StopTimeout = ev.Value case FlagWatchMapSignal: if ev.HasValue { - cmdWatchV.MapSignal = append(cmdWatchV.MapSignal, ev.Value) + cmdWatchV.MapSignal = append(cmdWatchV.MapSignal, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchDebounce: cmdWatchV.Debounce = ev.Value @@ -10150,7 +10150,7 @@ func Parse(args []string) (*Cli, error) { cmdWatchV.OnlyEmitEvents = !ev.Negated case FlagWatchEnv: if ev.HasValue { - cmdWatchV.Env = append(cmdWatchV.Env, ev.Value) + cmdWatchV.Env = append(cmdWatchV.Env, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchWrapProcess: cmdWatchV.WrapProcess = ev.Value @@ -10170,31 +10170,31 @@ func Parse(args []string) (*Cli, error) { cmdWatchV.Workdir = ev.Value case FlagWatchExts: if ev.HasValue { - cmdWatchV.Exts = append(cmdWatchV.Exts, ev.Value) + cmdWatchV.Exts = append(cmdWatchV.Exts, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchFilter: if ev.HasValue { - cmdWatchV.Filter = append(cmdWatchV.Filter, ev.Value) + cmdWatchV.Filter = append(cmdWatchV.Filter, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchFilterFile: if ev.HasValue { - cmdWatchV.FilterFile = append(cmdWatchV.FilterFile, ev.Value) + cmdWatchV.FilterFile = append(cmdWatchV.FilterFile, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchFilterProg: if ev.HasValue { - cmdWatchV.FilterProg = append(cmdWatchV.FilterProg, ev.Value) + cmdWatchV.FilterProg = append(cmdWatchV.FilterProg, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchIgnore: if ev.HasValue { - cmdWatchV.Ignore = append(cmdWatchV.Ignore, ev.Value) + cmdWatchV.Ignore = append(cmdWatchV.Ignore, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchIgnoreFile: if ev.HasValue { - cmdWatchV.IgnoreFile = append(cmdWatchV.IgnoreFile, ev.Value) + cmdWatchV.IgnoreFile = append(cmdWatchV.IgnoreFile, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchFsEvents: if ev.HasValue { - cmdWatchV.FsEvents = append(cmdWatchV.FsEvents, ev.Value) + cmdWatchV.FsEvents = append(cmdWatchV.FsEvents, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagWatchNoMeta: cmdWatchV.NoMeta = !ev.Negated @@ -10213,14 +10213,15 @@ func Parse(args []string) (*Cli, error) { } case argv.KindArg: seen[ev.Arg.Key]++ - given[ev.Arg.Key] = append(given[ev.Arg.Key], ev.Value) + values := argv.SplitValue(ev.Value, ev.Arg.Delimiter, ev.Delimit) + given[ev.Arg.Key] = append(given[ev.Arg.Key], values...) switch ev.Arg.Key { case ArgTask: out.Task = ev.Value case ArgTaskArgs: - out.TaskArgs = append(out.TaskArgs, ev.Value) + out.TaskArgs = append(out.TaskArgs, values...) case ArgTaskArgsLast: - out.TaskArgsLast = append(out.TaskArgsLast, ev.Value) + out.TaskArgsLast = append(out.TaskArgsLast, values...) case ArgActivateShellType: cmdActivateV.ShellType = ev.Value case ArgToolAliasGetTool: @@ -10240,43 +10241,43 @@ func Parse(args []string) (*Cli, error) { case ArgToolAliasUnsetAlias: cmdToolAliasUnsetV.Alias = ev.Value case ArgAsdfArgs: - cmdAsdfV.Args = append(cmdAsdfV.Args, ev.Value) + cmdAsdfV.Args = append(cmdAsdfV.Args, values...) case ArgBinPathsToolVersion: - cmdBinPathsV.ToolVersion = append(cmdBinPathsV.ToolVersion, ev.Value) + cmdBinPathsV.ToolVersion = append(cmdBinPathsV.ToolVersion, values...) case ArgBootstrapDotfilesAddTarget: - cmdBootstrapDotfilesAddV.Target = append(cmdBootstrapDotfilesAddV.Target, ev.Value) + cmdBootstrapDotfilesAddV.Target = append(cmdBootstrapDotfilesAddV.Target, values...) case ArgBootstrapDotfilesApplyTarget: - cmdBootstrapDotfilesApplyV.Target = append(cmdBootstrapDotfilesApplyV.Target, ev.Value) + cmdBootstrapDotfilesApplyV.Target = append(cmdBootstrapDotfilesApplyV.Target, values...) case ArgBootstrapDotfilesEditTarget: cmdBootstrapDotfilesEditV.Target = ev.Value case ArgBootstrapDotfilesStatusTarget: - cmdBootstrapDotfilesStatusV.Target = append(cmdBootstrapDotfilesStatusV.Target, ev.Value) + cmdBootstrapDotfilesStatusV.Target = append(cmdBootstrapDotfilesStatusV.Target, values...) case ArgBootstrapDotfilesUnapplyTarget: - cmdBootstrapDotfilesUnapplyV.Target = append(cmdBootstrapDotfilesUnapplyV.Target, ev.Value) + cmdBootstrapDotfilesUnapplyV.Target = append(cmdBootstrapDotfilesUnapplyV.Target, values...) case ArgBootstrapPackagesApplyPackage: - cmdBootstrapPackagesApplyV.Package = append(cmdBootstrapPackagesApplyV.Package, ev.Value) + cmdBootstrapPackagesApplyV.Package = append(cmdBootstrapPackagesApplyV.Package, values...) case ArgBootstrapPackagesBrewTapTap: cmdBootstrapPackagesBrewTapV.Tap = ev.Value case ArgBootstrapPackagesBrewTapUrl: cmdBootstrapPackagesBrewTapV.Url = ev.Value case ArgBootstrapPackagesBrewUntapTaps: - cmdBootstrapPackagesBrewUntapV.Taps = append(cmdBootstrapPackagesBrewUntapV.Taps, ev.Value) + cmdBootstrapPackagesBrewUntapV.Taps = append(cmdBootstrapPackagesBrewUntapV.Taps, values...) case ArgBootstrapPackagesUpgradePackage: - cmdBootstrapPackagesUpgradeV.Package = append(cmdBootstrapPackagesUpgradeV.Package, ev.Value) + cmdBootstrapPackagesUpgradeV.Package = append(cmdBootstrapPackagesUpgradeV.Package, values...) case ArgBootstrapPackagesUsePackage: - cmdBootstrapPackagesUseV.Package = append(cmdBootstrapPackagesUseV.Package, ev.Value) + cmdBootstrapPackagesUseV.Package = append(cmdBootstrapPackagesUseV.Package, values...) case ArgBootstrapRemoteTarget: - cmdBootstrapRemoteV.Target = append(cmdBootstrapRemoteV.Target, ev.Value) + cmdBootstrapRemoteV.Target = append(cmdBootstrapRemoteV.Target, values...) case ArgBootstrapReposExecPath: - cmdBootstrapReposExecV.Path = append(cmdBootstrapReposExecV.Path, ev.Value) + cmdBootstrapReposExecV.Path = append(cmdBootstrapReposExecV.Path, values...) case ArgBootstrapReposExecCommand: - cmdBootstrapReposExecV.Command = append(cmdBootstrapReposExecV.Command, ev.Value) + cmdBootstrapReposExecV.Command = append(cmdBootstrapReposExecV.Command, values...) case ArgBootstrapReposUpdatePath: - cmdBootstrapReposUpdateV.Path = append(cmdBootstrapReposUpdateV.Path, ev.Value) + cmdBootstrapReposUpdateV.Path = append(cmdBootstrapReposUpdateV.Path, values...) case ArgCacheClearTool: - cmdCacheClearV.Tool = append(cmdCacheClearV.Tool, ev.Value) + cmdCacheClearV.Tool = append(cmdCacheClearV.Tool, values...) case ArgCachePruneTool: - cmdCachePruneV.Tool = append(cmdCachePruneV.Tool, ev.Value) + cmdCachePruneV.Tool = append(cmdCachePruneV.Tool, values...) case ArgCacheTaskTask: cmdCacheTaskV.Task = ev.Value case ArgCompletionShell: @@ -10290,23 +10291,23 @@ func Parse(args []string) (*Cli, error) { case ArgCurrentPlugin: cmdCurrentV.Plugin = ev.Value case ArgDotfilesAddTarget: - cmdDotfilesAddV.Target = append(cmdDotfilesAddV.Target, ev.Value) + cmdDotfilesAddV.Target = append(cmdDotfilesAddV.Target, values...) case ArgDotfilesApplyTarget: - cmdDotfilesApplyV.Target = append(cmdDotfilesApplyV.Target, ev.Value) + cmdDotfilesApplyV.Target = append(cmdDotfilesApplyV.Target, values...) case ArgDotfilesEditTarget: cmdDotfilesEditV.Target = ev.Value case ArgDotfilesStatusTarget: - cmdDotfilesStatusV.Target = append(cmdDotfilesStatusV.Target, ev.Value) + cmdDotfilesStatusV.Target = append(cmdDotfilesStatusV.Target, values...) case ArgDotfilesUnapplyTarget: - cmdDotfilesUnapplyV.Target = append(cmdDotfilesUnapplyV.Target, ev.Value) + cmdDotfilesUnapplyV.Target = append(cmdDotfilesUnapplyV.Target, values...) case ArgEnDir: cmdEnV.Dir = ev.Value case ArgEnvToolVersion: - cmdEnvV.ToolVersion = append(cmdEnvV.ToolVersion, ev.Value) + cmdEnvV.ToolVersion = append(cmdEnvV.ToolVersion, values...) case ArgExecToolVersion: - cmdExecV.ToolVersion = append(cmdExecV.ToolVersion, ev.Value) + cmdExecV.ToolVersion = append(cmdExecV.ToolVersion, values...) case ArgExecCommand: - cmdExecV.CommandArg = append(cmdExecV.CommandArg, ev.Value) + cmdExecV.CommandArg = append(cmdExecV.CommandArg, values...) case ArgGenerateConfigPath: cmdGenerateConfigV.Path = ev.Value case ArgGenerateToolStubOutput: @@ -10314,13 +10315,13 @@ func Parse(args []string) (*Cli, error) { case ArgGithubTokenHost: cmdGithubTokenV.Host = ev.Value case ArgGlobalToolVersion: - cmdGlobalV.ToolVersion = append(cmdGlobalV.ToolVersion, ev.Value) + cmdGlobalV.ToolVersion = append(cmdGlobalV.ToolVersion, values...) case ArgHookNotFoundBin: cmdHookNotFoundV.Bin = ev.Value case ArgEditPath: cmdEditV.Path = ev.Value case ArgInstallToolVersion: - cmdInstallV.ToolVersion = append(cmdInstallV.ToolVersion, ev.Value) + cmdInstallV.ToolVersion = append(cmdInstallV.ToolVersion, values...) case ArgInstallIntoToolVersion: cmdInstallIntoV.ToolVersion = ev.Value case ArgInstallIntoPath: @@ -10334,11 +10335,11 @@ func Parse(args []string) (*Cli, error) { case ArgLinkPath: cmdLinkV.Path = ev.Value case ArgLocalToolVersion: - cmdLocalV.ToolVersion = append(cmdLocalV.ToolVersion, ev.Value) + cmdLocalV.ToolVersion = append(cmdLocalV.ToolVersion, values...) case ArgLockTool: - cmdLockV.Tool = append(cmdLockV.Tool, ev.Value) + cmdLockV.Tool = append(cmdLockV.Tool, values...) case ArgLsInstalledTool: - cmdLsV.InstalledTool = append(cmdLsV.InstalledTool, ev.Value) + cmdLsV.InstalledTool = append(cmdLsV.InstalledTool, values...) case ArgLsRemoteToolVersion: cmdLsRemoteV.ToolVersion = ev.Value case ArgLsRemotePrefix: @@ -10346,33 +10347,33 @@ func Parse(args []string) (*Cli, error) { case ArgOciPushRef: cmdOciPushV.Ref = ev.Value case ArgOciRunCmd: - cmdOciRunV.Cmd = append(cmdOciRunV.Cmd, ev.Value) + cmdOciRunV.Cmd = append(cmdOciRunV.Cmd, values...) case ArgOutdatedToolVersion: - cmdOutdatedV.ToolVersion = append(cmdOutdatedV.ToolVersion, ev.Value) + cmdOutdatedV.ToolVersion = append(cmdOutdatedV.ToolVersion, values...) case ArgPluginsInstallNewPlugin: cmdPluginsInstallV.NewPlugin = ev.Value case ArgPluginsInstallGitUrl: cmdPluginsInstallV.GitUrl = ev.Value case ArgPluginsInstallRest: - cmdPluginsInstallV.Rest = append(cmdPluginsInstallV.Rest, ev.Value) + cmdPluginsInstallV.Rest = append(cmdPluginsInstallV.Rest, values...) case ArgPluginsLinkName: cmdPluginsLinkV.Name = ev.Value case ArgPluginsLinkDir: cmdPluginsLinkV.Dir = ev.Value case ArgPluginsUninstallPlugin: - cmdPluginsUninstallV.Plugin = append(cmdPluginsUninstallV.Plugin, ev.Value) + cmdPluginsUninstallV.Plugin = append(cmdPluginsUninstallV.Plugin, values...) case ArgPluginsUpdatePlugin: - cmdPluginsUpdateV.Plugin = append(cmdPluginsUpdateV.Plugin, ev.Value) + cmdPluginsUpdateV.Plugin = append(cmdPluginsUpdateV.Plugin, values...) case ArgDepsProvider: cmdDepsV.Provider = ev.Value case ArgDepsAddPackages: - cmdDepsAddV.Packages = append(cmdDepsAddV.Packages, ev.Value) + cmdDepsAddV.Packages = append(cmdDepsAddV.Packages, values...) case ArgDepsInstallProvider: cmdDepsInstallV.Provider = ev.Value case ArgDepsRemovePackages: - cmdDepsRemoveV.Packages = append(cmdDepsRemoveV.Packages, ev.Value) + cmdDepsRemoveV.Packages = append(cmdDepsRemoveV.Packages, values...) case ArgPruneInstalledTool: - cmdPruneV.InstalledTool = append(cmdPruneV.InstalledTool, ev.Value) + cmdPruneV.InstalledTool = append(cmdPruneV.InstalledTool, values...) case ArgRegistryName: cmdRegistryV.Name = ev.Value case ArgReshimTool: @@ -10384,7 +10385,7 @@ func Parse(args []string) (*Cli, error) { case ArgSelfUpdateVersion: cmdSelfUpdateV.Version = ev.Value case ArgSetEnvVar: - cmdSetV.EnvVar = append(cmdSetV.EnvVar, ev.Value) + cmdSetV.EnvVar = append(cmdSetV.EnvVar, values...) case ArgSettingsSetting: cmdSettingsV.Setting = ev.Value case ArgSettingsValue: @@ -10404,7 +10405,7 @@ func Parse(args []string) (*Cli, error) { case ArgSettingsUnsetKey: cmdSettingsUnsetV.Key = ev.Value case ArgShellToolVersion: - cmdShellV.ToolVersion = append(cmdShellV.ToolVersion, ev.Value) + cmdShellV.ToolVersion = append(cmdShellV.ToolVersion, values...) case ArgShellAliasGetShellAlias: cmdShellAliasGetV.ShellAlias = ev.Value case ArgShellAliasSetShellAlias: @@ -10418,9 +10419,9 @@ func Parse(args []string) (*Cli, error) { case ArgTasksAddTask: cmdTasksAddV.Task = ev.Value case ArgTasksAddRun: - cmdTasksAddV.Run = append(cmdTasksAddV.Run, ev.Value) + cmdTasksAddV.Run = append(cmdTasksAddV.Run, values...) case ArgTasksDepsTasks: - cmdTasksDepsV.Tasks = append(cmdTasksDepsV.Tasks, ev.Value) + cmdTasksDepsV.Tasks = append(cmdTasksDepsV.Tasks, values...) case ArgTasksEditTask: cmdTasksEditV.Task = ev.Value case ArgTasksInfoTask: @@ -10428,13 +10429,13 @@ func Parse(args []string) (*Cli, error) { case ArgTasksRunTask: cmdTasksRunV.Task = ev.Value case ArgTasksRunArgs: - cmdTasksRunV.Args = append(cmdTasksRunV.Args, ev.Value) + cmdTasksRunV.Args = append(cmdTasksRunV.Args, values...) case ArgTasksRunArgsLast: - cmdTasksRunV.ArgsLast = append(cmdTasksRunV.ArgsLast, ev.Value) + cmdTasksRunV.ArgsLast = append(cmdTasksRunV.ArgsLast, values...) case ArgTasksValidateTasks: - cmdTasksValidateV.Tasks = append(cmdTasksValidateV.Tasks, ev.Value) + cmdTasksValidateV.Tasks = append(cmdTasksValidateV.Tasks, values...) case ArgTestToolTools: - cmdTestToolV.Tools = append(cmdTestToolV.Tools, ev.Value) + cmdTestToolV.Tools = append(cmdTestToolV.Tools, values...) case ArgTokenForgejoHost: cmdTokenForgejoV.Host = ev.Value case ArgTokenGithubHost: @@ -10446,25 +10447,25 @@ func Parse(args []string) (*Cli, error) { case ArgToolStubFile: cmdToolStubV.File = ev.Value case ArgToolStubArgs: - cmdToolStubV.Args = append(cmdToolStubV.Args, ev.Value) + cmdToolStubV.Args = append(cmdToolStubV.Args, values...) case ArgTrustConfigFile: cmdTrustV.ConfigFile = ev.Value case ArgUninstallInstalledToolVersion: - cmdUninstallV.InstalledToolVersion = append(cmdUninstallV.InstalledToolVersion, ev.Value) + cmdUninstallV.InstalledToolVersion = append(cmdUninstallV.InstalledToolVersion, values...) case ArgUnsetEnvKey: - cmdUnsetV.EnvKey = append(cmdUnsetV.EnvKey, ev.Value) + cmdUnsetV.EnvKey = append(cmdUnsetV.EnvKey, values...) case ArgUntrustConfigFile: cmdUntrustV.ConfigFile = ev.Value case ArgUnuseInstalledToolVersion: - cmdUnuseV.InstalledToolVersion = append(cmdUnuseV.InstalledToolVersion, ev.Value) + cmdUnuseV.InstalledToolVersion = append(cmdUnuseV.InstalledToolVersion, values...) case ArgUpgradeInstalledToolVersion: - cmdUpgradeV.InstalledToolVersion = append(cmdUpgradeV.InstalledToolVersion, ev.Value) + cmdUpgradeV.InstalledToolVersion = append(cmdUpgradeV.InstalledToolVersion, values...) case ArgUseToolVersion: - cmdUseV.ToolVersion = append(cmdUseV.ToolVersion, ev.Value) + cmdUseV.ToolVersion = append(cmdUseV.ToolVersion, values...) case ArgWatchTask: cmdWatchV.Task = ev.Value case ArgWatchArgs: - cmdWatchV.Args = append(cmdWatchV.Args, ev.Value) + cmdWatchV.Args = append(cmdWatchV.Args, values...) case ArgWhereToolVersion: cmdWhereV.ToolVersion = ev.Value case ArgWhereAsdfVersion: diff --git a/go/internal/spec/spec.go b/go/internal/spec/spec.go index 117754a59..825fcaec3 100644 --- a/go/internal/spec/spec.go +++ b/go/internal/spec/spec.go @@ -81,24 +81,25 @@ type Completer struct { // Cmd is one command in the lowered spec. type Cmd struct { - Name string `json:"name"` - Hide bool `json:"hide"` - Help string `json:"help"` - HelpLong string `json:"help_long"` - Usage string `json:"usage"` - BeforeHelp string `json:"before_help"` - AfterHelp string `json:"after_help"` - BeforeHelpLong string `json:"before_help_long"` - AfterHelpLong string `json:"after_help_long"` - Examples []Example `json:"examples"` - Aliases []string `json:"aliases"` - HiddenAliases []string `json:"hidden_aliases"` - Subcommands Subcommands `json:"subcommands"` - Args []Arg `json:"args"` - Flags []Flag `json:"flags"` - UnknownFlags *string `json:"unknown_flags"` - ExternalSubcommand bool `json:"external_subcommand"` - ArgRequiredElseHelp bool `json:"arg_required_else_help"` + Name string `json:"name"` + Hide bool `json:"hide"` + Help string `json:"help"` + HelpLong string `json:"help_long"` + Usage string `json:"usage"` + BeforeHelp string `json:"before_help"` + AfterHelp string `json:"after_help"` + BeforeHelpLong string `json:"before_help_long"` + AfterHelpLong string `json:"after_help_long"` + Examples []Example `json:"examples"` + Aliases []string `json:"aliases"` + HiddenAliases []string `json:"hidden_aliases"` + Subcommands Subcommands `json:"subcommands"` + Args []Arg `json:"args"` + Flags []Flag `json:"flags"` + UnknownFlags *string `json:"unknown_flags"` + ExternalSubcommand bool `json:"external_subcommand"` + ArgRequiredElseHelp bool `json:"arg_required_else_help"` + DontDelimitTrailingValues bool `json:"dont_delimit_trailing_values"` } // Subcommands is a command's children, in the order the spec declared them. @@ -302,6 +303,7 @@ type Arg struct { DoubleDash string `json:"double_dash"` AllowNegativeNumbers bool `json:"allow_negative_numbers"` ValueTerminator string `json:"value_terminator"` + Delimiter string `json:"delimiter"` Choices *Choices `json:"choices"` Default []string `json:"default"` Env string `json:"env"` @@ -583,11 +585,12 @@ func (b *builder) command(c *Cmd, inherited argv.UnknownFlags) *argv.Command { } out := &argv.Command{ - Name: c.Name, - UnknownFlags: unknown, - ExternalSubcommand: c.ExternalSubcommand, - ArgRequiredElseHelp: c.ArgRequiredElseHelp, - Key: b.next(), + Name: c.Name, + UnknownFlags: unknown, + ExternalSubcommand: c.ExternalSubcommand, + ArgRequiredElseHelp: c.ArgRequiredElseHelp, + DontDelimitTrailingValues: c.DontDelimitTrailingValues, + Key: b.next(), } examples := make([]argv.Example, 0, len(c.Examples)) for _, e := range c.Examples { @@ -803,6 +806,9 @@ func (b *builder) flag(f *Flag) *argv.Flag { DefaultMissing: f.DefaultMissing, Global: f.Global, } + if f.Arg != nil && len(f.Arg.Delimiter) == 1 { + out.Delimiter = f.Arg.Delimiter[0] + } b.recordNegation(out.Key, f.Negate) for _, s := range f.Short { if s != "" { @@ -882,6 +888,9 @@ func (b *builder) arg(a *Arg) *argv.Arg { AllowNegativeNumbers: a.AllowNegativeNumbers, ValueTerminator: a.ValueTerminator, } + if len(a.Delimiter) == 1 { + out.Delimiter = a.Delimiter[0] + } if a.Var { out.VarMax = clampVarMax(a.VarMax) } diff --git a/lib/src/docs/models.rs b/lib/src/docs/models.rs index 093972f96..453d80f51 100644 --- a/lib/src/docs/models.rs +++ b/lib/src/docs/models.rs @@ -483,6 +483,7 @@ impl From<&crate::SpecCommand> for SpecCommand { unknown_flags: _, external_subcommand: _, arg_required_else_help: _, + dont_delimit_trailing_values: _, // Rendered above, or deliberately absent from the docs model. args: _, flags: _, diff --git a/lib/src/go/mod.rs b/lib/src/go/mod.rs index 3b5b28ec7..d3f857655 100644 --- a/lib/src/go/mod.rs +++ b/lib/src/go/mod.rs @@ -377,6 +377,12 @@ impl<'a> Emitter<'a> { if e.cmd.arg_required_else_help { lines.push(Line::Field("ArgRequiredElseHelp".into(), "true".into())); } + if e.cmd.dont_delimit_trailing_values { + lines.push(Line::Field( + "DontDelimitTrailingValues".into(), + "true".into(), + )); + } if e.root { if let Some(var) = &default_subcommand { lines.push(Line::Field("DefaultSubcommand".into(), var.clone())); @@ -1178,6 +1184,9 @@ fn flag_literal(flag: &SpecFlag, named: &Named) -> String { if let Some(terminator) = &arg.value_terminator { fields.push(format!("ValueTerminator: {}", go_string(terminator))); } + if let Some(delimiter) = arg.delimiter { + fields.push(format!("Delimiter: {}", go_byte(delimiter))); + } } if flag.require_equals { fields.push("RequireEquals: true".to_string()); @@ -1208,6 +1217,9 @@ fn arg_literal(arg: &SpecArg, named: &Named) -> String { if let Some(terminator) = &arg.value_terminator { fields.push(format!("ValueTerminator: {}", go_string(terminator))); } + if let Some(delimiter) = arg.delimiter { + fields.push(format!("Delimiter: {}", go_byte(delimiter))); + } let double_dash = match arg.double_dash { SpecDoubleDashChoices::Required => Some("argv.DoubleDashRequired"), SpecDoubleDashChoices::Preserve => Some("argv.DoubleDashPreserve"), diff --git a/lib/src/go/snapshots/usage__go__tests__a_default_subcommand_points_into_the_tree.snap b/lib/src/go/snapshots/usage__go__tests__a_default_subcommand_points_into_the_tree.snap index 1cce7cf3d..19cde0b81 100644 --- a/lib/src/go/snapshots/usage__go__tests__a_default_subcommand_points_into_the_tree.snap +++ b/lib/src/go/snapshots/usage__go__tests__a_default_subcommand_points_into_the_tree.snap @@ -118,7 +118,7 @@ func Parse(args []string) (*Cli, error) { case argv.KindFlag: seen[ev.Flag.Key]++ if ev.HasValue { - given[ev.Flag.Key] = append(given[ev.Flag.Key], ev.Value) + given[ev.Flag.Key] = append(given[ev.Flag.Key], argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } else if given[ev.Flag.Key] == nil { // Given without a value is still given, and nil would read as // absent when the fallbacks are applied. @@ -128,12 +128,13 @@ func Parse(args []string) (*Cli, error) { } case argv.KindArg: seen[ev.Arg.Key]++ - given[ev.Arg.Key] = append(given[ev.Arg.Key], ev.Value) + values := argv.SplitValue(ev.Value, ev.Arg.Delimiter, ev.Delimit) + given[ev.Arg.Key] = append(given[ev.Arg.Key], values...) switch ev.Arg.Key { case ArgTask: out.Task = ev.Value case ArgRunArgs: - cmdRunV.Args = append(cmdRunV.Args, ev.Value) + cmdRunV.Args = append(cmdRunV.Args, values...) } } } diff --git a/lib/src/go/snapshots/usage__go__tests__a_whole_cli.snap b/lib/src/go/snapshots/usage__go__tests__a_whole_cli.snap index dccbb4d1b..4cb883351 100644 --- a/lib/src/go/snapshots/usage__go__tests__a_whole_cli.snap +++ b/lib/src/go/snapshots/usage__go__tests__a_whole_cli.snap @@ -201,7 +201,7 @@ func Parse(args []string) (*Cli, error) { case argv.KindFlag: seen[ev.Flag.Key]++ if ev.HasValue { - given[ev.Flag.Key] = append(given[ev.Flag.Key], ev.Value) + given[ev.Flag.Key] = append(given[ev.Flag.Key], argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } else if given[ev.Flag.Key] == nil { // Given without a value is still given, and nil would read as // absent when the fallbacks are applied. @@ -216,7 +216,7 @@ func Parse(args []string) (*Cli, error) { out.Jobs = ev.Value case FlagInclude: if ev.HasValue { - out.Include = append(out.Include, ev.Value) + out.Include = append(out.Include, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } case FlagInstallForce: cmdInstallV.Force = !ev.Negated @@ -225,12 +225,13 @@ func Parse(args []string) (*Cli, error) { } case argv.KindArg: seen[ev.Arg.Key]++ - given[ev.Arg.Key] = append(given[ev.Arg.Key], ev.Value) + values := argv.SplitValue(ev.Value, ev.Arg.Delimiter, ev.Delimit) + given[ev.Arg.Key] = append(given[ev.Arg.Key], values...) switch ev.Arg.Key { case ArgFile: out.File = ev.Value case ArgRest: - out.Rest = append(out.Rest, ev.Value) + out.Rest = append(out.Rest, values...) case ArgInstallPkg: cmdInstallV.Pkg = ev.Value } diff --git a/lib/src/go/snapshots/usage__go__tests__colliding_names_get_distinct_identifiers.snap b/lib/src/go/snapshots/usage__go__tests__colliding_names_get_distinct_identifiers.snap index b5de6bf7a..39bbd97e1 100644 --- a/lib/src/go/snapshots/usage__go__tests__colliding_names_get_distinct_identifiers.snap +++ b/lib/src/go/snapshots/usage__go__tests__colliding_names_get_distinct_identifiers.snap @@ -154,7 +154,7 @@ func Parse(args []string) (*Cli, error) { case argv.KindFlag: seen[ev.Flag.Key]++ if ev.HasValue { - given[ev.Flag.Key] = append(given[ev.Flag.Key], ev.Value) + given[ev.Flag.Key] = append(given[ev.Flag.Key], argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } else if given[ev.Flag.Key] == nil { // Given without a value is still given, and nil would read as // absent when the fallbacks are applied. @@ -168,7 +168,8 @@ func Parse(args []string) (*Cli, error) { } case argv.KindArg: seen[ev.Arg.Key]++ - given[ev.Arg.Key] = append(given[ev.Arg.Key], ev.Value) + values := argv.SplitValue(ev.Value, ev.Arg.Delimiter, ev.Delimit) + given[ev.Arg.Key] = append(given[ev.Arg.Key], values...) switch ev.Arg.Key { } } diff --git a/lib/src/go/snapshots/usage__go__tests__unknown_flags_are_inherited_and_overridable.snap b/lib/src/go/snapshots/usage__go__tests__unknown_flags_are_inherited_and_overridable.snap index b147ffc42..ad2a459ec 100644 --- a/lib/src/go/snapshots/usage__go__tests__unknown_flags_are_inherited_and_overridable.snap +++ b/lib/src/go/snapshots/usage__go__tests__unknown_flags_are_inherited_and_overridable.snap @@ -162,7 +162,7 @@ func Parse(args []string) (*Cli, error) { case argv.KindFlag: seen[ev.Flag.Key]++ if ev.HasValue { - given[ev.Flag.Key] = append(given[ev.Flag.Key], ev.Value) + given[ev.Flag.Key] = append(given[ev.Flag.Key], argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...) } else if given[ev.Flag.Key] == nil { // Given without a value is still given, and nil would read as // absent when the fallbacks are applied. @@ -172,7 +172,8 @@ func Parse(args []string) (*Cli, error) { } case argv.KindArg: seen[ev.Arg.Key]++ - given[ev.Arg.Key] = append(given[ev.Arg.Key], ev.Value) + values := argv.SplitValue(ev.Value, ev.Arg.Delimiter, ev.Delimit) + given[ev.Arg.Key] = append(given[ev.Arg.Key], values...) switch ev.Arg.Key { } } diff --git a/lib/src/go/structs.rs b/lib/src/go/structs.rs index 1edb59369..5b1c0c64e 100644 --- a/lib/src/go/structs.rs +++ b/lib/src/go/structs.rs @@ -188,7 +188,7 @@ fn parse_fn(out: &mut String, commands: &[Emitted], assigned: &Fields) { "\t\t\t}}\n\t\tcase argv.KindFlag:\n\t\t\tseen[ev.Flag.Key]++\n\ {conditional_event}\ \t\t\tif ev.HasValue {{\n\ - \t\t\t\tgiven[ev.Flag.Key] = append(given[ev.Flag.Key], ev.Value)\n\ + \t\t\t\tgiven[ev.Flag.Key] = append(given[ev.Flag.Key], argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...)\n\ \t\t\t}} else if given[ev.Flag.Key] == nil {{\n\ \t\t\t\t// Given without a value is still given, and nil would read as\n\ \t\t\t\t// absent when the fallbacks are applied.\n\ @@ -210,7 +210,8 @@ fn parse_fn(out: &mut String, commands: &[Emitted], assigned: &Fields) { let _ = writeln!( out, "\t\t\t}}\n\t\tcase argv.KindArg:\n\t\t\tseen[ev.Arg.Key]++\n\ - \t\t\tgiven[ev.Arg.Key] = append(given[ev.Arg.Key], ev.Value)\n\ + \t\t\tvalues := argv.SplitValue(ev.Value, ev.Arg.Delimiter, ev.Delimit)\n\ + \t\t\tgiven[ev.Arg.Key] = append(given[ev.Arg.Key], values...)\n\ \t\t\tswitch ev.Arg.Key {{" ); for e in commands { @@ -218,7 +219,7 @@ fn parse_fn(out: &mut String, commands: &[Emitted], assigned: &Fields) { for (arg, named) in &e.args { let field = &assigned[&named.key]; let assign = if arg.var { - format!("\t\t\t\t{owner}.{field} = append({owner}.{field}, ev.Value)") + format!("\t\t\t\t{owner}.{field} = append({owner}.{field}, values...)") } else { format!("\t\t\t\t{owner}.{field} = ev.Value") }; @@ -372,7 +373,7 @@ fn flag_assign(flag: &SpecFlag, owner: &str, field: &str) -> String { "int" => format!("\t\t\t\t{owner}.{field}++"), "bool" => format!("\t\t\t\t{owner}.{field} = !ev.Negated"), "[]string" => format!( - "\t\t\t\tif ev.HasValue {{\n\t\t\t\t\t{owner}.{field} = append({owner}.{field}, ev.Value)\n\t\t\t\t}}" + "\t\t\t\tif ev.HasValue {{\n\t\t\t\t\t{owner}.{field} = append({owner}.{field}, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...)\n\t\t\t\t}}" ), _ => format!("\t\t\t\t{owner}.{field} = ev.Value"), } diff --git a/lib/src/parse.rs b/lib/src/parse.rs index 498e91561..fb8a124fe 100644 --- a/lib/src/parse.rs +++ b/lib/src/parse.rs @@ -1317,7 +1317,16 @@ fn parse_partial_with_env( // no longer one value, and `choices` has to be asked about each. Judging // first rejects `src:docs` against a list that both halves are on, and // names the whole word rather than the half that was wrong. - let parts: Vec = match arg.delimiter { + let trailing_value = + seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic; + let suppress_trailing_delimiter = + out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values); + let delimiter = if suppress_trailing_delimiter && trailing_value { + None + } else { + arg.delimiter + }; + let parts: Vec = match delimiter { Some(delimiter) => w.split(delimiter).map(str::to_string).collect(), None => vec![w.clone()], }; diff --git a/lib/src/spec/cmd.rs b/lib/src/spec/cmd.rs index a2fb59825..14e2b1a9e 100644 --- a/lib/src/spec/cmd.rs +++ b/lib/src/spec/cmd.rs @@ -105,6 +105,9 @@ pub struct SpecCommand { /// Whether a bare invocation of this command shows its help. #[serde(skip_serializing_if = "is_false")] pub arg_required_else_help: bool, + /// Whether delimiter splitting is disabled after `--` or for an automatic trailing arg. + #[serde(skip_serializing_if = "is_false")] + pub dont_delimit_trailing_values: bool, /// Token that resets argument parsing, allowing multiple command invocations. /// e.g., `mise run lint ::: test ::: check` with restart_token=":::" #[serde(skip_serializing_if = "Option::is_none")] @@ -175,6 +178,7 @@ impl Default for SpecCommand { subcommand_required: false, external_subcommand: false, arg_required_else_help: false, + dont_delimit_trailing_values: false, restart_token: None, help: None, help_long: None, @@ -282,6 +286,9 @@ impl SpecCommand { "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?, "external_subcommand" => cmd.external_subcommand = v.ensure_bool()?, "arg_required_else_help" => cmd.arg_required_else_help = v.ensure_bool()?, + "dont_delimit_trailing_values" => { + cmd.dont_delimit_trailing_values = v.ensure_bool()? + } "hide" => cmd.hide = v.ensure_bool()?, "unknown_flags" => { let raw = v.ensure_string()?; @@ -410,6 +417,10 @@ impl SpecCommand { cmd.arg_required_else_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()? } + "dont_delimit_trailing_values" => { + cmd.dont_delimit_trailing_values = + child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()? + } "hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?, "effect" => { let arg = child.ensure_arg_len(1..=1)?.arg(0)?; @@ -521,6 +532,7 @@ impl SpecCommand { subcommand_required, external_subcommand, arg_required_else_help, + dont_delimit_trailing_values, restart_token, subcommands, complete, @@ -595,6 +607,7 @@ impl SpecCommand { self.subcommand_required = subcommand_required; self.external_subcommand = external_subcommand; self.arg_required_else_help = arg_required_else_help; + self.dont_delimit_trailing_values = dont_delimit_trailing_values; if effect.is_some() { self.effect = effect; } @@ -699,6 +712,7 @@ impl From<&SpecCommand> for KdlNode { subcommand_required, external_subcommand, arg_required_else_help, + dont_delimit_trailing_values, restart_token, unknown_flags, aliases, @@ -745,6 +759,10 @@ impl From<&SpecCommand> for KdlNode { node.entries_mut() .push(KdlEntry::new_prop("arg_required_else_help", true)); } + if *dont_delimit_trailing_values { + node.entries_mut() + .push(KdlEntry::new_prop("dont_delimit_trailing_values", true)); + } if let Some(restart_token) = &restart_token { node.entries_mut() .push(KdlEntry::new_prop("restart_token", restart_token.clone())); @@ -993,6 +1011,7 @@ impl From<&clap::Command> for SpecCommand { } spec.subcommand_required = cmd.is_subcommand_required_set(); spec.arg_required_else_help = cmd.is_arg_required_else_help_set(); + spec.dont_delimit_trailing_values = cmd.is_dont_delimit_trailing_values_set(); for subcmd in cmd.get_subcommands() { let mut scmd: SpecCommand = subcmd.into(); scmd.name = subcmd.get_name().to_string(); diff --git a/lib/src/spec/mod.rs b/lib/src/spec/mod.rs index 982405594..8016c4b99 100644 --- a/lib/src/spec/mod.rs +++ b/lib/src/spec/mod.rs @@ -301,6 +301,9 @@ impl Spec { "arg_required_else_help" => { schema.cmd.arg_required_else_help = node.arg(0)?.ensure_bool()?; } + "dont_delimit_trailing_values" => { + schema.cmd.dont_delimit_trailing_values = node.arg(0)?.ensure_bool()?; + } "example" => { let code = node.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?; let mut example = SpecExample::new(code.trim().to_string()); @@ -584,6 +587,11 @@ impl Display for Spec { node.push(KdlEntry::new(true)); nodes.push(node); } + if self.cmd.dont_delimit_trailing_values { + let mut node = KdlNode::new("dont_delimit_trailing_values"); + node.push(true); + nodes.push(node); + } if !self.usage.is_empty() { let mut node = KdlNode::new("usage"); node.push(string_entry(None, &self.usage));