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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -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
Expand Down
22 changes: 20 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading