(* nushell.ml — generate nushell extern definitions from parsed help data. * * this module is the code generation backend. it takes a help_result (from * the parser or manpage modules) and produces nushell source code that * defines "extern" declarations — nushell's mechanism for teaching the shell * about external commands' flags and subcommands so it can offer completions. * * it also maintains a list of nushell's built-in commands to avoid generating * extern definitions that would shadow them. * * key responsibilities: * - deduplicating flag entries (same flag from multiple help sources) * - mapping parameter names to nushell types (path, int, string) * - formatting flags in nushell syntax: --flag(-f): type # description * - handling positional arguments with nushell's ordering constraints * - escaping special characters for nushell string literals *) open Parser module SSet = Set.Make(String) module SMap = Map.Make(String) module CSet = Set.Make(Char) (* nushell built-in commands and keywords — we must never generate extern * definitions for these because it would shadow nushell's own implementations. * this list is maintained manually and should be updated with new nushell releases. *) let nushell_builtins = [ "alias"; "all"; "ansi"; "any"; "append"; "ast"; "attr"; "bits"; "break"; "bytes"; "cal"; "cd"; "char"; "chunk-by"; "chunks"; "clear"; "collect"; "columns"; "commandline"; "compact"; "complete"; "config"; "const"; "continue"; "cp"; "date"; "debug"; "decode"; "def"; "default"; "describe"; "detect"; "do"; "drop"; "du"; "each"; "echo"; "encode"; "enumerate"; "error"; "every"; "exec"; "exit"; "explain"; "explore"; "export"; "export-env"; "extern"; "fill"; "filter"; "find"; "first"; "flatten"; "for"; "format"; "from"; "generate"; "get"; "glob"; "grid"; "group-by"; "hash"; "headers"; "help"; "hide"; "hide-env"; "histogram"; "history"; "http"; "if"; "ignore"; "input"; "insert"; "inspect"; "interleave"; "into"; "is-admin"; "is-empty"; "is-not-empty"; "is-terminal"; "items"; "job"; "join"; "keybindings"; "kill"; "last"; "length"; "let"; "let-env"; "lines"; "load-env"; "loop"; "ls"; "match"; "math"; "merge"; "metadata"; "mkdir"; "mktemp"; "module"; "move"; "mut"; "mv"; "nu-check"; "nu-highlight"; "open"; "overlay"; "panic"; "par-each"; "parse"; "path"; "plugin"; "port"; "prepend"; "print"; "ps"; "query"; "random"; "reduce"; "reject"; "rename"; "return"; "reverse"; "rm"; "roll"; "rotate"; "run-external"; "save"; "schema"; "scope"; "select"; "seq"; "shuffle"; "skip"; "sleep"; "slice"; "sort"; "sort-by"; "source"; "source-env"; "split"; "start"; "stor"; "str"; "sys"; "table"; "take"; "tee"; "term"; "timeit"; "to"; "touch"; "transpose"; "try"; "tutor"; "ulimit"; "umask"; "uname"; "uniq"; "uniq-by"; "unlet"; "update"; "upsert"; "url"; "use"; "values"; "version"; "view"; "watch"; "where"; "which"; "while"; "whoami"; "window"; "with-env"; "wrap"; "zip"; ] (* lazily constructed set for fast lookup *) let builtin_set = lazy (SSet.of_list nushell_builtins) let is_nushell_builtin cmd = SSet.mem cmd (Lazy.force builtin_set) (* deduplicate flag entries that refer to the same flag. * when the same flag appears multiple times (e.g. from overlapping manpage * sections or repeated help text), we keep the "best" version using a score: * - both short+long form: +10 (most informative) * - has a parameter: +5 * - description length bonus: up to +5 * * peculiarity: after deduplication by long name, we also remove standalone * short flags whose letter is already covered by a Both(short, long) entry. * this prevents emitting both "-v" and "--verbose(-v)" which nushell would * reject as a duplicate. the filtering preserves original ordering from the * help text. *) let dedup_entries entries = let key_of entry = match entry.switch with | Short c -> Printf.sprintf "-%c" c | Long l | Both (_, l) -> Printf.sprintf "--%s" l in let score entry = let sw = match entry.switch with Both _ -> 10 | _ -> 0 in let p = match entry.param with Some _ -> 5 | None -> 0 in let d = min 5 (String.length entry.desc / 10) in sw + p + d in let best = List.fold_left (fun acc e -> let k = key_of e in match SMap.find_opt k acc with | Some prev when score prev >= score e -> acc | _ -> SMap.add k e acc ) SMap.empty entries in let covered = SMap.fold (fun _ e acc -> match e.switch with | Both (c, _) -> CSet.add c acc | _ -> acc ) best CSet.empty in List.fold_left (fun (seen, acc) e -> let k = key_of e in if SSet.mem k seen then (seen, acc) else match e.switch with | Short c when CSet.mem c covered -> (seen, acc) | _ -> (SSet.add k seen, SMap.find k best :: acc) ) (SSet.empty, []) entries |> snd |> List.rev (* map parameter names to nushell types. * nushell's extern declarations use typed parameters, so we infer the type * from the parameter name. file/path-related names become "path" (enables * path completion), numeric names become "int", everything else is "string". *) let nushell_type_of_param = function | "FILE" | "file" | "PATH" | "path" | "DIR" | "dir" | "DIRECTORY" | "FILENAME" | "PATTERNFILE" -> "path" | "NUM" | "N" | "COUNT" | "NUMBER" | "int" | "INT" | "COLS" | "WIDTH" | "LINES" | "DEPTH" | "depth" -> "int" | _ -> "string" (* escape a string for use inside nushell double-quoted string literals. * only double quotes and backslashes need escaping in nushell's syntax. *) let escape_nu s = if not (String.contains s '"') && not (String.contains s '\\') then s else begin let buf = Buffer.create (String.length s + 4) in String.iter (fun c -> match c with | '"' -> Buffer.add_string buf "\\\"" | '\\' -> Buffer.add_string buf "\\\\" | _ -> Buffer.add_char buf c ) s; Buffer.contents buf end (* format a single flag entry as a nushell extern parameter line. * output examples: * " --verbose(-v) # increase verbosity" * " --output(-o): path # write output to file" * " -n: int # number of results" * * the description is right-padded to column 40 with a "# " comment prefix. * nushell's syntax for combined short+long is "--long(-s)". *) let format_flag entry = let name = match entry.switch with | Both (s, l) -> Printf.sprintf "--%s(-%c)" l s | Long l -> Printf.sprintf "--%s" l | Short s -> Printf.sprintf "-%c" s in let typed = match entry.param with | Some (Mandatory p) | Some (Optional p) -> ": " ^ nushell_type_of_param p | None -> "" in let flag = " " ^ name ^ typed in if String.length entry.desc = 0 then flag else let pad_len = max 1 (40 - String.length flag) in flag ^ String.make pad_len ' ' ^ "# " ^ entry.desc (* format a positional argument as a nushell extern parameter line. * nushell syntax: "...name: type" for variadic, "name?: type" for optional. * hyphens in names are converted to underscores (nushell identifiers can't * contain hyphens). *) let format_positional p = let name = String.map (function '-' -> '_' | c -> c) p.pos_name in let prefix = if p.variadic then "..." else "" in let suffix = if p.optional && not p.variadic then "?" else "" in let typ = nushell_type_of_param (String.uppercase_ascii p.pos_name) in Printf.sprintf " %s%s%s: %s" prefix name suffix typ (* enforce nushell's positional argument ordering rules: * 1. no required positional may follow an optional one * 2. at most one variadic ("rest") parameter is allowed * * if a required positional appears after an optional one, it's silently * promoted to optional. duplicate variadic params are dropped. *) let fixup_positionals positionals = 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 (* generate the full nushell extern block for a command. * produces output like: * export extern "git add" [ * ...pathspec?: path * --verbose(-v) # be verbose * --dry-run(-n) # dry run * ] * * subcommands that weren't resolved into their own full definitions get * stub externs with just a comment containing their description: * export extern "git stash" [ # stash changes * ] *) let extern_of cmd_name result = let entries = dedup_entries result.entries in let cmd = escape_nu cmd_name 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) -> Printf.sprintf "\nexport extern \"%s %s\" [ # %s\n]\n" cmd (escape_nu sc.name) (escape_nu sc.desc) ) result.subcommands in String.concat "" (main :: subs) (* public alias for extern_of *) let generate_extern = extern_of (* derive a nushell module name from a command name. * replaces non-alphanumeric characters with hyphens and appends "-completions". * e.g. "git" → "git-completions", "docker-compose" → "docker-compose-completions" *) let module_name_of cmd_name = let s = String.map (function | ('a'..'z' | 'A'..'Z' | '0'..'9' | '-' | '_') as c -> c | _ -> '-') cmd_name in s ^ "-completions" (* generate a complete nushell module wrapping the extern. * output: "module git-completions { ... }\n\nuse git-completions *\n" * the "use" at the end makes the extern immediately available. *) let generate_module cmd_name result = let m = module_name_of cmd_name in Printf.sprintf "module %s {\n%s}\n\nuse %s *\n" m (extern_of cmd_name result) m (* convenience wrapper: generate an extern from just a list of entries * (no subcommands, positionals, or description). used when we only have * flag data and nothing else. *) let generate_extern_from_entries cmd_name entries = generate_extern cmd_name { entries; subcommands = []; positionals = []; description = "" }