diff --git a/src/completion_.toit b/src/completion_.toit index ea4df4a..5912285 100644 --- a/src/completion_.toit +++ b/src/completion_.toit @@ -292,6 +292,11 @@ add-options-for-command_ command/Command named-options/Map short-options/Map: command.options_.do: | option/Option | named-options[option.name] = option if option.short-name: short-options[option.short-name] = option + if command is CommandGroup: + group := command as CommandGroup + group.commands_.options_.do: | option/Option | + named-options[option.name] = option + if option.short-name: short-options[option.short-name] = option /** Completes option names for the given $current-word. diff --git a/src/help-generator_.toit b/src/help-generator_.toit index d366c65..a96735c 100644 --- a/src/help-generator_.toit +++ b/src/help-generator_.toit @@ -128,6 +128,9 @@ build-json-help_ path/Path -> Map: for i := path.size - 2; i >= 0; i--: parent-command := path[i] extract-options.call parent-command global-options + if parent-command is CommandGroup: + group := parent-command as CommandGroup + extract-options.call group.commands_ global-options json-examples := command.examples_.map: | example/Example | { "description": example.description, @@ -190,6 +193,9 @@ class HelpGenerator: result := [] for i := 0; i < path_.size - 1; i++: result.add-all path_[i].options_ + if path_[i] is CommandGroup: + group := path_[i] as CommandGroup + result.add-all group.commands_.options_ return result /** @@ -362,7 +368,7 @@ class HelpGenerator: build-global-options -> none: build-options_ --title="Global options" global-options_ - build-options_ --title/string options/List --add-help/bool=false --rest/bool=false -> none: + build-options_ --title/string options/List --add-help/bool=false --rest/bool=false --indentation/int=2 -> none: if options.is-empty and not add-help: return if add-help: @@ -427,7 +433,7 @@ class HelpGenerator: ensure-vertical-space_ writeln_ "$title:" - write-table_ options-type-defaults-and-help --indentation=2 + write-table_ options-type-defaults-and-help --indentation=indentation /** Builds the examples section. @@ -703,10 +709,10 @@ class HelpGenerator: sorted-commands := commands-and-help.sort: | a/List b/List | a[0].compare-to b[0] write-table_ sorted-commands --indentation=4 - build-options_ --title=" Options" command.options_ --add-help + build-options_ --title=" Options" command.options_ --add-help --indentation=4 if not command.rest_.is-empty: - build-options_ --title=" Rest" command.rest_ --rest + build-options_ --title=" Rest" command.rest_ --rest --indentation=4 /** Builds a usage line for an inner command, with the given $indentation. diff --git a/src/parser_.toit b/src/parser_.toit index 5ceb360..e60a972 100644 --- a/src/parser_.toit +++ b/src/parser_.toit @@ -94,6 +94,19 @@ class Parser_: else: options[option.name] = option.default + // If this is a CommandGroup, also register the commands_ wrapper's + // options so they are available when dispatching to subcommands. + if new-command is CommandGroup: + group := new-command as CommandGroup + group.commands_.options_.do: | option/Option | + all-named-options[option.name] = option + if option.short-name: all-short-options[option.short-name] = option + group.commands_.options_.do: | option/Option | + if option.is-multi: + options[option.name] = [] + else: + options[option.name] = option.default + command = new-command if add-to-path: path += command diff --git a/tests/command_group_options_test.toit b/tests/command_group_options_test.toit new file mode 100644 index 0000000..72ab212 --- /dev/null +++ b/tests/command_group_options_test.toit @@ -0,0 +1,69 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import cli +import expect show * + +/** +Reproduces a bug where options on the commands_ wrapper of a CommandGroup + are not available to subcommands. + +When a subcommand is found on a CommandGroup, the parser goes directly + from the CommandGroup to the subcommand, skipping the commands_ wrapper. + Options defined on the wrapper (like --sdk-dir) are therefore never + registered, causing "No option named 'sdk-dir'" at runtime. +*/ + +main: + test-commands-option-accessible-from-subcommand + test-commands-option-accessible-with-value + +test-commands-option-accessible-from-subcommand: + sub-invoked := false + + commands-cmd := cli.Command "commands" + --options=[ + cli.Option "sdk-dir" --help="Path to the SDK.", + ] + + sub := cli.Command "run" + --help="Run a file." + --run=:: | invocation/cli.Invocation | + sub-invoked = true + // This line throws "No option named 'sdk-dir'". + sdk-dir := invocation["sdk-dir"] + expect-null sdk-dir + commands-cmd.add sub + + root := cli.CommandGroup "app" + --default=(cli.Command "default" + --rest=[cli.Option "source" --required] + --run=:: unreachable) + --commands=commands-cmd + + root.run ["run"] + expect sub-invoked + +test-commands-option-accessible-with-value: + captured-value := null + + commands-cmd := cli.Command "commands" + --options=[ + cli.Option "sdk-dir" --help="Path to the SDK.", + ] + + sub := cli.Command "run" + --help="Run a file." + --run=:: | invocation/cli.Invocation | + captured-value = invocation["sdk-dir"] + commands-cmd.add sub + + root := cli.CommandGroup "app" + --default=(cli.Command "default" + --rest=[cli.Option "source" --required] + --run=:: unreachable) + --commands=commands-cmd + + root.run ["--sdk-dir", "/my/sdk", "run"] + expect-equals "/my/sdk" captured-value diff --git a/tests/help_test.toit b/tests/help_test.toit index e7fb4b7..bac1fe9 100644 --- a/tests/help_test.toit +++ b/tests/help_test.toit @@ -982,12 +982,12 @@ test-command-group-help: bin/app [] [--] [...] Options: - -h, --help Show help for this command. - -O, --optimization-level int Set the optimization level. (default: 1) + -h, --help Show help for this command. + -O, --optimization-level int Set the optimization level. (default: 1) Rest: - arg string Arguments. (multi) - source string The source file. (required) + arg string Arguments. (multi) + source string The source file. (required) Subcommands: Use a subcommand. @@ -1001,8 +1001,8 @@ test-command-group-help: run Run a file. Options: - -h, --help Show help for this command. - -v, --verbose Be verbose. + -h, --help Show help for this command. + -v, --verbose Be verbose. """ check-output expected: | cli/cli.Cli | root.run ["--help"] --cli=cli --invoked-command="bin/app"