diff --git a/bin/main.ml b/bin/main.ml index d51d840..fffd493 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -675,9 +675,18 @@ let help_resolve_par ?(timeout=200) ?(mandirs=[]) cmd rest name = | Error _ -> None | Ok r when r.entries = [] && r.subcommands = [] && r.positionals = [] -> None | Ok r -> - let at_limit = depth >= 5 in - let subs = if at_limit then [] else r.subcommands in - Some (r, subs, [])) + let self_listed = match cmd_args with + | [] -> false + | _ -> + let leaf = List.nth cmd_args (List.length cmd_args - 1) in + List.exists (fun (sc : subcommand) -> sc.name = leaf) r.subcommands in + if self_listed then + Some ({ entries = []; subcommands = []; positionals = []; + description = "" }, [], []) + else + let at_limit = depth >= 5 in + let subs = if at_limit then [] else r.subcommands in + Some (r, subs, [])) else match parse_help text with | Error _ -> None @@ -688,7 +697,13 @@ let help_resolve_par ?(timeout=200) ?(mandirs=[]) cmd rest name = | _ -> let leaf = List.nth cmd_args (List.length cmd_args - 1) in List.exists (fun (sc : subcommand) -> sc.name = leaf) r.subcommands in - if self_listed then None + if self_listed then + (* the subcommand's --help returned the parent's help text + * (it lists itself as a subcommand). cache a leaf stub so the + * completer knows this is a leaf node, not a parent with + * further subcommands. *) + Some ({ entries = []; subcommands = []; positionals = []; + description = "" }, [], []) else let at_limit = depth >= 5 in let subs = if at_limit then [] else r.subcommands in @@ -871,7 +886,22 @@ let cmd_index bindirs mandirs ignorelist help_only dir = done_cmds := SSet.add sub_cmd !done_cmds; incr result_count end - ) subs + ) subs; + (* for COMMANDS section subcommands (e.g. systemctl start/stop), + * write leaf stubs so the completer treats them as leaf nodes + * rather than falling back to the parent's flags/subcommands. + * only when there are no clap-style sub-sections (subs = []), + * meaning the subcommands came from the COMMANDS section. + * deliberately not added to done_cmds — if a per-subcommand + * manpage exists (e.g. docker-start.1), it will overwrite the stub. *) + if subs = [] then + List.iter (fun (sc : subcommand) -> + let sub_cmd = cmd ^ " " ^ sc.name in + if not (SSet.mem sub_cmd !done_cmds) then + write_result ~dir ~source:"manpage" sub_cmd + { entries = []; subcommands = []; positionals = []; + description = sc.desc } + ) result.subcommands ) files end ) command_sections diff --git a/test/test_inshellah.ml b/test/test_inshellah.ml index 55567f3..bb36889 100644 --- a/test/test_inshellah.ml +++ b/test/test_inshellah.ml @@ -436,6 +436,70 @@ Use \fB\-\-verbose\fR to see more. let verbose_lines = List.filter (fun l -> contains l "verbose") lines in check "only one verbose line" (List.length verbose_lines = 1) +let test_commands_section_subcommands () = + Printf.printf "\n== COMMANDS section subcommand extraction ==\n"; + (* manpages like systemctl have a COMMANDS section with bold command names + * inside .PP + .RS/.RE blocks. these should be extracted as subcommands + * and treated as leaf nodes (no entries of their own). *) + let groff = {|.SH OPTIONS +.TP +\fB\-\-user\fR +Talk to the service manager of the calling user. +.TP +\fB\-\-system\fR +Talk to the service manager of the system. +.SH COMMANDS +.PP +\fBstart\fR \fIUNIT\fR\&... +.RS 4 +Start (activate) one or more units. +.RE +.PP +\fBstop\fR \fIUNIT\fR\&... +.RS 4 +Stop (deactivate) one or more units. +.RE +.PP +\fBreload\fR \fIUNIT\fR\&... +.RS 4 +Asks all units to reload their configuration. +.RE +.SH SEE ALSO +|} in + let result = parse_manpage_string groff in + check "has options entries" (List.length result.entries = 2); + check "has subcommands" (List.length result.subcommands = 3); + let sc_names = List.map (fun (sc : subcommand) -> sc.name) result.subcommands in + check "has start" (List.mem "start" sc_names); + check "has stop" (List.mem "stop" sc_names); + check "has reload" (List.mem "reload" sc_names); + (* verify subcommand descriptions are extracted *) + let start_sc = List.find (fun (sc : subcommand) -> sc.name = "start") result.subcommands in + check "start has desc" (String.length start_sc.desc > 0) + +let test_self_listing_detection () = + Printf.printf "\n== Self-listing subcommand detection ==\n"; + (* when a subcommand's --help shows the parent's help text, + * the subcommand name appears in its own subcommand list. + * the parser should detect this — tested via parse_help. *) + let help_text = {|systemctl [OPTIONS...] COMMAND ... + +Unit Commands: + start UNIT... Start (activate) one or more units + stop UNIT... Stop (deactivate) one or more units + status [PATTERN...] Show runtime status + +Options: + --user Talk to the user service manager + --system Talk to the system service manager +|} in + let r = parse help_text in + let has_start = List.exists (fun (sc : subcommand) -> sc.name = "start") r.subcommands in + check "detected start as subcommand" has_start; + (* the self-listing logic (in main.ml) would check: is "start" in r.subcommands? + * here we just verify the parser extracts it correctly. *) + check "has entries too" (List.length r.entries >= 2) + let test_font_boundary_spacing () = Printf.printf "\n== Font boundary spacing ==\n"; (* \fB--max-results\fR\fIcount\fR should become "--max-results count" *) @@ -488,5 +552,9 @@ let () = test_dedup_manpage (); test_font_boundary_spacing (); + Printf.printf "\nRunning COMMANDS section tests...\n"; + test_commands_section_subcommands (); + test_self_listing_detection (); + Printf.printf "\n=== Results: %d passed, %d failed ===\n" !passes !failures; if !failures > 0 then exit 1