Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac713ac97b | ||
|
|
091f5cc518 | ||
|
|
6243514def |
No files matched your search
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.0.22
|
||||||
|
|
||||||
|
- Fix shell completion with multiple NetBox instances: typing a hostname without an instance prefix (`fsn1-bx1<Tab>`) found nothing in bash and zsh, because only `prod/…` was offered. Hostnames that are unique across all instances are now offered without the prefix; connecting to them still checks every instance for ambiguous names.
|
||||||
|
- zsh also accepts instance-qualified matches for the typed hostname, so names that exist in several instances are offered as `prod/…` and `stg/…`.
|
||||||
|
|
||||||
## v0.0.21
|
## v0.0.21
|
||||||
|
|
||||||
- Ship a zsh completion script built for large caches: 5000 hosts complete in under 0.1 s instead of hanging for seconds with Cobra's generated script.
|
- Ship a zsh completion script built for large caches: 5000 hosts complete in under 0.1 s instead of hanging for seconds with Cobra's generated script.
|
||||||
|
|||||||
@@ -552,6 +552,10 @@ rm -f ~/.zcompdump* && exec zsh
|
|||||||
```
|
```
|
||||||
|
|
||||||
Completions are served from the local cache — no network request on every `<Tab>`.
|
Completions are served from the local cache — no network request on every `<Tab>`.
|
||||||
|
With several NetBox instances, a hostname that exists in only one instance is
|
||||||
|
completed without the instance prefix (`web01`); names that exist in several
|
||||||
|
instances are offered as `prod/web01`, `staging/web01`. In bash, type the
|
||||||
|
instance prefix for those; zsh and fish offer them for the hostname alone.
|
||||||
The zsh script that `netssh completion zsh` prints is built for large caches:
|
The zsh script that `netssh completion zsh` prints is built for large caches:
|
||||||
5000 hosts complete in well under a second. To limit the number of hostnames
|
5000 hosts complete in well under a second. To limit the number of hostnames
|
||||||
offered, set `completion.max_results`.
|
offered, set `completion.max_results`.
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache"
|
||||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
||||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox"
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox"
|
||||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/shortcuts"
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/shortcuts"
|
||||||
@@ -30,3 +34,36 @@ func normalizeTarget(cfg config.ShortcutsConfig, qualified bool) func(string) st
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hostCompletions returns the cached hosts matching toComplete in a form that
|
||||||
|
// starts with the typed text, because bash and fish only accept such
|
||||||
|
// candidates. Without an instance prefix, a hostname that is unique across
|
||||||
|
// all instances is offered bare ("web01"), so it completes like with a single
|
||||||
|
// NetBox; connecting to it still checks every instance for ambiguity.
|
||||||
|
// Names present in several instances stay qualified ("prod/web01").
|
||||||
|
func hostCompletions(entries []cache.Entry, toComplete string) []string {
|
||||||
|
prefix := strings.ToLower(toComplete)
|
||||||
|
qualifiedInput := strings.Contains(prefix, "/")
|
||||||
|
instances := make(map[string]int)
|
||||||
|
for _, e := range entries {
|
||||||
|
_, name := netbox.SplitTarget(e.Name)
|
||||||
|
instances[strings.ToLower(name)]++
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for _, e := range entries {
|
||||||
|
source, name := netbox.SplitTarget(e.Name)
|
||||||
|
target := strings.ToLower(e.Name)
|
||||||
|
switch {
|
||||||
|
case source != "" && !qualifiedInput && strings.HasPrefix(strings.ToLower(name), prefix):
|
||||||
|
if instances[strings.ToLower(name)] == 1 {
|
||||||
|
out = append(out, name)
|
||||||
|
} else {
|
||||||
|
out = append(out, e.Name)
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(target, prefix):
|
||||||
|
out = append(out, e.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -206,3 +207,33 @@ func TestRefreshCachesIndependentDevicesAndVMs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHostCompletionsMatchTypedText(t *testing.T) {
|
||||||
|
entries := []cache.Entry{
|
||||||
|
{Name: "prod/fsn1-bx1.example.com"},
|
||||||
|
{Name: "prod/fsn1-bx2.example.com"},
|
||||||
|
{Name: "stg/fsn1-bx2.example.com"},
|
||||||
|
{Name: "stg/hel1-tb1.example.com"},
|
||||||
|
{Name: "prod/HEL1-TB2"},
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
input string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
// Unique names are bare; names in both instances stay qualified.
|
||||||
|
{"fsn1", []string{"fsn1-bx1.example.com", "prod/fsn1-bx2.example.com", "stg/fsn1-bx2.example.com"}},
|
||||||
|
{"hel1", []string{"HEL1-TB2", "hel1-tb1.example.com"}},
|
||||||
|
{"prod/", []string{"prod/HEL1-TB2", "prod/fsn1-bx1.example.com", "prod/fsn1-bx2.example.com"}},
|
||||||
|
{"stg/fsn", []string{"stg/fsn1-bx2.example.com"}},
|
||||||
|
// An instance name typed without the slash completes qualified targets.
|
||||||
|
{"st", []string{"stg/fsn1-bx2.example.com", "stg/hel1-tb1.example.com"}},
|
||||||
|
} {
|
||||||
|
if got := hostCompletions(entries, tc.input); !reflect.DeepEqual(got, tc.want) {
|
||||||
|
t.Errorf("%q: got %v, want %v", tc.input, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Single-instance caches are unchanged.
|
||||||
|
if got := hostCompletions([]cache.Entry{{Name: "web01"}, {Name: "db01"}}, "w"); !reflect.DeepEqual(got, []string{"web01"}) {
|
||||||
|
t.Errorf("single instance: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-4
@@ -339,14 +339,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()
|
||||||
|
|
||||||
hosts := c.Search(toComplete)
|
hosts := hostCompletions(c.All(), 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 {
|
if limit := cfg.Completion.MaxResults; limit > 0 && len(hosts) > limit {
|
||||||
hosts = hosts[:limit]
|
hosts = hosts[:limit]
|
||||||
}
|
}
|
||||||
completions := make([]cobra.Completion, 0, len(hosts))
|
completions := make([]cobra.Completion, 0, len(hosts))
|
||||||
for _, e := range hosts {
|
for _, host := range hosts {
|
||||||
completions = append(completions, cobra.Completion(e.Name))
|
completions = append(completions, cobra.Completion(host))
|
||||||
}
|
}
|
||||||
// Subcommands at the end, after all hostnames.
|
// Subcommands at the end, after all hostnames.
|
||||||
for _, sub := range cmd.Commands() {
|
for _, sub := range cmd.Commands() {
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ _netssh
|
|||||||
t.Fatalf("%v\n%s", err, out)
|
t.Fatalf("%v\n%s", err, out)
|
||||||
}
|
}
|
||||||
// Directive 36 = NoFileComp|KeepOrder: unsorted host group, no file fallback.
|
// 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"
|
want := "describe cache:Manage the cache\ncompadd -V hosts -X %Bhosts%b -M l:|=* -- web01 web:02\n"
|
||||||
if string(out) != want {
|
if string(out) != want {
|
||||||
t.Fatalf("got:\n%s\nwant:\n%s", out, want)
|
t.Fatalf("got:\n%s\nwant:\n%s", out, want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,12 +58,13 @@ _netssh() {
|
|||||||
_describe -t commands 'netssh command' items $nospace && ret=0
|
_describe -t commands 'netssh command' items $nospace && ret=0
|
||||||
fi
|
fi
|
||||||
if (( $#plain )); then
|
if (( $#plain )); then
|
||||||
|
# netssh already filtered the hosts, also by the name after an instance
|
||||||
|
# prefix: "fsn1" offers "prod/fsn1-…". 'l:|=*' lets zsh accept text
|
||||||
|
# before the typed word instead of discarding these matches.
|
||||||
|
local -a group=(-J hosts)
|
||||||
# Directive 32: keep the order supplied by netssh.
|
# Directive 32: keep the order supplied by netssh.
|
||||||
if (( directive & 32 )); then
|
(( directive & 32 )) && group=(-V hosts)
|
||||||
compadd -V hosts -X '%Bhosts%b' $nospace -- $plain && ret=0
|
compadd $group -X '%Bhosts%b' -M 'l:|=*' $nospace -- $plain && ret=0
|
||||||
else
|
|
||||||
compadd -J hosts -X '%Bhosts%b' $nospace -- $plain && ret=0
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
# Directive 4: no file completion as a fallback.
|
# Directive 4: no file completion as a fallback.
|
||||||
if (( ! $#lines && ! (directive & 4) )); then
|
if (( ! $#lines && ! (directive & 4) )); then
|
||||||
|
|||||||
Reference in new issue
Block a user