commit 34cc44911425161f481f9fd590a720cea99ee2a2
parent 0ee4773750e039fc852b5a5ab03e1da720664d7b
Author: Sean Enck <sean@ttypty.com>
Date: Fri, 21 Jul 2023 18:18:27 -0400
zsh completions
Diffstat:
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")
}