diff --git a/bin/main.ml b/bin/main.ml index 57b4bf6..d51d840 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -13,8 +13,8 @@ * * the indexing pipeline for each binary: * a. classify the binary (skip? try --help? try native completions?) - * b. if the tool has native nushell completion support, try various - * subcommand patterns ("completions nushell", "--completion nushell", etc.) + * b. if the tool has native nushell completion support, run --help and + * discover subcommands containing "complet", then try them with "nushell" * c. otherwise, run the tool with --help/-h and parse the output * d. recursively resolve subcommands (depth-limited to 5) * e. after binaries, parse manpages for any commands not yet covered @@ -255,7 +255,7 @@ let elf_scan path needles = (* detect nix-generated c wrapper scripts and extract the real binary path. * nix's makeCWrapper creates small c programs that set up the environment - * and exec the real binary. these wrappers won't contain "-h" or "completion" + * and exec the real binary. these wrappers won't contain "-h" or "complet" * in their own binary (they're just wrappers), so elf_scan would say "skip". * this function reads the wrapper source to find the actual /nix/store/.../bin/... * target path, so we can try --help on the real binary instead. @@ -330,8 +330,8 @@ type bin_class = Skip | Try_help | Try_native_and_help (* classify an elf binary path for indexing. *) let classify_elf path = - let scan = elf_scan path ["-h"; "completion"] in - if Hashtbl.mem scan "completion" then Try_native_and_help + let scan = elf_scan path ["-h"; "complet"] in + if Hashtbl.mem scan "complet" then Try_native_and_help else if Hashtbl.mem scan "-h" then Try_help else Skip @@ -341,7 +341,7 @@ let classify_elf path = * 2. not executable -> Skip * 3. script (has shebang) -> resolve through nix script wrapper if possible, * otherwise Try_help - * 4. elf binary containing "completion" -> Try_native_and_help + * 4. elf binary containing "complet" -> Try_native_and_help * 5. elf binary containing "-h" -> Try_help * 6. nix c wrapper -> Try_help (the wrapper itself is just an exec shim) * 7. otherwise -> Skip (binary has no help infrastructure) *) @@ -374,28 +374,68 @@ let num_cores () = close_in ic; max 1 !count with _ -> 4 +(* extract words from text that contain any of the given substrings. + * words are sequences of [a-zA-Z0-9_-] optionally prefixed with --. + * returns a deduplicated list. *) +let extract_matching_words text needles = + let len = String.length text in + let module SSet = Set.Make(String) in + let words = ref SSet.empty in + let i = ref 0 in + while !i < len do + while !i < len && not (text.[!i] >= 'a' && text.[!i] <= 'z' + || text.[!i] >= 'A' && text.[!i] <= 'Z' + || text.[!i] = '-') do + incr i + done; + let start = !i in + while !i < len && (text.[!i] >= 'a' && text.[!i] <= 'z' + || text.[!i] >= 'A' && text.[!i] <= 'Z' + || text.[!i] >= '0' && text.[!i] <= '9' + || text.[!i] = '-' || text.[!i] = '_') do + incr i + done; + if !i > start then begin + let word = String.sub text start (!i - start) in + let lower = String.lowercase_ascii word in + if List.exists (fun needle -> + try ignore (Str.search_forward (Str.regexp_string needle) lower 0); true + with Not_found -> false + ) needles then + words := SSet.add word !words + end + done; + SSet.elements !words + (* try to get native nushell completions from a binary. - * tries several common subcommand patterns that tools use for shell completions. - * returns the first one that produces valid nushell source code. - * the 500ms timeout is generous enough for most tools but prevents hangs. + * runs --help, scans the output for words containing completion-related + * substrings ("complet"), then tries each match as a subcommand or flag + * with "nushell" as the argument. * - * the patterns cover: cobra (go), clap (rust), click (python), and various - * ad-hoc implementations. *) + * this catches arbitrary patterns (completions, generate-completions, + * shell-completion, gen-completions, etc.) without maintaining a hardcoded + * list. the worst case is a few failed attempts before falling back to + * manpage/--help parsing. *) let try_native_completion bin_path = - List.find_map (fun args -> - match run_cmd args 500 with - | Some text when is_nushell_source text -> Some text - | _ -> None - ) [ - [bin_path; "completions"; "nushell"]; - [bin_path; "completion"; "nushell"]; - [bin_path; "--completions"; "nushell"]; - [bin_path; "--completion"; "nushell"]; - [bin_path; "generate-completion"; "nushell"]; - [bin_path; "--generate-completion"; "nushell"]; - [bin_path; "gen-completions"; "nushell"]; - [bin_path; "shell-completions"; "nushell"]; - ] + let help_text = match run_cmd [bin_path; "--help"] 500 with + | Some t -> t | None -> "" in + if help_text = "" then None + else + let candidates = extract_matching_words help_text ["complet"] in + List.find_map (fun word -> + let attempts = + if String.starts_with ~prefix:"--" word then + [[bin_path; word; "nushell"]] + else + [[bin_path; word; "nushell"]; + [bin_path; "--" ^ word; "nushell"]] + in + List.find_map (fun args -> + match run_cmd args 500 with + | Some text when is_nushell_source text -> Some text + | _ -> None + ) attempts + ) candidates (* parse a manpage file, extracting the command name, its flags/subcommands, * and any clap-style per-subcommand sections.