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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/completion_.toit
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 10 additions & 4 deletions src/help-generator_.toit
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

/**
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions src/parser_.toit
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
69 changes: 69 additions & 0 deletions tests/command_group_options_test.toit
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions tests/help_test.toit
Original file line number Diff line number Diff line change
Expand Up @@ -982,12 +982,12 @@ test-command-group-help:
bin/app [<options>] [--] <source> [<arg>...]

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.
Expand All @@ -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"
Expand Down