diff --git a/src/help-generator_.toit b/src/help-generator_.toit index 6364b7f..77b3681 100644 --- a/src/help-generator_.toit +++ b/src/help-generator_.toit @@ -16,12 +16,17 @@ It finds the selected command and prints its help. */ help-command_ path/Path arguments/List --ui/Ui: command/Command := path.last + show-all := false for i := 0; i < arguments.size; i++: argument := arguments[i] if argument == "--": break - // Simply drop all options. + if argument == "--all": + show-all = true + continue + + // Simply drop all other options. if argument.starts-with "-": continue @@ -32,7 +37,24 @@ help-command_ path/Path arguments/List --ui/Ui: command = subcommand path += command - emit-help_ path --ui=ui + if show-all: + emit-help-all_ path --ui=ui + else: + emit-help_ path --ui=ui + +/** +Emits the help for all commands in the subtree rooted at the given command. + +The command is identified by the $path where the command is the last element. +*/ +emit-help-all_ path/Path --ui/Ui: + ui.emit --kind=Ui.RESULT + --structured=: + build-json-help-all_ path + --text=: + generator := HelpGenerator path + generator.build-all-commands + generator.to-string /** Emits the help for the given command. @@ -48,6 +70,35 @@ emit-help_ path/Path --ui/Ui: generator.build-all generator.to-string +build-json-help-all_ path/Path -> Map: + return build-json-command-tree_ path.last --is-root=(path.size == 1) + +build-json-command-tree_ command/Command --is-root/bool=false -> Map: + sub-list := [] + sorted := command.subcommands_.sort: | a/Command b/Command | a.name.compare-to b.name + sorted.do: | sub/Command | + if sub.is-hidden_: continue.do + sub-list.add (build-json-command-tree_ sub) + + if is-root: + has-help := false + has-completion := false + command.subcommands_.do: | sub/Command | + if sub.name == "help": has-help = true + sub.aliases_.do: if it == "help": has-help = true + if sub.name == "completion": has-completion = true + sub.aliases_.do: if it == "completion": has-completion = true + if not has-help: + sub-list.add {"name": "help", "help": "Show help for a command.", "subcommands": []} + if not has-completion: + sub-list.add {"name": "completion", "help": "Generate shell completion scripts.", "subcommands": []} + + return { + "name": command.name, + "help": command.short-help, + "subcommands": sub-list, + } + build-json-help_ path/Path -> Map: // Local block to build json objects for the given command. // Adds the converted json object to the out-map. @@ -254,6 +305,53 @@ class HelpGenerator: sorted-commands := commands-and-help.sort: | a/List b/List | a[0].compare-to b[0] write-table_ sorted-commands --indentation=2 + /** + Builds a hierarchical summary of all commands in the subtree. + + Recursively lists all subcommands with indentation showing nesting. + Hidden commands are excluded. At root level, auto-added 'help' and + 'completion' entries are included. + */ + build-all-commands -> none: + rows := [] + collect-commands-recursive_ command_ rows --indent=0 --is-root=is-root-command_ + if rows.is-empty: return + write-table_ rows + + /** + Collects all commands recursively into $rows as [indented-name, short-help] pairs. + */ + collect-commands-recursive_ command/Command rows/List --indent/int --is-root/bool=false -> none: + // Each entry is [name, help, subcommand-or-null]. + entries := [] + command.subcommands_.do: | subcommand/Command | + if subcommand.is-hidden_: continue.do + entries.add [subcommand.name, subcommand.short-help, subcommand] + + if is-root: + has-help := false + has-completion := false + command.subcommands_.do: | sub/Command | + if sub.name == "help": has-help = true + sub.aliases_.do: if it == "help": has-help = true + if sub.name == "completion": has-completion = true + sub.aliases_.do: if it == "completion": has-completion = true + if not has-help: + entries.add ["help", "Show help for a command.", null] + if not has-completion: + entries.add ["completion", "Generate shell completion scripts.", null] + + sorted := entries.sort: | a/List b/List | + (a[0] as string).compare-to (b[0] as string) + + prefix := " " * indent + sorted.do: | entry/List | + name := entry[0] as string + help-str := entry[1] as string + rows.add ["$prefix$name", help-str] + if entry[2]: + collect-commands-recursive_ (entry[2] as Command) rows --indent=(indent + 2) + /** Builds the local options section. diff --git a/tests/help_test.toit b/tests/help_test.toit index c27172c..4b6ea49 100644 --- a/tests/help_test.toit +++ b/tests/help_test.toit @@ -16,6 +16,7 @@ main: test-options test-examples test-short-help + test-help-all check-output expected/string [block]: ui := TestUi @@ -870,3 +871,66 @@ test-short-help: expected = "Test command." actual = cmd.short-help expect-equals expected actual + +test-help-all: + sub-sub := cli.Command "sub2" + --help="Even more nested info." + --run=:: null + + sub1 := cli.Command "sub1" + --help="Sub info." + --subcommands=[sub-sub] + + tool-foo := cli.Command "foo" + --help="The foo tool." + --run=:: null + + tool-bar := cli.Command "bar" + --help="The bar tool." + --run=:: null + + tool := cli.Command "tool" + --help="Tools for xyz." + --subcommands=[tool-foo, tool-bar] + + hidden-cmd := cli.Command "secret" + --help="Secret command." + --hidden + --run=:: null + + execute := cli.Command "execute" + --help="Runs toto." + --run=:: null + + root := cli.Command "root" + --help="Root command." + --subcommands=[tool, execute, sub1, hidden-cmd] + + expected := """ + completion Generate shell completion scripts. + execute Runs toto. + help Show help for a command. + sub1 Sub info. + sub2 Even more nested info. + tool Tools for xyz. + bar The bar tool. + foo The foo tool. + """ + check-output expected: | cli/cli.Cli | + root.run ["help", "--all"] --cli=cli --invoked-command="bin/app" + + // Test --all on a subcommand. + expected = """ + bar The bar tool. + foo The foo tool. + """ + check-output expected: | cli/cli.Cli | + root.run ["help", "--all", "tool"] --cli=cli --invoked-command="bin/app" + + // Test --all on a leaf command (no subcommands). + expected = "" + ui := TestUi + cli-obj := cli.Cli "test" --ui=ui + root.run ["help", "--all", "execute"] --cli=cli-obj --invoked-command="bin/app" + all-output := ui.stdout + ui.stderr + expect-equals (expected + "\n") all-output