From cd43d96388c5d53011f8cd1a6820f2fbbffda0ba Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Tue, 14 Apr 2026 23:55:40 +0200 Subject: [PATCH] Add --dash-dash-is-rest option to Command Allow commands to treat '--' as a regular rest argument instead of an option/rest separator. When set, '--' is added to rest args and option parsing continues for subsequent arguments. --- src/cli.toit | 29 +++++++++++++++++-------- src/completion_.toit | 9 +++++++- src/help-generator_.toit | 2 +- src/parser_.toit | 9 +++++--- tests/dashdash_test.toit | 47 +++++++++++++++++++++++++++++++--------- 5 files changed, 72 insertions(+), 24 deletions(-) diff --git a/src/cli.toit b/src/cli.toit index 63b6b20..ac492b9 100644 --- a/src/cli.toit +++ b/src/cli.toit @@ -193,6 +193,16 @@ class Command: /** The rest arguments. */ rest_/List + /** + Whether '--' is treated as a regular rest argument. + + When false (the default), '--' stops option parsing and all subsequent + arguments become rest arguments. + When true, '--' is treated as a normal rest argument and option parsing + continues for subsequent arguments. + */ + dash-dash-is-rest_/bool + /** Whether this command should show up in the help. */ is-hidden_/bool @@ -227,44 +237,44 @@ class Command: */ constructor name --usage/string?=null --help/string?=null --examples/List=[] \ --aliases/List=[] --options/List=[] --rest/List=[] --subcommands/List=[] --hidden/bool=false \ - --run/Lambda?=null: + --dash-dash-is-rest/bool=false --run/Lambda?=null: return Command.private name --usage=usage --help=help --examples=examples \ --aliases=aliases --options=options --rest=rest --subcommands=subcommands --hidden=hidden \ - --run=run + --dash-dash-is-rest=dash-dash-is-rest --run=run /** Deprecated. Use '--help' instead of '--short-help'. */ constructor name --usage/string?=null --short-help/string --examples/List=[] \ --aliases/List=[] --options/List=[] --rest/List=[] --subcommands/List=[] --hidden/bool=false \ - --run/Lambda?=null: + --dash-dash-is-rest/bool=false --run/Lambda?=null: return Command.private name --usage=usage --short-help=short-help --examples=examples \ --aliases=aliases --options=options --rest=rest --subcommands=subcommands --hidden=hidden \ - --run=run + --dash-dash-is-rest=dash-dash-is-rest --run=run /** Deprecated. Use '--help' instead of '--long-help'. */ constructor name --usage/string?=null --long-help/string --examples/List=[] \ --aliases/List=[] --options/List=[] --rest/List=[] --subcommands/List=[] --hidden/bool=false \ - --run/Lambda?=null: + --dash-dash-is-rest/bool=false --run/Lambda?=null: return Command.private name --usage=usage --help=long-help --examples=examples \ --aliases=aliases --options=options --rest=rest --subcommands=subcommands --hidden=hidden \ - --run=run + --dash-dash-is-rest=dash-dash-is-rest --run=run /** Deprecated. Use '--help' with a meaningful first paragraph instead of '--short-help' and '--long-help'. */ constructor name --usage/string?=null --short-help/string --long-help/string --examples/List=[] \ --aliases/List=[] --options/List=[] --rest/List=[] --subcommands/List=[] --hidden/bool=false \ - --run/Lambda?=null: + --dash-dash-is-rest/bool=false --run/Lambda?=null: return Command.private name --usage=usage --short-help=short-help --help=long-help --examples=examples \ --aliases=aliases --options=options --rest=rest --subcommands=subcommands --hidden=hidden \ - --run=run + --dash-dash-is-rest=dash-dash-is-rest --run=run constructor.private .name --usage/string?=null --short-help/string?=null --help/string?=null --examples/List=[] \ --aliases/List=[] --options/List=[] --rest/List=[] --subcommands/List=[] --hidden/bool=false \ - --run/Lambda?=null: + --dash-dash-is-rest/bool=false --run/Lambda?=null: usage_ = usage short-help_ = short-help help_ = help @@ -272,6 +282,7 @@ class Command: aliases_ = aliases options_ = options rest_ = rest + dash-dash-is-rest_ = dash-dash-is-rest subcommands_ = subcommands run-callback_ = run is-hidden_ = hidden diff --git a/src/completion_.toit b/src/completion_.toit index 5912285..f489236 100644 --- a/src/completion_.toit +++ b/src/completion_.toit @@ -115,7 +115,14 @@ complete_ root/Command arguments/List -> CompletionResult_: all-named-options.clear all-short-options.clear add-options-for-command_ current-command all-named-options all-short-options - past-dashdash = true + if not current-command.dash-dash-is-rest_: + past-dashdash = true + continue.repeat + // Treat "--" as a regular rest argument. + rest-option := rest-option-for-index_ current-command positional-index + if rest-option: + (seen-options.get rest-option.name --init=:[]).add arg + positional-index++ continue.repeat if arg.starts-with "--": diff --git a/src/help-generator_.toit b/src/help-generator_.toit index a96735c..3858217 100644 --- a/src/help-generator_.toit +++ b/src/help-generator_.toit @@ -744,7 +744,7 @@ class HelpGenerator: write-usage-suffix_ command/Command --has-more-options/bool -> none: if not command.subcommands_.is-empty: write_ " " if has-more-options: write_ " []" - if not command.rest_.is-empty: write_ " [--]" + if not command.rest_.is-empty and not command.dash-dash-is-rest_: write_ " [--]" command.rest_.do: | option/Option | type := option.type option-str/string := ? diff --git a/src/parser_.toit b/src/parser_.toit index e60a972..47909d8 100644 --- a/src/parser_.toit +++ b/src/parser_.toit @@ -124,10 +124,13 @@ class Parser_: remaining := ["--"] + arguments[index..] parse group.default_ remaining block return - rest.add-all arguments[index ..] - break // We're done! + if not command.dash-dash-is-rest_: + rest.add-all arguments[index ..] + break // We're done! + // Treat "--" as a regular rest argument. + rest.add argument - if argument.starts-with "--": + else if argument.starts-with "--": value/string? := null // Get the option name. split := argument.index-of "=" diff --git a/tests/dashdash_test.toit b/tests/dashdash_test.toit index b6a2813..dce0b20 100644 --- a/tests/dashdash_test.toit +++ b/tests/dashdash_test.toit @@ -6,16 +6,43 @@ import cli import expect show * main: + test-dashdash-separator + test-no-allow-dashdash + test-no-allow-dashdash-with-options + +test-dashdash-separator: root := cli.Command "root" - --rest=[ - cli.Option "first" --required, - cli.Option "arg" --multi - ] - --run=:: test-dashdash it + --rest=[ + cli.Option "first" --required, + cli.Option "arg" --multi, + ] + --run=:: | invocation/cli.Invocation | + expect-equals "prog" invocation["first"] + expect-list-equals ["arg1", "arg2", "arg3"] invocation["arg"] root.run ["--", "prog", "arg1", "arg2", "arg3"] -test-dashdash invocation/cli.Invocation: - first := invocation["first"] - rest := invocation["arg"] - expect-equals "prog" first - expect-list-equals ["arg1", "arg2", "arg3"] rest +test-no-allow-dashdash: + // When --allow-dash-dash is false, "--" becomes a regular rest argument. + root := cli.Command "root" + --dash-dash-is-rest + --rest=[ + cli.Option "arg" --multi, + ] + --run=:: | invocation/cli.Invocation | + expect-list-equals ["--", "foo", "bar"] invocation["arg"] + root.run ["--", "foo", "bar"] + +test-no-allow-dashdash-with-options: + // Options after "--" are still parsed when allow-dash-dash is false. + root := cli.Command "root" + --dash-dash-is-rest + --options=[ + cli.Flag "verbose" --short-name="v", + ] + --rest=[ + cli.Option "arg" --multi, + ] + --run=:: | invocation/cli.Invocation | + expect invocation["verbose"] + expect-list-equals ["--", "foo"] invocation["arg"] + root.run ["--", "foo", "--verbose"]