feat(completion): fast zsh completion for large host caches

Cobra's generated zsh script spawns a subshell per candidate and sends
every host through _describe, so <Tab> hangs for seconds with thousands
of cached hosts. netssh now ships its own script: subcommands and flags
go through _describe, plain hostnames through compadd. 5000 hosts
complete in ~0.08 s instead of ~4 s.

Add completion.max_results to cap the number of offered hostnames and
sort host candidates for a stable order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sebastian UnterschützandClaude Opus 5.5 committed 2026-10-28 11:22:18 +01:00
1 parent 7f53b90266
commit 8fb7633783
5 files changed
+172 -18

No files matched your search

+1 -1
View File
@@ -83,7 +83,7 @@ func commandCompletionPath(shell, alias string) string {
case "bash":
return filepath.Join(os.Getenv("HOME"), ".local", "share", "bash-completion", "completions", alias)
case "zsh":
return filepath.Join(os.Getenv("HOME"), ".zfunc", "_"+alias)
return filepath.Join(ZshFuncDir(), "_"+alias)
default:
return filepath.Join(FishConfigDir(), "completions", alias+".fish")
}
+38 -2
View File
@@ -88,7 +88,7 @@ func TestCommandAliasCompletesServerNamesAcrossShells(t *testing.T) {
case "bash":
err = root.GenBashCompletionV2(&canonical, true)
case "zsh":
err = root.GenZshCompletion(&canonical)
_, err = canonical.WriteString(hook.ZshCompletionScript)
case "fish":
err = root.GenFishCompletion(&canonical, true)
}
@@ -149,7 +149,8 @@ compinit -u -D
alias j=netssh
autoload -Uz _j
# Capture matches without requiring an interactive ZLE completion widget.
_describe() { print -rl -- "${completions[@]}"; }
_describe() { print -rl -- "${(P)4}"; }
compadd() { print -rl -- "$@"; }
words=(j ser)
CURRENT=2
_j
@@ -169,3 +170,38 @@ complete --do-complete 'j ser'
})
}
}
func TestZshCompletionSplitsHostsAndCommands(t *testing.T) {
zsh, err := exec.LookPath("zsh")
if err != nil {
t.Skip("zsh unavailable")
}
dir := t.TempDir()
backend := "#!/bin/sh\nprintf 'web01\\nweb:02\\ncache\\tManage the cache\\n:36\\n'\n"
if err := os.WriteFile(filepath.Join(dir, "netssh"), []byte(backend), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "_netssh"), []byte(hook.ZshCompletionScript), 0o644); err != nil {
t.Fatal(err)
}
script := `compdef() { :; }
source "$DIR/_netssh"
_describe() { print -r -- "describe ${(P)4}"; }
compadd() { print -r -- "compadd $*"; }
_files() { print -r -- files; }
words=(netssh "")
CURRENT=2
_netssh
`
cmd := exec.Command(zsh, "-f", "-c", script)
cmd.Env = append(os.Environ(), "DIR="+dir, "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH"))
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%v\n%s", err, out)
}
// Directive 36 = NoFileComp|KeepOrder: unsorted host group, no file fallback.
want := "describe cache:Manage the cache\ncompadd -V hosts -X %Bhosts%b -- web01 web:02\n"
if string(out) != want {
t.Fatalf("got:\n%s\nwant:\n%s", out, want)
}
}
+99
View File
@@ -0,0 +1,99 @@
package hook
import (
"fmt"
"os"
"path/filepath"
)
// ZshCompletionScript replaces Cobra's generated zsh completion. Cobra's script
// spawns a subshell per candidate and passes every host through _describe,
// which takes tens of seconds for thousands of hosts. This script splits the
// candidates with parameter expansion only: entries with a description
// (subcommands, flags) go through _describe, plain hostnames through compadd.
//
// The file works both from fpath (autoloaded as _netssh) and when sourced.
const ZshCompletionScript = `#compdef netssh
compdef _netssh netssh
# netssh zsh completion — generated by netssh, do not edit.
_netssh() {
local -a lines described plain
local out directive=0 entry ret=1
local -a nospace
out=$(netssh __complete "${(@)words[2,CURRENT]}" 2>/dev/null) || return 1
lines=("${(@f)out}")
if [[ ${lines[-1]} == :<-> ]]; then
directive=${lines[-1]#:}
lines[-1]=()
fi
# Directive 1: error.
(( directive & 1 )) && return 1
lines=(${lines:#_activeHelp_*})
lines=(${lines:#})
# Directive 2: no trailing space.
(( directive & 2 )) && nospace=(-S '')
# Directive 8: the lines are file extensions to filter by.
if (( directive & 8 )); then
local -a globs
for entry in $lines; do globs+=("*.${entry}"); done
_files -g "(${(j:|:)globs})"
return
fi
# Directive 16: complete directories only.
if (( directive & 16 )); then
_files -/
return
fi
described=(${(M)lines:#*$'\t'*})
plain=(${lines:#*$'\t'*})
if (( $#described )); then
local -a items
for entry in $described; do
items+=("${${entry%%$'\t'*}//:/\\:}:${entry#*$'\t'}")
done
_describe -t commands 'netssh command' items $nospace && ret=0
fi
if (( $#plain )); then
# Directive 32: keep the order supplied by netssh.
if (( directive & 32 )); then
compadd -V hosts -X '%Bhosts%b' $nospace -- $plain && ret=0
else
compadd -J hosts -X '%Bhosts%b' $nospace -- $plain && ret=0
fi
fi
# Directive 4: no file completion as a fallback.
if (( ! $#lines && ! (directive & 4) )); then
_files && ret=0
fi
return ret
}
if [ "$funcstack[1]" = "_netssh" ]; then
_netssh "$@"
fi
`
// ZshCompletionNote tells the user how zsh picks up a completion file in dir.
func ZshCompletionNote(dir string) string {
dump := filepath.Join(zshDir(), ".zcompdump")
return fmt.Sprintf(`zsh loads it from fpath. The netssh block in your .zshrc adds %s
(netssh hook install). Otherwise add this before compinit:
fpath=(%s $fpath)
Then refresh the completion cache: rm -f %s* && exec zsh`, dir, dir, dump)
}
func zshDir() string {
if dir := os.Getenv("ZDOTDIR"); dir != "" {
return dir
}
return os.Getenv("HOME")
}
// ZshFuncDir is the user directory netssh installs zsh completion files into.
func ZshFuncDir() string {
return filepath.Join(os.Getenv("HOME"), ".zfunc")
}