lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 34cc44911425161f481f9fd590a720cea99ee2a2
parent 0ee4773750e039fc852b5a5ab03e1da720664d7b
Author: Sean Enck <sean@ttypty.com>
Date:   Fri, 21 Jul 2023 18:18:27 -0400

zsh completions

Diffstat:
Minternal/app/info.go | 4++--
Minternal/app/info_test.go | 26++++++++++++++++++++++++++
Minternal/cli/completions.bash | 6+++---
Ainternal/cli/completions.zsh | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/cli/core.go | 18+++++++++++++++---
Minternal/cli/core_test.go | 23++++++++++++++---------
6 files changed, 121 insertions(+), 17 deletions(-)

diff --git a/internal/app/info.go b/internal/app/info.go @@ -43,7 +43,7 @@ func info(command string, args []string) ([]string, error) { return nil, err } return results, nil - case cli.EnvCommand, cli.BashCommand: + case cli.EnvCommand, cli.BashCommand, cli.ZshCommand: defaultFlag := cli.BashDefaultsCommand isEnv := command == cli.EnvCommand if isEnv { @@ -56,7 +56,7 @@ func info(command string, args []string) ([]string, error) { if isEnv { return inputs.ListEnvironmentVariables(!defaults), nil } - return cli.BashCompletions(defaults) + return cli.GenerateCompletions(command == cli.BashCommand, defaults) } return nil, nil } diff --git a/internal/app/info_test.go b/internal/app/info_test.go @@ -94,3 +94,29 @@ func TestEnvInfo(t *testing.T) { t.Errorf("invalid error: %v", err) } } + +func TestZshInfo(t *testing.T) { + os.Clearenv() + var buf bytes.Buffer + ok, err := app.Info(&buf, "zsh", []string{}) + if !ok || err != nil { + t.Errorf("invalid error: %v", err) + } + if buf.String() == "" { + t.Error("nothing written") + } + buf = bytes.Buffer{} + ok, err = app.Info(&buf, "zsh", []string{"defaults"}) + if !ok || err != nil { + t.Errorf("invalid error: %v", err) + } + if buf.String() == "" { + t.Error("nothing written") + } + if _, err = app.Info(&buf, "zsh", []string{"default"}); err.Error() != "invalid argument" { + t.Errorf("invalid error: %v", err) + } + if _, err = app.Info(&buf, "zsh", []string{"test", "default"}); err.Error() != "invalid argument" { + t.Errorf("invalid error: %v", err) + } +} diff --git a/internal/cli/completions.bash b/internal/cli/completions.bash @@ -12,13 +12,13 @@ _{{ $.Executable }}() { else if [ "$COMP_CWORD" -eq 2 ]; then case ${COMP_WORDS[1]} in + "{{ $.HelpCommand }}") + opts="{{ $.HelpAdvancedCommand }}" + ;; {{- if not $.ReadOnly }} "{{ $.InsertCommand }}" | "{{ $.MultiLineCommand }}") opts="$opts $({{ $.DoList }})" ;; - "{{ $.HelpCommand }}") - opts="{{ $.HelpAdvancedCommand }}" - ;; "{{ $.MoveCommand }}") opts=$({{ $.DoList }}) ;; diff --git a/internal/cli/completions.zsh b/internal/cli/completions.zsh @@ -0,0 +1,61 @@ +#compdef _{{ $.Executable }} {{ $.Executable }} + +_{{ $.Executable }}() { + local curcontext="$curcontext" state len + typeset -A opt_args + + _arguments \ + '1: :->main'\ + '*: :->args' + + len=${#words[@]} + case $state in + main) + _arguments '1:main:({{ range $idx, $value := $.Options }}{{ if gt $idx 0}} {{ end }}{{ $value }}{{ end }})' + ;; + *) + case $words[2] in + "{{ $.HelpCommand }}") + if [ "$len" -eq 3 ]; then + compadd "$@" "{{ $.HelpAdvancedCommand }}" + fi + ;; +{{- if not $.ReadOnly }} + "{{ $.InsertCommand }}" | "{{ $.MultiLineCommand }}") + if [ "$len" -eq 3 ]; then + compadd "$@" $({{ $.DoList }}) + fi + ;; + "{{ $.MoveCommand }}") + case "$len" in + 3 | 4) + compadd "$@" $({{ $.DoList }}) + ;; + esac + ;; +{{- end}} +{{- if $.CanTOTP }} + "{{ $.TOTPCommand }}") + case "$len" in + 3) + compadd "$@" {{ $.TOTPListCommand }}{{ range $key, $value := .TOTPSubCommands }} {{ $value }}{{ end }} + ;; + 4) + case $words[3] in +{{- range $key, $value := .TOTPSubCommands }} + "{{ $value }}") + compadd "$@" $({{ $.DoTOTPList }}) + ;; +{{- end}} + esac + esac + ;; +{{- end}} + "{{ $.ShowCommand }}" | "{{ $.JSONCommand }}" {{ if not $.ReadOnly }}| "{{ $.RemoveCommand }}" {{end}} {{ if $.CanClip }} | "{{ $.ClipCommand }}" {{end}}) + if [ "$len" -eq 3 ]; then + compadd "$@" $({{ $.DoList }}) + fi + ;; + esac + esac +} diff --git a/internal/cli/core.go b/internal/cli/core.go @@ -67,11 +67,17 @@ const ( TOTPInsertCommand = InsertCommand // JSONCommand handles JSON outputs JSONCommand = "json" + // ZshCommand is the command to generate zsh completions + ZshCommand = "zsh" + // ZshDefaultsCommand will generate environment agnostic completions + ZshDefaultsCommand = "defaults" ) var ( //go:embed "completions.bash" bashCompletions string + //go:embed "completions.zsh" + zshCompletions string //go:embed "doc.txt" docSection string @@ -126,8 +132,8 @@ func exeName() (string, error) { return filepath.Base(n), nil } -// BashCompletions handles creating bash completion outputs -func BashCompletions(defaults bool) ([]string, error) { +// GenerateCompletions handles creating shell completion outputs +func GenerateCompletions(isBash, defaults bool) ([]string, error) { name, err := exeName() if err != nil { return nil, err @@ -188,7 +194,11 @@ func BashCompletions(defaults bool) ([]string, error) { if c.CanTOTP { c.Options = append(c.Options, TOTPCommand) } - t, err := template.New("t").Parse(bashCompletions) + using := zshCompletions + if isBash { + using = bashCompletions + } + t, err := template.New("t").Parse(using) if err != nil { return nil, err } @@ -227,6 +237,8 @@ func Usage(verbose bool) ([]string, error) { results = append(results, subCommand(TOTPCommand, TOTPMinimalCommand, "entry", "display the first generated code (no details)")) results = append(results, subCommand(TOTPCommand, TOTPShowCommand, "entry", "show the totp entry")) results = append(results, command(VersionCommand, "", "display version information")) + results = append(results, command(ZshCommand, "", "generate user environment zsh completion")) + results = append(results, subCommand(ZshCommand, ZshDefaultsCommand, "", "generate default zsh completion")) sort.Strings(results) usage := []string{fmt.Sprintf("%s usage:", name)} if verbose { diff --git a/internal/cli/core_test.go b/internal/cli/core_test.go @@ -10,11 +10,11 @@ import ( func TestUsage(t *testing.T) { u, _ := cli.Usage(false) - if len(u) != 22 { + if len(u) != 24 { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = cli.Usage(true) - if len(u) != 92 { + if len(u) != 94 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { @@ -26,36 +26,41 @@ func TestUsage(t *testing.T) { } } -func TestCompletionsBash(t *testing.T) { +func TestGenerateCompletions(t *testing.T) { + testCompletions(t, true) + testCompletions(t, false) +} + +func testCompletions(t *testing.T, bash bool) { os.Setenv("LOCKBOX_NOTOTP", "yes") os.Setenv("LOCKBOX_READONLY", "yes") os.Setenv("LOCKBOX_NOCLIP", "yes") - defaults, _ := cli.BashCompletions(true) - roNoTOTPClip, _ := cli.BashCompletions(false) + defaults, _ := cli.GenerateCompletions(bash, true) + roNoTOTPClip, _ := cli.GenerateCompletions(bash, false) if roNoTOTPClip[0] == defaults[0] { t.Error("should not match defaults") } os.Setenv("LOCKBOX_NOTOTP", "") - roNoClip, _ := cli.BashCompletions(false) + roNoClip, _ := cli.GenerateCompletions(bash, false) if roNoClip[0] == defaults[0] || roNoClip[0] == roNoTOTPClip[0] { t.Error("should not equal defaults nor no totp/clip") } os.Setenv("LOCKBOX_READONLY", "") os.Setenv("LOCKBOX_NOCLIP", "yes") - noClip, _ := cli.BashCompletions(false) + noClip, _ := cli.GenerateCompletions(bash, false) if roNoClip[0] == noClip[0] || noClip[0] == defaults[0] || noClip[0] == roNoTOTPClip[0] { t.Error("readonly/noclip != noclip (nor defaults, nor ro/no totp/clip)") } os.Setenv("LOCKBOX_READONLY", "yes") os.Setenv("LOCKBOX_NOCLIP", "") - ro, _ := cli.BashCompletions(false) + ro, _ := cli.GenerateCompletions(bash, false) if roNoClip[0] == ro[0] || noClip[0] == ro[0] || ro[0] == defaults[0] || ro[0] == roNoTOTPClip[0] { t.Error("readonly/noclip != ro (nor ro == noclip, nor ro == defaults)") } os.Setenv("LOCKBOX_READONLY", "") os.Setenv("LOCKBOX_NOCLIP", "") os.Setenv("LOCKBOX_NOTOTP", "") - isDefaultsToo, _ := cli.BashCompletions(false) + isDefaultsToo, _ := cli.GenerateCompletions(bash, false) if isDefaultsToo[0] != defaults[0] { t.Error("defaults should match env defaults") }