diff --git a/README.md b/README.md index 878df3e..c254a74 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Discovery is bounded and configurable: | Flag | Default | Limit | |---|---:|---| | `--max-depth` | `3` | deepest subcommand help level inspected (`0` runs only the root help) | -| `--max-commands` | `64` | total help commands run, including the root | +| `--max-commands` | `128` | total help commands run, including the root | | `--timeout` | `5s` | wall-clock limit for each help command | | `--max-output` | `4194304` | cumulative help bytes accepted across the tree | @@ -118,7 +118,9 @@ depth, command-count, and output policy. `--timeout` remains a global runtime override for all subprocess-based operations. It is deliberately conservative: only the three exact headings above and -indented `name description` rows are recognized as subcommands. Aliases, +indented `name description` rows are recognized as subcommands. In sections +that display a whole hierarchy, only the shallowest indentation is treated as +the current command's direct children. Aliases, multi-word command columns, missing descriptions, unrecognized headings, and unusual layouts are omitted rather than guessed. Positionals are never assumed to be files, and descriptions are rendered inert — help text can't inject code diff --git a/main.go b/main.go index ba0b501..380bb1c 100644 --- a/main.go +++ b/main.go @@ -582,7 +582,7 @@ func firstLine(s string) string { func (a *app) generate() *cobra.Command { var help []string var force bool - limits := discoveryLimits{MaxDepth: 3, MaxCommands: 64, MaxOutput: maxOutput} + limits := discoveryLimits{MaxDepth: 3, MaxCommands: 128, MaxOutput: maxOutput} c := &cobra.Command{Use: "generate TOOL", Args: cobra.ExactArgs(1), RunE: func(c *cobra.Command, x []string) error { if len(help) == 0 { help = []string{"--help"} @@ -828,6 +828,12 @@ func parseSubcommandSections(lines []string, command *parsedCommand) { if !subcommandHeadings[strings.TrimSpace(lines[i])] { continue } + type row struct { + indent int + sub parsedSubcommand + } + var rows []row + minIndent := int(^uint(0) >> 1) for i++; i < len(lines); i++ { line := lines[i] trimmed := strings.TrimSpace(line) @@ -842,7 +848,19 @@ func parseSubcommandSections(lines []string, command *parsedCommand) { if len(parts) != 2 || strings.ContainsAny(parts[0], " \t,") || !nameOK(parts[0]) || strings.TrimSpace(parts[1]) == "" { continue } - command.Subcommands = append(command.Subcommands, parsedSubcommand{parts[0], strings.TrimSpace(parts[1])}) + indent := len(line) - len(strings.TrimLeft(line, " \t")) + rows = append(rows, row{indent, parsedSubcommand{parts[0], strings.TrimSpace(parts[1])}}) + if indent < minIndent { + minIndent = indent + } + } + // Some CLIs print the full hierarchy in one section. Only rows at the + // section's shallowest indentation are direct children; deeper rows are + // discovered by asking their parent for help. + for _, r := range rows { + if r.indent == minIndent { + command.Subcommands = append(command.Subcommands, r.sub) + } } } } diff --git a/main_test.go b/main_test.go index da0febf..4b3f6ff 100644 --- a/main_test.go +++ b/main_test.go @@ -185,6 +185,27 @@ func TestParseRecognizedSubcommandSectionsOnly(t *testing.T) { } } +func TestParseSubcommandSectionUsesImmediateIndentationOnly(t *testing.T) { + help := `Commands: + orb Manage orbs + service Manage services + start Start a service + threads Manage threads + list List threads + +Options: + --version Print version +` + got, err := parseHelp([]byte(help)) + if err != nil { + t.Fatal(err) + } + want := []parsedSubcommand{{"orb", "Manage orbs"}, {"threads", "Manage threads"}} + if !reflect.DeepEqual(got.Subcommands, want) { + t.Fatalf("subcommands=%#v, want %#v", got.Subcommands, want) + } +} + func TestDiscoverHelpRecursesAndHonorsLimits(t *testing.T) { bin := t.TempDir() tool := filepath.Join(bin, "tool")