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:
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) {