lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 815d90dfcfe120ff5e07e4fcb003d29edde75d24
parent d0fc87bdd745a33a434989e940a0b0019c8ff56e
Author: Sean Enck <sean@ttypty.com>
Date:   Fri, 19 Apr 2024 22:32:54 -0400

completions are a subcommand now

Diffstat:
Minternal/app/completions.go | 7+++++--
Minternal/app/core.go | 57++++++++++++++++++++++++++++++---------------------------
Minternal/app/core_test.go | 4++--
Minternal/app/doc/bash.sh | 2+-
Minternal/app/doc/completions.txt | 2+-
Minternal/app/doc/fish.sh | 2+-
Minternal/app/doc/zsh.sh | 1-
Minternal/app/info.go | 36+++++++++++++++++++++++++-----------
Minternal/app/info_test.go | 89++++++++++++++++++++++++++++++++++---------------------------------------------
9 files changed, 103 insertions(+), 97 deletions(-)

diff --git a/internal/app/completions.go b/internal/app/completions.go @@ -122,7 +122,11 @@ func loadProfiles(exe string, canFilter bool) []Profile { // GenerateCompletions handles creating shell completion outputs func GenerateCompletions(completionType string, isHelp bool, exe string) ([]string, error) { if isHelp { - var h []string + h := []string{"completions are available for:"} + for _, s := range []string{CompletionsBashCommand, CompletionsFishCommand, CompletionsZshCommand} { + h = append(h, fmt.Sprintf(" - %s", s)) + } + h = append(h, "") for _, p := range loadProfiles(exe, false) { if p.IsDefault { continue @@ -155,7 +159,6 @@ when %s=<unknown> JSONCommand: JSONCommand, HelpCommand: HelpCommand, HelpAdvancedCommand: HelpAdvancedCommand, - HelpShellCommand: HelpShellCommand, TOTPCommand: TOTPCommand, MoveCommand: MoveCommand, DoList: fmt.Sprintf("%s %s", exe, ListCommand), diff --git a/internal/app/core.go b/internal/app/core.go @@ -55,10 +55,12 @@ const ( TOTPListCommand = ListCommand // TOTPOnceCommand will perform like a normal totp request but not refresh TOTPOnceCommand = "once" - // BashCommand is the command to generate bash completions - BashCommand = "bash" - // HelpShellCommand is the help output about shell variables - HelpShellCommand = "shell" + // CompletionsBashCommand is the command to generate bash completions + CompletionsBashCommand = "bash" + // CompletionsCommand are used to generate shell completions + CompletionsCommand = "completions" + // CompletionsHelpCommand displays information about shell completions + CompletionsHelpCommand = "help" // ReKeyCommand will rekey the underlying database ReKeyCommand = "rekey" // MultiLineCommand handles multi-line inserts (when not piped) @@ -69,12 +71,12 @@ const ( TOTPInsertCommand = InsertCommand // JSONCommand handles JSON outputs JSONCommand = "json" - // ZshCommand is the command to generate zsh completions - ZshCommand = "zsh" - // FishCommand is the command to generate fish completions - FishCommand = "fish" - docDir = "doc" - textFile = ".txt" + // CompletionsZshCommand is the command to generate zsh completions + CompletionsZshCommand = "zsh" + // CompletionsFishCommand is the command to generate fish completions + CompletionsFishCommand = "fish" + docDir = "doc" + textFile = ".txt" ) //go:embed doc/* @@ -96,13 +98,13 @@ type ( } // Documentation is how documentation segments are templated Documentation struct { - Executable string - MoveCommand string - RemoveCommand string - ReKeyCommand string - HelpCommand string - HelpShellCommand string - ReKey struct { + Executable string + MoveCommand string + RemoveCommand string + ReKeyCommand string + CompletionsCommand string + CompletionsHelpCommand string + ReKey struct { Store string KeyFile string Key string @@ -184,12 +186,15 @@ func commandText(args, name, desc string) string { // Usage return usage information func Usage(verbose bool, exe string) ([]string, error) { var results []string - results = append(results, command(BashCommand, "", "generate user environment bash completion")) results = append(results, command(ClipCommand, "entry", "copy the entry's value into the clipboard")) + results = append(results, command(CompletionsCommand, "<shell>", "generate completions via auto-detection of shell")) + results = append(results, subCommand(CompletionsCommand, CompletionsBashCommand, "", "generate bash completions")) + results = append(results, subCommand(CompletionsCommand, CompletionsFishCommand, "", "generate fish completions")) + results = append(results, subCommand(CompletionsCommand, CompletionsHelpCommand, "", "show help information about completions")) + results = append(results, subCommand(CompletionsCommand, CompletionsZshCommand, "", "generate zsh completions")) results = append(results, command(EnvCommand, "", "display environment variable information")) results = append(results, command(HelpCommand, "", "show this usage information")) results = append(results, subCommand(HelpCommand, HelpAdvancedCommand, "", "display verbose help information")) - results = append(results, subCommand(HelpCommand, HelpShellCommand, "", "display shell variable help information")) results = append(results, command(InsertCommand, "entry", "insert a new entry into the store")) results = append(results, command(JSONCommand, "filter", "display detailed information")) results = append(results, command(ListCommand, "", "list entries")) @@ -206,19 +211,17 @@ func Usage(verbose bool, exe string) ([]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, command(FishCommand, "", "generate user environment fish completion")) sort.Strings(results) usage := []string{fmt.Sprintf("%s usage:", exe)} if verbose { results = append(results, "") document := Documentation{ - Executable: filepath.Base(exe), - MoveCommand: MoveCommand, - RemoveCommand: RemoveCommand, - ReKeyCommand: ReKeyCommand, - HelpShellCommand: HelpShellCommand, - HelpCommand: HelpCommand, + Executable: filepath.Base(exe), + MoveCommand: MoveCommand, + RemoveCommand: RemoveCommand, + ReKeyCommand: ReKeyCommand, + CompletionsCommand: CompletionsCommand, + CompletionsHelpCommand: CompletionsHelpCommand, } document.ReKey.Store = setDocFlag(config.ReKeyStoreFlag) document.ReKey.Key = setDocFlag(config.ReKeyKeyFlag) diff --git a/internal/app/core_test.go b/internal/app/core_test.go @@ -9,11 +9,11 @@ import ( func TestUsage(t *testing.T) { u, _ := app.Usage(false, "lb") - if len(u) != 25 { + if len(u) != 26 { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = app.Usage(true, "lb") - if len(u) != 108 { + if len(u) != 109 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { diff --git a/internal/app/doc/bash.sh b/internal/app/doc/bash.sh @@ -17,7 +17,7 @@ if [ "$COMP_CWORD" -eq 2 ]; then case ${COMP_WORDS[1]} in "{{ $.HelpCommand }}") - opts="{{ $.HelpAdvancedCommand }} {{ $.HelpShellCommand }}" + opts="{{ $.HelpAdvancedCommand }}" ;; {{- if not $profile.ReadOnly }} {{- if $profile.CanList }} diff --git a/internal/app/doc/completions.txt b/internal/app/doc/completions.txt @@ -1,5 +1,5 @@ Completions are available for certain shells and, by default, assume all features of `{{ $.Executable }}` are enabled and available. When changing certain environment flags it may be useful to change the completion profile to more closely match -the restricted command options, run `{{ $.Executable }} {{ $.HelpCommand }} {{ $.HelpShellCommand }}` for information +the restricted command options, run `{{ $.Executable }} {{ $.CompletionsCommand }} {{ $.CompletionsHelpCommand }}` for information on how best to alter completion outputs. diff --git a/internal/app/doc/fish.sh b/internal/app/doc/fish.sh @@ -5,7 +5,7 @@ complete -c {{ $.Executable }} -f function {{ $profile.Name }} set -l commands {{ range $idx, $value := $profile.Options }}{{ if gt $idx 0}} {{ end }}{{ $value }}{{ end }} complete -c {{ $.Executable }} -n "not __fish_seen_subcommand_from $commands" -a "$commands" - complete -c {{ $.Executable }} -n "__fish_seen_subcommand_from {{ $.HelpCommand }}; and test (count (commandline -opc)) -lt 3" -a "{{ $.HelpAdvancedCommand }} {{ $.HelpShellCommand }}" + complete -c {{ $.Executable }} -n "__fish_seen_subcommand_from {{ $.HelpCommand }}; and test (count (commandline -opc)) -lt 3" -a "{{ $.HelpAdvancedCommand }}" {{- if not $profile.ReadOnly }} {{- if $profile.CanList }} complete -c {{ $.Executable }} -n "__fish_seen_subcommand_from {{ $.InsertCommand }} {{ $.MultiLineCommand }} {{ $.RemoveCommand }}; and test (count (commandline -opc)) -lt 3" -a "({{ $.DoList }})" diff --git a/internal/app/doc/zsh.sh b/internal/app/doc/zsh.sh @@ -21,7 +21,6 @@ case $words[2] in "{{ $.HelpCommand }}") if [ "$len" -eq 3 ]; then - compadd "$@" "{{ $.HelpShellCommand }}" compadd "$@" "{{ $.HelpAdvancedCommand }}" fi ;; diff --git a/internal/app/info.go b/internal/app/info.go @@ -40,20 +40,18 @@ func info(command string, args []string) ([]string, error) { return nil, errors.New("invalid help command") } isAdvanced := false - exe, err := exeName() - if err != nil { - return nil, err - } if len(args) == 1 { switch args[0] { case HelpAdvancedCommand: isAdvanced = true - case HelpShellCommand: - return GenerateCompletions("", true, exe) default: return nil, errors.New("invalid help option") } } + exe, err := exeName() + if err != nil { + return nil, err + } results, err := Usage(isAdvanced, exe) if err != nil { return nil, err @@ -64,15 +62,31 @@ func info(command string, args []string) ([]string, error) { return nil, errors.New("invalid env command") } return config.Environ(), nil - case BashCommand, ZshCommand, FishCommand: - if len(args) != 0 { - return nil, fmt.Errorf("invalid %s command", command) - } + case CompletionsCommand: + shell := "" exe, err := exeName() if err != nil { return nil, err } - return GenerateCompletions(command, false, exe) + switch len(args) { + case 0: + shell = filepath.Base(os.Getenv("SHELL")) + case 1: + sub := args[0] + if sub == CompletionsHelpCommand { + return GenerateCompletions("", true, exe) + } + shell = sub + default: + return nil, errors.New("invalid completions subcommand") + } + switch shell { + case CompletionsZshCommand, CompletionsBashCommand, CompletionsFishCommand: + break + default: + return nil, fmt.Errorf("unknown completion type: %s", shell) + } + return GenerateCompletions(shell, false, exe) } return nil, nil } diff --git a/internal/app/info_test.go b/internal/app/info_test.go @@ -3,6 +3,7 @@ package app_test import ( "bytes" "os" + "strings" "testing" "github.com/enckse/lockbox/internal/app" @@ -41,36 +42,6 @@ func TestHelpInfo(t *testing.T) { if _, err = app.Info(&buf, "help", []string{"verbose", "A"}); err.Error() != "invalid help command" { t.Errorf("invalid error: %v", err) } - old = buf.String() - buf = bytes.Buffer{} - ok, err = app.Info(&buf, "help", []string{"shell"}) - if !ok || err != nil { - t.Errorf("invalid error: %v", err) - } - if buf.String() == "" || old == buf.String() { - t.Error("nothing written") - } -} - -func TestBashInfo(t *testing.T) { - os.Clearenv() - var buf bytes.Buffer - ok, err := app.Info(&buf, "bash", []string{}) - if !ok || err != nil { - t.Errorf("invalid error: %v", err) - } - if buf.String() == "" { - t.Error("nothing written") - } - if _, err = app.Info(&buf, "bash", []string{"defaults"}); err.Error() != "invalid bash command" { - t.Errorf("invalid error: %v", err) - } - if _, err = app.Info(&buf, "bash", []string{"test", "default"}); err.Error() != "invalid bash command" { - t.Errorf("invalid error: %v", err) - } - if _, err = app.Info(&buf, "bash", []string{"short"}); err.Error() != "invalid bash command" { - t.Errorf("invalid error: %v", err) - } } func TestEnvInfo(t *testing.T) { @@ -99,38 +70,54 @@ func TestEnvInfo(t *testing.T) { } } -func TestZshInfo(t *testing.T) { - os.Clearenv() +func TestCompletionInfo(t *testing.T) { + defer os.Clearenv() + for k, v := range map[string]string{ + "zsh": "typeset -A opt_args", + "fish": "set -l commands", + "bash": "local cur opts", + } { + for _, b := range []bool{true, false} { + os.Clearenv() + sub := []string{k} + os.Setenv("SHELL", "invalid") + if b { + sub = []string{} + os.Setenv("SHELL", k) + } + var buf bytes.Buffer + ok, err := app.Info(&buf, "completions", sub) + if !ok || err != nil { + t.Errorf("invalid error: %v", err) + } + s := buf.String() + if s == "" { + t.Error("nothing written") + } + if !strings.Contains(s, v) { + t.Errorf("invalid completions for %s", k) + } + } + } var buf bytes.Buffer - ok, err := app.Info(&buf, "zsh", []string{}) + ok, err := app.Info(&buf, "completions", []string{"help"}) if !ok || err != nil { t.Errorf("invalid error: %v", err) } - if buf.String() == "" { + s := buf.String() + if s == "" { t.Error("nothing written") } - if _, err = app.Info(&buf, "zsh", []string{"defaults"}); err.Error() != "invalid zsh command" { - t.Errorf("invalid error: %v", err) - } - if _, err = app.Info(&buf, "zsh", []string{"test", "default"}); err.Error() != "invalid zsh command" { - t.Errorf("invalid error: %v", err) - } -} -func TestFishInfo(t *testing.T) { - os.Clearenv() - var buf bytes.Buffer - ok, err := app.Info(&buf, "fish", []string{}) - if !ok || err != nil { + if _, err = app.Info(&buf, "completions", []string{"helps"}); err.Error() != "unknown completion type: helps" { t.Errorf("invalid error: %v", err) } - if buf.String() == "" { - t.Error("nothing written") - } - if _, err = app.Info(&buf, "fish", []string{"help"}); err.Error() != "invalid fish command" { + os.Clearenv() + os.Setenv("SHELL", "bad") + if _, err = app.Info(&buf, "completions", []string{}); err.Error() != "unknown completion type: bad" { t.Errorf("invalid error: %v", err) } - if _, err = app.Info(&buf, "fish", []string{"test", "default"}); err.Error() != "invalid fish command" { + if _, err = app.Info(&buf, "completions", []string{"bash"}); err != nil { t.Errorf("invalid error: %v", err) } }