diff --git a/bin/main.ml b/bin/main.ml index aeef9e9..e56a02d 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -281,6 +281,34 @@ let nix_wrapper_target path = end with _ -> None +(* detect nix bash/sh wrapper scripts that exec a real binary. + * nix sometimes generates small shell scripts (e.g. to set env vars like + * XDG_CONFIG_HOME) that exec the real binary. these look like: + * #!/nix/store/.../bash -e + * export FOO=... + * exec -a "$0" "/nix/store/.../bin/.foo-wrapped" "$@" + * we extract the exec target path and resolve through it. *) +let nix_script_wrapper_target path = + try + let real = Unix.realpath path in + let ic = open_in real in + let size = in_channel_length ic in + if size > 4096 then (close_in ic; None) + else begin + let contents = Bytes.create size in + really_input ic contents 0 size; close_in ic; + let contents = Bytes.to_string contents in + if not (contains_str contents "exec") then None + else + let re = Str.regexp "exec[ \t]+\\(-a[ \t]+\"\\$0\"[ \t]+\\)?\"?\\(/nix/store/[a-z0-9]+-[^\" \t\n]+/bin/[a-zA-Z0-9._-]+\\)\"?" in + try ignore (Str.search_forward re contents 0); + let target = Str.matched_group 2 contents in + let target = Unix.realpath target in + if Sys.file_exists target then Some target else None + with Not_found -> None + end + with _ -> None + (* heuristic filter for binary names that should never be indexed. * skips: empty names, "-", dotfiles, libraries (lib-prefix), daemon wrappers * (suffixes -daemon, -wrapped), shared objects (.so suffix), and names with no @@ -299,25 +327,37 @@ let skip_name name = * Try_native_and_help — try native nushell completion first, fall back to --help *) 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 + else if Hashtbl.mem scan "-h" then Try_help + else Skip + (* classify a binary to decide the indexing strategy. * decision tree: * 1. nushell builtin or bad name -> Skip * 2. not executable -> Skip - * 3. script (has shebang) -> Try_help (scripts can't have native completions) + * 3. script (has shebang) -> resolve through nix script wrapper if possible, + * otherwise Try_help * 4. elf binary containing "completion" -> Try_native_and_help * 5. elf binary containing "-h" -> Try_help - * 6. nix wrapper -> Try_help (the wrapper itself is just an exec shim) + * 6. nix c wrapper -> Try_help (the wrapper itself is just an exec shim) * 7. otherwise -> Skip (binary has no help infrastructure) *) let classify_binary bindir name = if is_nushell_builtin name || skip_name name then Skip else let path = Filename.concat bindir name in if not (is_executable path) then Skip - else if is_script path then Try_help + else if is_script path then + match nix_script_wrapper_target path with + | Some target -> + let cls = classify_elf target in + if cls <> Skip then cls else Try_help + | None -> Try_help else - let scan = elf_scan path ["-h"; "completion"] in - if Hashtbl.mem scan "completion" then Try_native_and_help - else if Hashtbl.mem scan "-h" then Try_help + let cls = classify_elf path in + if cls <> Skip then cls else if nix_wrapper_target path <> None then Try_help else Skip @@ -352,6 +392,7 @@ let try_native_completion bin_path = [bin_path; "--completion"; "nushell"]; [bin_path; "generate-completion"; "nushell"]; [bin_path; "--generate-completion"; "nushell"]; + [bin_path; "gen-completions"; "nushell"]; [bin_path; "shell-completions"; "nushell"]; ]