lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 4d76c8be3e8b8c20b3ee5fa3d5379360f20ae486
parent d68c5970207d286dd50ff0694f216368b98f8cc4
Author: Sean Enck <sean@ttypty.com>
Date:   Sat,  7 Dec 2024 12:56:36 -0500

completions should call back into lb to get vars

Diffstat:
Minternal/app/completions.go | 55++++++++++++++++++++++++++++++++-----------------------
Minternal/app/info.go | 19++++++++++++++++---
Minternal/app/info_test.go | 28+++++++++++++++++++++++-----
Minternal/app/shell/bash.sh | 1+
Minternal/app/shell/fish.sh | 4++++
Minternal/app/shell/zsh.sh | 2+-
Minternal/config/core.go | 21++++++++++++++-------
Minternal/config/core_test.go | 4++++
8 files changed, 95 insertions(+), 39 deletions(-)

diff --git a/internal/app/completions.go b/internal/app/completions.go @@ -12,10 +12,6 @@ import ( "github.com/seanenck/lockbox/internal/config" ) -const ( - shellIsNotText = `[ "%s" != "%s" ]` -) - type ( // Completions handles the inputs to completions for templating Completions struct { @@ -34,18 +30,22 @@ type ( HelpCommand string HelpAdvancedCommand string HelpConfigCommand string + ExportCommand string Options []CompletionOption TOTPSubCommands []CompletionOption - Conditionals struct { - Not struct { - ReadOnly string - CanClip string - CanTOTP string - AskMode string - Ever string - CanPasswordGen string - } + Conditionals Conditionals + } + // Conditionals help control completion flow + Conditionals struct { + Not struct { + ReadOnly string + CanClip string + CanTOTP string + AskMode string + Ever string + CanPasswordGen string } + Exported []string } // CompletionOption are conditional wrapped logic for options that may be disabled CompletionOption struct { @@ -57,10 +57,6 @@ type ( //go:embed shell/* var shell embed.FS -func newShellIsNotEqualConditional(keyed interface{ Key() string }, right string) string { - return fmt.Sprintf(shellIsNotText, fmt.Sprintf("$%s", keyed.Key()), right) -} - func (c Completions) newGenOptions(defaults []string, kv map[string]string) []CompletionOption { opt := []CompletionOption{} for _, a := range defaults { @@ -78,6 +74,23 @@ func (c Completions) newGenOptions(defaults []string, kv map[string]string) []Co return opt } +func newConditionals() Conditionals { + const shellIsNotText = `[ "%s" != "%s" ]` + c := Conditionals{} + registerIsNotEqual := func(key interface{ Key() string }, right string) string { + k := key.Key() + c.Exported = append(c.Exported, k) + return fmt.Sprintf(shellIsNotText, fmt.Sprintf("$%s", k), right) + } + c.Not.ReadOnly = registerIsNotEqual(config.EnvReadOnly, config.YesValue) + c.Not.CanClip = registerIsNotEqual(config.EnvClipEnabled, config.NoValue) + c.Not.CanTOTP = registerIsNotEqual(config.EnvTOTPEnabled, config.NoValue) + c.Not.AskMode = registerIsNotEqual(config.EnvPasswordMode, string(config.AskKeyMode)) + c.Not.CanPasswordGen = registerIsNotEqual(config.EnvPasswordGenEnabled, config.NoValue) + c.Not.Ever = fmt.Sprintf(shellIsNotText, "1", "0") + return c +} + // GenerateCompletions handles creating shell completion outputs func GenerateCompletions(completionType, exe string) ([]string, error) { if !slices.Contains(completionTypes, completionType) { @@ -99,13 +112,9 @@ func GenerateCompletions(completionType, exe string) ([]string, error) { MoveCommand: MoveCommand, DoList: fmt.Sprintf("%s %s", exe, ListCommand), DoTOTPList: fmt.Sprintf("%s %s %s", exe, TOTPCommand, TOTPListCommand), + ExportCommand: fmt.Sprintf("%s %s %s", exe, EnvCommand, CompletionsCommand), } - c.Conditionals.Not.ReadOnly = newShellIsNotEqualConditional(config.EnvReadOnly, config.YesValue) - c.Conditionals.Not.CanClip = newShellIsNotEqualConditional(config.EnvClipEnabled, config.NoValue) - c.Conditionals.Not.CanTOTP = newShellIsNotEqualConditional(config.EnvTOTPEnabled, config.NoValue) - c.Conditionals.Not.AskMode = newShellIsNotEqualConditional(config.EnvPasswordMode, string(config.AskKeyMode)) - c.Conditionals.Not.CanPasswordGen = newShellIsNotEqualConditional(config.EnvPasswordGenEnabled, config.NoValue) - c.Conditionals.Not.Ever = fmt.Sprintf(shellIsNotText, "1", "0") + c.Conditionals = newConditionals() c.Options = c.newGenOptions([]string{EnvCommand, HelpCommand, ListCommand, ShowCommand, VersionCommand, JSONCommand}, map[string]string{ diff --git a/internal/app/info.go b/internal/app/info.go @@ -64,10 +64,23 @@ func info(command string, args []string) ([]string, error) { } return results, nil case EnvCommand: - if len(args) != 0 { - return nil, errors.New("invalid env command") + var set []string + switch len(args) { + case 0: + case 1: + sub := args[0] + if sub != CompletionsCommand { + return nil, fmt.Errorf("unknown env subset: %s", sub) + } + set = newConditionals().Exported + default: + return nil, errors.New("invalid env command, too many arguments") + } + env := config.Environ(set...) + if len(env) == 0 { + env = []string{""} } - return config.Environ(), nil + return env, nil case CompletionsCommand: shell := "" exe, err := exeName() diff --git a/internal/app/info_test.go b/internal/app/info_test.go @@ -55,24 +55,42 @@ func TestEnvInfo(t *testing.T) { os.Clearenv() var buf bytes.Buffer ok, err := app.Info(&buf, "env", []string{}) - if ok || err != nil { + if !ok || err != nil { t.Errorf("invalid error: %v", err) } - if buf.String() != "" { + if buf.String() != "\n" { t.Error("nothing written") } + buf = bytes.Buffer{} t.Setenv("LOCKBOX_STORE", "1") ok, err = app.Info(&buf, "env", []string{}) if !ok || err != nil { t.Errorf("invalid error: %v", err) } - if buf.String() == "" { + if strings.TrimSpace(buf.String()) != "LOCKBOX_STORE=1" { + t.Error("nothing written") + } + buf = bytes.Buffer{} + ok, err = app.Info(&buf, "env", []string{"completions"}) + if !ok || err != nil { + t.Errorf("invalid error: %v", err) + } + if buf.String() != "\n" { + t.Error("nothing written") + } + t.Setenv("LOCKBOX_READONLY", "true") + buf = bytes.Buffer{} + ok, err = app.Info(&buf, "env", []string{"completions"}) + if !ok || err != nil { + t.Errorf("invalid error: %v", err) + } + if strings.TrimSpace(buf.String()) != "LOCKBOX_READONLY=true" { t.Error("nothing written") } - if _, err = app.Info(&buf, "env", []string{"defaults"}); err.Error() != "invalid env command" { + if _, err = app.Info(&buf, "env", []string{"defaults"}); err.Error() != "unknown env subset: defaults" { t.Errorf("invalid error: %v", err) } - if _, err = app.Info(&buf, "env", []string{"test", "default"}); err.Error() != "invalid env command" { + if _, err = app.Info(&buf, "env", []string{"test", "default"}); err.Error() != "invalid env command, too many arguments" { t.Errorf("invalid error: %v", err) } } diff --git a/internal/app/shell/bash.sh b/internal/app/shell/bash.sh @@ -2,6 +2,7 @@ _{{ $.Executable }}() { local cur opts chosen found + source <({{ $.ExportCommand }}) cur=${COMP_WORDS[COMP_CWORD]} if [ "$COMP_CWORD" -eq 1 ]; then {{- range $idx, $value := $.Options }} diff --git a/internal/app/shell/fish.sh b/internal/app/shell/fish.sh @@ -3,6 +3,10 @@ complete -c {{ $.Executable }} -f function {{ $.Executable }}-completion set -f commands "" + for line in ({{ $.ExportCommand }}) + set item (string split -m 1 "=" $line) + set -f $item[1] $item[2] + end {{- range $idx, $value := $.Options }} {{- if gt $idx 0 }} set -f commands " $commands" diff --git a/internal/app/shell/zsh.sh b/internal/app/shell/zsh.sh @@ -3,7 +3,7 @@ _{{ $.Executable }}() { local curcontext="$curcontext" state len chosen found args typeset -A opt_args - + source <({{ $.ExportCommand }}) _arguments \ '1: :->main'\ '*: :->args' diff --git a/internal/config/core.go b/internal/config/core.go @@ -7,6 +7,7 @@ import ( "net/url" "os" "path/filepath" + "slices" "sort" "strings" "time" @@ -125,19 +126,25 @@ func IsUnset(k, v string) (bool, error) { } // Environ will list the current environment keys -func Environ() []string { +func Environ(set ...string) []string { var results []string + filtered := len(set) > 0 for _, k := range os.Environ() { for _, r := range registry { - key := r.self().Key() - if key == EnvConfig.Key() { + rawKey := r.self().Key() + if rawKey == EnvConfig.Key() { continue } - key = fmt.Sprintf("%s=", key) - if strings.HasPrefix(k, key) { - results = append(results, k) - break + key := fmt.Sprintf("%s=", rawKey) + if !strings.HasPrefix(k, key) { + continue + } + if filtered { + if !slices.Contains(set, rawKey) { + continue + } } + results = append(results, k) } } sort.Strings(results) diff --git a/internal/config/core_test.go b/internal/config/core_test.go @@ -107,6 +107,10 @@ func TestEnviron(t *testing.T) { if len(e) != 2 || fmt.Sprintf("%v", e) != "[LOCKBOX_CREDENTIALS_PASSWORD=2 LOCKBOX_STORE=1]" { t.Errorf("invalid environ: %v", e) } + e = config.Environ("LOCKBOX_STORE", "LOCKBOX_OTHER") + if len(e) != 1 || fmt.Sprintf("%v", e) != "[LOCKBOX_STORE=1]" { + t.Errorf("invalid environ: %v", e) + } } func TestWrap(t *testing.T) {