clap subcommands

This commit is contained in:
atagen 2026-03-23 15:11:56 +11:00
parent adea668355
commit a2f207272a
2 changed files with 77 additions and 3 deletions

View file

@ -717,6 +717,65 @@ let parse_manpage_string contents =
| Some d -> d | None -> "" in
{ result with description }
(* --- Clap-style SUBCOMMAND section extraction --- *)
(* Manpages generated by clap (Rust) put each subcommand under its own
.SH SUBCOMMAND header with a Usage: line giving the name. *)
let extract_subcommand_sections contents =
let lines = String.split_on_char '\n' contents in
let classified = List.map classify_line lines in
(* Split into sections at .SH boundaries *)
let rec collect_sections acc current_name current_lines = function
| [] ->
let acc = match current_name with
| Some n -> (n, List.rev current_lines) :: acc
| None -> acc in
List.rev acc
| Macro ("SH", args) :: rest ->
let acc = match current_name with
| Some n -> (n, List.rev current_lines) :: acc
| None -> acc in
let name = String.uppercase_ascii (String.trim args) in
if name = "SUBCOMMAND" || name = "SUBCOMMANDS" then
collect_sections acc (Some name) [] rest
else
collect_sections acc None [] rest
| line :: rest ->
collect_sections acc current_name (line :: current_lines) rest
in
let sections = collect_sections [] None [] classified in
(* For each SUBCOMMAND section, extract name from Usage: line and parse entries *)
let usage_re = Str.regexp {|Usage: \([a-zA-Z0-9_-]+\)|} in
List.filter_map (fun (_header, section_lines) ->
(* Find subcommand name from Usage: line *)
let name = ref None in
let desc_lines = ref [] in
List.iter (fun line ->
if !name = None then
match line with
| Text s ->
if try ignore (Str.search_forward usage_re s 0); true
with Not_found -> false
then name := Some (Str.matched_group 1 s)
else desc_lines := s :: !desc_lines
| Macro (("TP" | "B" | "BI" | "BR"), args) ->
let s = strip_inline_macro_args args |> strip_groff_escapes |> String.trim in
if try ignore (Str.search_forward usage_re s 0); true
with Not_found -> false
then name := Some (Str.matched_group 1 s)
| _ -> ()
) section_lines;
match !name with
| None -> None
| Some subcmd_name ->
let entries = extract_entries section_lines in
let desc = String.concat " " (List.rev !desc_lines)
|> strip_groff_escapes |> String.trim in
(* Remove backtick quoting common in clap output *)
let desc = Str.global_replace (Str.regexp "`\\([^`]*\\)`") "\\1" desc in
Some (subcmd_name, desc, { entries; subcommands = []; positionals = []; description = desc })
) sections
let read_manpage_file path =
if Filename.check_suffix path ".gz" then begin
let ic = Gzip.open_in path in