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:
1 parent
7f53b90266
commit
8fb7633783
5 files changed
+172
-18
No files matched your search
+21
-8
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -311,8 +312,13 @@ func rootCmd() *cobra.Command {
|
|||||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||||
_ = c.Load()
|
_ = c.Load()
|
||||||
|
|
||||||
var completions []cobra.Completion
|
hosts := c.Search(toComplete)
|
||||||
for _, e := range c.Search(toComplete) {
|
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
|
||||||
|
if limit := cfg.Completion.MaxResults; limit > 0 && len(hosts) > limit {
|
||||||
|
hosts = hosts[:limit]
|
||||||
|
}
|
||||||
|
completions := make([]cobra.Completion, 0, len(hosts))
|
||||||
|
for _, e := range hosts {
|
||||||
completions = append(completions, cobra.Completion(e.Name))
|
completions = append(completions, cobra.Completion(e.Name))
|
||||||
}
|
}
|
||||||
// Subcommands at the end, after all hostnames.
|
// Subcommands at the end, after all hostnames.
|
||||||
@@ -332,6 +338,17 @@ func rootCmd() *cobra.Command {
|
|||||||
for _, cmd := range root.Commands() {
|
for _, cmd := range root.Commands() {
|
||||||
if cmd.Name() == "completion" {
|
if cmd.Name() == "completion" {
|
||||||
cmd.AddCommand(completionInstallCmd(root))
|
cmd.AddCommand(completionInstallCmd(root))
|
||||||
|
// Cobra's zsh script spawns a subshell per line and sends every host
|
||||||
|
// through _describe, which hangs with thousands of cached hosts.
|
||||||
|
for _, sub := range cmd.Commands() {
|
||||||
|
if sub.Name() == "zsh" {
|
||||||
|
sub.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
_, err := io.WriteString(cmd.OutOrStdout(), hook.ZshCompletionScript)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sub.Run = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,12 +386,8 @@ func completionInstallCmd(root *cobra.Command) *cobra.Command {
|
|||||||
case "zsh":
|
case "zsh":
|
||||||
dir = filepath.Join(os.Getenv("HOME"), ".zfunc")
|
dir = filepath.Join(os.Getenv("HOME"), ".zfunc")
|
||||||
file = filepath.Join(dir, "_netssh")
|
file = filepath.Join(dir, "_netssh")
|
||||||
gen = func() ([]byte, error) {
|
gen = func() ([]byte, error) { return []byte(hook.ZshCompletionScript), nil }
|
||||||
var buf strings.Builder
|
note = hook.ZshCompletionNote(dir)
|
||||||
err := root.GenZshCompletion(&buf)
|
|
||||||
return []byte(buf.String()), err
|
|
||||||
}
|
|
||||||
note = "Make sure ~/.zfunc is in your fpath:\n fpath=(~/.zfunc $fpath)\n autoload -Uz compinit && compinit"
|
|
||||||
case "fish":
|
case "fish":
|
||||||
dir = filepath.Join(hook.FishConfigDir(), "completions")
|
dir = filepath.Join(hook.FishConfigDir(), "completions")
|
||||||
file = filepath.Join(dir, "netssh.fish")
|
file = filepath.Join(dir, "netssh.fish")
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Shell ShellConfig `mapstructure:"shell"`
|
Shell ShellConfig `mapstructure:"shell"`
|
||||||
NetBox NetBoxConfig `mapstructure:"netbox"`
|
NetBox NetBoxConfig `mapstructure:"netbox"`
|
||||||
NetBoxes []NetBoxConfig `mapstructure:"netboxes"`
|
NetBoxes []NetBoxConfig `mapstructure:"netboxes"`
|
||||||
Resolver ResolverConfig `mapstructure:"resolver"`
|
Resolver ResolverConfig `mapstructure:"resolver"`
|
||||||
Cache CacheConfig `mapstructure:"cache"`
|
Cache CacheConfig `mapstructure:"cache"`
|
||||||
SSH SSHConfig `mapstructure:"ssh"`
|
SSH SSHConfig `mapstructure:"ssh"`
|
||||||
Shortcuts ShortcutsConfig `mapstructure:"shortcuts"`
|
Shortcuts ShortcutsConfig `mapstructure:"shortcuts"`
|
||||||
|
Completion CompletionConfig `mapstructure:"completion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShellConfig struct {
|
type ShellConfig struct {
|
||||||
@@ -47,6 +48,11 @@ type SSHConfig struct {
|
|||||||
DefaultPort int `mapstructure:"default_port"`
|
DefaultPort int `mapstructure:"default_port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CompletionConfig struct {
|
||||||
|
// MaxResults limits the number of hostnames offered per completion; 0 = unlimited.
|
||||||
|
MaxResults int `mapstructure:"max_results"`
|
||||||
|
}
|
||||||
|
|
||||||
type ShortcutsConfig struct {
|
type ShortcutsConfig struct {
|
||||||
Domains []string `mapstructure:"domains"`
|
Domains []string `mapstructure:"domains"`
|
||||||
StripHyphens bool `mapstructure:"strip_hyphens"`
|
StripHyphens bool `mapstructure:"strip_hyphens"`
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func commandCompletionPath(shell, alias string) string {
|
|||||||
case "bash":
|
case "bash":
|
||||||
return filepath.Join(os.Getenv("HOME"), ".local", "share", "bash-completion", "completions", alias)
|
return filepath.Join(os.Getenv("HOME"), ".local", "share", "bash-completion", "completions", alias)
|
||||||
case "zsh":
|
case "zsh":
|
||||||
return filepath.Join(os.Getenv("HOME"), ".zfunc", "_"+alias)
|
return filepath.Join(ZshFuncDir(), "_"+alias)
|
||||||
default:
|
default:
|
||||||
return filepath.Join(FishConfigDir(), "completions", alias+".fish")
|
return filepath.Join(FishConfigDir(), "completions", alias+".fish")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func TestCommandAliasCompletesServerNamesAcrossShells(t *testing.T) {
|
|||||||
case "bash":
|
case "bash":
|
||||||
err = root.GenBashCompletionV2(&canonical, true)
|
err = root.GenBashCompletionV2(&canonical, true)
|
||||||
case "zsh":
|
case "zsh":
|
||||||
err = root.GenZshCompletion(&canonical)
|
_, err = canonical.WriteString(hook.ZshCompletionScript)
|
||||||
case "fish":
|
case "fish":
|
||||||
err = root.GenFishCompletion(&canonical, true)
|
err = root.GenFishCompletion(&canonical, true)
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,8 @@ compinit -u -D
|
|||||||
alias j=netssh
|
alias j=netssh
|
||||||
autoload -Uz _j
|
autoload -Uz _j
|
||||||
# Capture matches without requiring an interactive ZLE completion widget.
|
# Capture matches without requiring an interactive ZLE completion widget.
|
||||||
_describe() { print -rl -- "${completions[@]}"; }
|
_describe() { print -rl -- "${(P)4}"; }
|
||||||
|
compadd() { print -rl -- "$@"; }
|
||||||
words=(j ser)
|
words=(j ser)
|
||||||
CURRENT=2
|
CURRENT=2
|
||||||
_j
|
_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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
Reference in new issue
Block a user