diff --git a/bin/main.ml b/bin/main.ml index 4bda8b5..5fd294e 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -165,6 +165,7 @@ let skip_name name = || String.ends_with ~suffix:"-daemon" name || String.ends_with ~suffix:"-wrapped" name || String.ends_with ~suffix:".so" name + || not (String.exists (fun c -> (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) name) type bin_class = Skip | Try_help | Try_native_and_help diff --git a/lib/manpage.ml b/lib/manpage.ml index 7e7c5c6..552c246 100644 --- a/lib/manpage.ml +++ b/lib/manpage.ml @@ -560,105 +560,108 @@ let mdoc_text_of line = if s = "" then None else Some s) | _ -> None +let parse_mdoc_it args = + let words = String.split_on_char ' ' args + |> List.filter (fun w -> w <> "" && w <> "Ns") in + let param = match words with + | _ :: _ :: "Ar" :: p :: _ -> Some (Mandatory p) + | _ -> None + in + match words with + | "Fl" :: c :: _ when String.length c = 1 && is_alphanumeric c.[0] -> + Some { switch = Short c.[0]; param; desc = "" } + | "Fl" :: name :: _ when String.length name > 1 && name.[0] = '-' -> + Some { switch = Long (String.sub name 1 (String.length name - 1)); param; desc = "" } + | _ -> None + +let positional_of_mdoc_line optional args = + let words = String.split_on_char ' ' args + |> List.filter (fun w -> w <> "") in + match words with + | name :: _ when String.length name >= 2 -> + Some { pos_name = String.lowercase_ascii name; + optional; variadic = List.mem "..." words } + | _ -> None + let parse_mdoc_lines lines = let classified = List.map classify_line lines in - let entries = ref [] in - let positionals = ref [] in let rec skip_to_el = function | [] -> [] | Macro ("El", _) :: rest -> rest | _ :: rest -> skip_to_el rest in - let collect_desc rest = - let rec go acc = function - | [] -> (acc, []) - | (Macro ("It", _) | Macro ("El", _) - | Macro ("Sh", _) | Macro ("Ss", _)) :: _ as rest -> (acc, rest) - | line :: rest -> - go (match mdoc_text_of line with Some s -> s :: acc | None -> acc) rest - in - let parts, rest = go [] rest in + let rec collect_desc acc = function + | [] -> (acc, []) + | (Macro ("It", _) | Macro ("El", _) + | Macro ("Sh", _) | Macro ("Ss", _)) :: _ as rest -> (acc, rest) + | line :: rest -> + collect_desc (match mdoc_text_of line with Some s -> s :: acc | None -> acc) rest + in + let desc_of rest = + let parts, rest = collect_desc [] rest in (String.concat " " (List.rev parts) |> String.trim, rest) in - let parse_mdoc_it args = - let words = String.split_on_char ' ' args - |> List.filter (fun w -> w <> "" && w <> "Ns") in - match words with - | "Fl" :: c :: _ when String.length c = 1 && is_alphanumeric c.[0] -> - let param = match words with - | _ :: _ :: "Ar" :: p :: _ -> Some (Mandatory p) - | _ -> None - in - Some { switch = Short c.[0]; param; desc = "" } - | "Fl" :: name :: _ when String.length name > 1 && name.[0] = '-' -> - let long = String.sub name 1 (String.length name - 1) in - let param = match words with - | _ :: _ :: "Ar" :: p :: _ -> Some (Mandatory p) - | _ -> None - in - Some { switch = Long long; param; desc = "" } - | _ -> None + let parse_it args rest entries = + let desc, rest = desc_of rest in + let entries = match parse_mdoc_it args with + | Some e -> { e with desc } :: entries + | None -> entries + in + (entries, rest) in - let rec parse_option_list = function - | [] -> [] - | Macro ("El", _) :: rest -> rest + let rec parse_option_list entries = function + | [] -> (entries, []) + | Macro ("El", _) :: rest -> (entries, rest) | Macro ("It", args) :: rest -> - let desc, rest = collect_desc rest in - (match parse_mdoc_it args with - | Some e -> entries := { e with desc } :: !entries - | None -> ()); - parse_option_list rest - | _ :: rest -> parse_option_list rest + let entries, rest = parse_it args rest entries in + parse_option_list entries rest + | _ :: rest -> parse_option_list entries rest in - let rec scan = function - | [] -> () + let rec scan entries positionals = function + | [] -> (entries, positionals) | Macro ("Bl", _) :: Macro ("It", it_args) :: rest -> let words = String.split_on_char ' ' it_args |> List.filter (fun w -> w <> "") in - if (match words with "Fl" :: _ -> true | _ -> false) then begin - let desc, rest = collect_desc rest in - (match parse_mdoc_it it_args with - | Some e -> entries := { e with desc } :: !entries - | None -> ()); - scan (parse_option_list rest) - end else - scan (skip_to_el rest) - | Macro ("Bl", _) :: rest -> scan (skip_to_el rest) + if (match words with "Fl" :: _ -> true | _ -> false) then + let entries, rest = parse_it it_args rest entries in + let entries, rest = parse_option_list entries rest in + scan entries positionals rest + else + scan entries positionals (skip_to_el rest) + | Macro ("Bl", _) :: rest -> scan entries positionals (skip_to_el rest) | Macro ("Sh", args) :: rest when String.uppercase_ascii (String.trim args) = "SYNOPSIS" -> - scan (parse_synopsis rest) - | _ :: rest -> scan rest - and parse_synopsis = function - | [] -> [] - | Macro ("Sh", _) :: _ as rest -> rest + let positionals, rest = parse_synopsis positionals rest in + scan entries positionals rest + | _ :: rest -> scan entries positionals rest + and parse_synopsis positionals = function + | [] -> (positionals, []) + | Macro ("Sh", _) :: _ as rest -> (positionals, rest) | Macro ("Ar", args) :: rest -> - let words = String.split_on_char ' ' args - |> List.filter (fun w -> w <> "") in - (match words with - | name :: _ when String.length name >= 2 -> - let variadic = List.mem "..." words in - positionals := { pos_name = String.lowercase_ascii name; - optional = false; variadic } :: !positionals - | _ -> ()); - parse_synopsis rest + let positionals = match positional_of_mdoc_line false args with + | Some p -> p :: positionals | None -> positionals in + parse_synopsis positionals rest | Macro ("Op", args) :: rest -> let words = String.split_on_char ' ' args |> List.filter (fun w -> w <> "") in - (match words with - | "Ar" :: name :: _ when String.length name >= 2 -> - let variadic = List.mem "..." words in - positionals := { pos_name = String.lowercase_ascii name; - optional = true; variadic } :: !positionals - | _ -> ()); - parse_synopsis rest - | _ :: rest -> parse_synopsis rest + let positionals = match words with + | "Ar" :: _ -> + (match positional_of_mdoc_line true args with + | Some p -> p :: positionals | None -> positionals) + | _ -> positionals in + parse_synopsis positionals rest + | _ :: rest -> parse_synopsis positionals rest in - scan classified; - let seen = Hashtbl.create 8 in - let positionals = List.rev !positionals |> List.filter (fun p -> - if Hashtbl.mem seen p.pos_name then false - else (Hashtbl.replace seen p.pos_name true; true)) in - { entries = List.rev !entries; subcommands = []; positionals } + let entries, positionals = scan [] [] classified in + let positionals = + List.rev positionals + |> List.fold_left (fun (seen, acc) p -> + if List.mem p.pos_name seen then (seen, acc) + else (p.pos_name :: seen, p :: acc) + ) ([], []) + |> snd |> List.rev + in + { entries = List.rev entries; subcommands = []; positionals } (* --- Top-level API --- *) diff --git a/lib/nushell.ml b/lib/nushell.ml index 688a879..d976806 100644 --- a/lib/nushell.ml +++ b/lib/nushell.ml @@ -122,10 +122,24 @@ let format_positional p = let typ = nushell_type_of_param (String.uppercase_ascii p.pos_name) in Printf.sprintf " %s%s%s: %s" prefix name suffix typ +let fixup_positionals positionals = + (* Nushell rules: no required after optional, only one rest param *) + List.fold_left (fun (saw_opt, saw_rest, acc) p -> + if p.variadic then + if saw_rest then (saw_opt, saw_rest, acc) + else (true, true, p :: acc) + else if saw_opt then + (true, saw_rest, { p with optional = true } :: acc) + else + (p.optional, saw_rest, p :: acc) + ) (false, false, []) positionals + |> fun (_, _, acc) -> List.rev acc + let extern_of cmd_name result = let entries = dedup_entries result.entries in let cmd = escape_nu cmd_name in - let pos_lines = List.map (fun p -> format_positional p ^ "\n") result.positionals in + let positionals = fixup_positionals result.positionals in + let pos_lines = List.map (fun p -> format_positional p ^ "\n") positionals in let flags = List.map (fun e -> format_flag e ^ "\n") entries in let main = Printf.sprintf "export extern \"%s\" [\n%s%s]\n" cmd (String.concat "" pos_lines) (String.concat "" flags) in let subs = List.map (fun (sc : subcommand) -> diff --git a/lib/parser.ml b/lib/parser.ml index ffa91ec..efe3087 100644 --- a/lib/parser.ml +++ b/lib/parser.ml @@ -257,7 +257,15 @@ let help_parser = in many (choice [ try_entry; try_subcommand; try_skip ]) >>| fun items -> let entries = List.filter_map (function `Entry e -> Some e | _ -> None) items in - let subcommands = List.filter_map (function `Subcommand sc -> Some sc | _ -> None) items in + let subcommands = + List.filter_map (function `Subcommand sc -> Some sc | _ -> None) items + |> List.fold_left (fun acc sc -> + match List.assoc_opt sc.name acc with + | Some prev when String.length prev.desc >= String.length sc.desc -> acc + | _ -> (sc.name, sc) :: List.remove_assoc sc.name acc + ) [] + |> List.rev_map snd + in { entries; subcommands; positionals = [] }) let skip_command_prefix s = @@ -390,10 +398,12 @@ let parse_usage_args s = | _ -> incr i done; - let seen = Hashtbl.create 8 in - List.rev !results |> List.filter (fun p -> - if Hashtbl.mem seen p.pos_name then false - else (Hashtbl.replace seen p.pos_name true; true)) + List.rev !results + |> List.fold_left (fun (seen, acc) p -> + if List.mem p.pos_name seen then (seen, acc) + else (p.pos_name :: seen, p :: acc) + ) ([], []) + |> snd |> List.rev let extract_usage_positionals text = let lines = String.split_on_char '\n' text in