improve native completion detection

This commit is contained in:
atagen 2026-03-27 18:35:26 +11:00
parent 7c6a37fd13
commit 9557fcc1c4

View file

@ -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.