commit f0d938d6eae13e4a0fb3b0de80502a6700440a32
parent 9ac06306866fabede95a7af9fe76ced117b8dbae
Author: Sean Enck <sean@ttypty.com>
Date: Sun, 3 Sep 2023 13:57:41 -0400
shell completions have profiles instead of templating to control various aspects
Diffstat:
11 files changed, 279 insertions(+), 206 deletions(-)
diff --git a/internal/app/completions.go b/internal/app/completions.go
@@ -4,6 +4,8 @@ package app
import (
"bytes"
"fmt"
+ "sort"
+ "strings"
"text/template"
"github.com/enckse/lockbox/internal/config"
@@ -12,13 +14,7 @@ import (
type (
// Completions handles the inputs to completions for templating
Completions struct {
- Options []string
- CanClip bool
- CanTOTP bool
- CanList bool
- ReadOnly bool
InsertCommand string
- TOTPSubCommands []string
TOTPListCommand string
RemoveCommand string
ClipCommand string
@@ -32,17 +28,133 @@ type (
JSONCommand string
HelpCommand string
HelpAdvancedCommand string
+ Profiles []Profile
+ DefaultProfile Profile
+ Shell string
+ CompletionEnv string
}
+
+ // Profile is a completion profile
+ Profile struct {
+ Name string
+ Comment string
+ CanClip bool
+ CanTOTP bool
+ CanList bool
+ ReadOnly bool
+ IsDefault bool
+ }
+)
+
+const (
+ askProfile = "ask"
+ roProfile = "readonly"
+ noTOTPProfile = "nototp"
+ noClipProfile = "noclip"
)
+// Env will get the environment settable value to use this profile
+func (p Profile) Env() string {
+ return fmt.Sprintf("%s=%s", config.EnvironmentCompletionKey, p.Display())
+}
+
+// Options will list the profile options
+func (p Profile) Options() []string {
+ opts := []string{EnvCommand, HelpCommand, ListCommand, ShowCommand, VersionCommand, JSONCommand}
+ if p.CanClip {
+ opts = append(opts, ClipCommand)
+ }
+ if !p.ReadOnly {
+ opts = append(opts, MoveCommand, RemoveCommand, InsertCommand, MultiLineCommand)
+ }
+ if p.CanTOTP {
+ opts = append(opts, TOTPCommand)
+ }
+ return opts
+}
+
+// Display is the profile display name
+func (p Profile) Display() string {
+ return strings.Join(strings.Split(strings.ToUpper(p.Name), "-")[2:], "-")
+}
+
+// TOTPSubCommands are the list of sub commands for TOTP within the profile
+func (p Profile) TOTPSubCommands() []string {
+ totp := []string{TOTPMinimalCommand, TOTPOnceCommand, TOTPShowCommand}
+ if p.CanClip {
+ totp = append(totp, TOTPClipCommand)
+ }
+ if !p.ReadOnly {
+ totp = append(totp, TOTPInsertCommand)
+ }
+ return totp
+}
+
+func newProfile(exe string, keys []string) Profile {
+ p := Profile{}
+ p.CanClip = true
+ p.CanList = true
+ p.CanTOTP = true
+ p.ReadOnly = false
+ name := "profile-"
+ sort.Strings(keys)
+ var comments []string
+ for _, k := range keys {
+ name = fmt.Sprintf("%s%s-", name, k)
+ switch k {
+ case askProfile:
+ comments = append(comments, "ask key mode = on")
+ p.CanList = false
+ case noTOTPProfile:
+ comments = append(comments, "totp = off")
+ p.CanTOTP = false
+ case noClipProfile:
+ comments = append(comments, "clipboard = off")
+ p.CanClip = false
+ case roProfile:
+ comments = append(comments, "readonly = on")
+ p.ReadOnly = true
+ }
+ }
+ sort.Strings(comments)
+ p.Name = newCompletionName(exe, strings.TrimSuffix(name, "-"))
+ p.Comment = fmt.Sprintf("# - %s", strings.Join(comments, "\n# - "))
+ return p
+}
+
+func generateProfiles(exe string, keys []string) map[string]Profile {
+ m := make(map[string]Profile)
+ if len(keys) == 0 {
+ return m
+ }
+ p := newProfile(exe, keys)
+ m[p.Name] = p
+ for _, cur := range keys {
+ var subset []string
+ for _, key := range keys {
+ if key == cur {
+ continue
+ }
+ subset = append(subset, key)
+ }
+
+ for _, p := range generateProfiles(exe, subset) {
+ m[p.Name] = p
+ }
+ }
+ return m
+}
+
+func newCompletionName(exe, name string) string {
+ return fmt.Sprintf("_%s-%s", exe, name)
+}
+
// GenerateCompletions handles creating shell completion outputs
-func GenerateCompletions(isBash, defaults bool, exe string) ([]string, error) {
+func GenerateCompletions(isBash bool, exe string) ([]string, error) {
c := Completions{
- CanList: true,
Executable: exe,
InsertCommand: InsertCommand,
RemoveCommand: RemoveCommand,
- TOTPSubCommands: []string{TOTPMinimalCommand, TOTPOnceCommand, TOTPShowCommand},
TOTPListCommand: TOTPListCommand,
ClipCommand: ClipCommand,
ShowCommand: ShowCommand,
@@ -54,52 +166,7 @@ func GenerateCompletions(isBash, defaults bool, exe string) ([]string, error) {
MoveCommand: MoveCommand,
DoList: fmt.Sprintf("%s %s", exe, ListCommand),
DoTOTPList: fmt.Sprintf("%s %s %s", exe, TOTPCommand, TOTPListCommand),
- Options: []string{EnvCommand, HelpCommand, ListCommand, ShowCommand, VersionCommand, JSONCommand},
- }
- isReadOnly := false
- isClip := true
- isTOTP := true
- if !defaults {
- ro, err := config.EnvReadOnly.Get()
- if err != nil {
- return nil, err
- }
- isReadOnly = ro
- noClip, err := config.EnvNoClip.Get()
- if err != nil {
- return nil, err
- }
- if noClip {
- isClip = false
- }
- noTOTP, err := config.EnvNoTOTP.Get()
- if err != nil {
- return nil, err
- }
- if noTOTP {
- isTOTP = false
- }
- k, err := config.NewKey(config.IgnoreKeyMode)
- if err != nil {
- return nil, err
- }
- if k.Ask() {
- c.CanList = false
- }
- }
- c.CanClip = isClip
- c.ReadOnly = isReadOnly
- c.CanTOTP = isTOTP
- if c.CanClip {
- c.Options = append(c.Options, ClipCommand)
- c.TOTPSubCommands = append(c.TOTPSubCommands, TOTPClipCommand)
- }
- if !c.ReadOnly {
- c.Options = append(c.Options, MoveCommand, RemoveCommand, InsertCommand, MultiLineCommand)
- c.TOTPSubCommands = append(c.TOTPSubCommands, TOTPInsertCommand)
- }
- if c.CanTOTP {
- c.Options = append(c.Options, TOTPCommand)
+ CompletionEnv: fmt.Sprintf("$%s", config.EnvironmentCompletionKey),
}
using, err := readDoc("zsh")
if err != nil {
@@ -111,13 +178,41 @@ func GenerateCompletions(isBash, defaults bool, exe string) ([]string, error) {
return nil, err
}
}
- t, err := template.New("t").Parse(using)
+ shellScript, err := readDoc("shell")
+ if err != nil {
+ return nil, err
+ }
+ profiles := generateProfiles(exe, []string{noClipProfile, roProfile, noTOTPProfile, askProfile})
+ profileObjects := []Profile{}
+ for _, v := range profiles {
+ profileObjects = append(profileObjects, v)
+ }
+ sort.Slice(profileObjects, func(i, j int) bool {
+ return strings.Compare(profileObjects[i].Name, profileObjects[j].Name) < 0
+ })
+ c.Profiles = append(c.Profiles, profileObjects...)
+ c.DefaultProfile = Profile{IsDefault: true, CanClip: true, CanTOTP: true, CanList: true, ReadOnly: false, Name: newCompletionName(exe, "default")}
+ c.Profiles = append(c.Profiles, c.DefaultProfile)
+ shell, err := templateScript(shellScript, c)
if err != nil {
return nil, err
}
+ c.Shell = shell
+ s, err := templateScript(using, c)
+ if err != nil {
+ return nil, err
+ }
+ return []string{s}, nil
+}
+
+func templateScript(script string, c Completions) (string, error) {
+ t, err := template.New("t").Parse(script)
+ if err != nil {
+ return "", err
+ }
var buf bytes.Buffer
if err := t.Execute(&buf, c); err != nil {
- return nil, err
+ return "", err
}
- return []string{buf.String()}, nil
+ return buf.String(), nil
}
diff --git a/internal/app/completions_test.go b/internal/app/completions_test.go
@@ -1,93 +1,81 @@
package app_test
import (
- "fmt"
- "os"
- "strings"
"testing"
"github.com/enckse/lockbox/internal/app"
)
-func TestGenerateCompletions(t *testing.T) {
- testCompletions(t, true)
- testCompletions(t, false)
-}
-
-func generate(keys []string, bash bool, t *testing.T) (string, string) {
- os.Setenv("LOCKBOX_NOTOTP", "")
- os.Setenv("LOCKBOX_READONLY", "")
- os.Setenv("LOCKBOX_NOCLIP", "")
- os.Setenv("LOCKBOX_KEYMODE", "")
- key := "bash"
- if !bash {
- key = "zsh"
+func TestBashCompletion(t *testing.T) {
+ v, err := app.GenerateCompletions(true, "lb")
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
}
- for _, k := range keys {
- use := "yes"
- if k == "KEYMODE" {
- use = "ask"
- }
- os.Setenv(fmt.Sprintf("LOCKBOX_%s", k), use)
- key = fmt.Sprintf("%s-%s", key, strings.ToLower(k))
+ if len(v) != 1 {
+ t.Errorf("invalid result")
}
- v, err := app.GenerateCompletions(bash, false, "lb")
+}
+
+func TestZshCompletion(t *testing.T) {
+ v, err := app.GenerateCompletions(false, "lb")
if err != nil {
t.Errorf("invalid error: %v", err)
}
if len(v) != 1 {
t.Errorf("invalid result")
}
- return key, v[0]
}
-func generateTest(keys []string, bash bool, t *testing.T) map[string]string {
- r := make(map[string]string)
- if len(keys) == 0 {
- return r
+func TestProfileDisplay(t *testing.T) {
+ p := app.Profile{Name: "_abc-test-awera-zzz"}
+ if p.Display() != "AWERA-ZZZ" {
+ t.Error("invalid display")
}
- k, v := generate(keys, bash, t)
- r[k] = v
- for _, cur := range keys {
- var subset []string
- for _, key := range keys {
- if key == cur {
- continue
- }
- subset = append(subset, key)
- }
+}
+
+func TestProfileEnv(t *testing.T) {
+ p := app.Profile{Name: "_abc-test-awera-zzz"}
+ if p.Env() != "LOCKBOX_COMPLETION_FUNCTION=AWERA-ZZZ" {
+ t.Error("invalid env")
+ }
+}
- for k, v := range generateTest(subset, bash, t) {
- r[k] = v
- }
+func TestProfileOptions(t *testing.T) {
+ p := app.Profile{Name: "_abc-test-awera-zzz"}
+ p.CanClip = true
+ p.CanTOTP = true
+ if len(p.Options()) != 12 {
+ t.Errorf("invalid options: %v", p.Options())
+ }
+ p.CanClip = false
+ if len(p.Options()) != 11 {
+ t.Errorf("invalid options: %v", p.Options())
+ }
+ p.CanClip = true
+ p.CanTOTP = false
+ if len(p.Options()) != 11 {
+ t.Errorf("invalid options: %v", p.Options())
+ }
+ p.CanTOTP = true
+ p.ReadOnly = true
+ if len(p.Options()) != 8 {
+ t.Errorf("invalid options: %v", p.Options())
}
- return r
}
-func testCompletions(t *testing.T, bash bool) {
- m := make(map[string]string)
- defaults, _ := app.GenerateCompletions(bash, true, "lb")
- m["defaults"] = defaults[0]
- for k, v := range generateTest([]string{"NOTOTP", "READONLY", "NOCLIP", "KEYMODE"}, true, t) {
- m[k] = v
+func TestProfileTOTPSubOptions(t *testing.T) {
+ p := app.Profile{Name: "_abc-test-awera-zzz"}
+ p.CanClip = true
+ if len(p.TOTPSubCommands()) != 5 {
+ t.Errorf("invalid options: %v", p.TOTPSubCommands())
}
- os.Setenv("LOCKBOX_KEYMODE", "")
- os.Setenv("LOCKBOX_READONLY", "")
- os.Setenv("LOCKBOX_NOCLIP", "")
- os.Setenv("LOCKBOX_NOTOTP", "")
- defaultsToo, _ := app.GenerateCompletions(bash, false, "lb")
- if defaultsToo[0] != defaults[0] || len(defaultsToo) != 1 || len(defaults) != 1 {
- t.Error("defaults should match env defaults/invalid defaults detected")
+ p.CanClip = false
+ if len(p.TOTPSubCommands()) != 4 {
+ t.Errorf("invalid options: %v", p.TOTPSubCommands())
}
- for k, v := range m {
- fmt.Println(k)
- for kOther, vOther := range m {
- if kOther == k {
- continue
- }
- if vOther == v {
- t.Errorf("found overlapping completion: %s == %s", k, kOther)
- }
- }
+ p.CanClip = true
+ p.ReadOnly = true
+ if len(p.TOTPSubCommands()) != 4 {
+ t.Errorf("invalid options: %v", p.TOTPSubCommands())
}
}
diff --git a/internal/app/core.go b/internal/app/core.go
@@ -56,8 +56,6 @@ const (
TOTPOnceCommand = "once"
// BashCommand is the command to generate bash completions
BashCommand = "bash"
- // BashDefaultsCommand will generate environment agnostic completions
- BashDefaultsCommand = "defaults"
// ReKeyCommand will rekey the underlying database
ReKeyCommand = "rekey"
// MultiLineCommand handles multi-line inserts (when not piped)
@@ -70,8 +68,6 @@ const (
JSONCommand = "json"
// ZshCommand is the command to generate zsh completions
ZshCommand = "zsh"
- // ZshDefaultsCommand will generate environment agnostic completions
- ZshDefaultsCommand = "defaults"
)
//go:embed doc/*
@@ -180,7 +176,6 @@ func commandText(args, name, desc string) string {
func Usage(verbose bool, exe string) ([]string, error) {
var results []string
results = append(results, command(BashCommand, "", "generate user environment bash completion"))
- results = append(results, subCommand(BashCommand, BashDefaultsCommand, "", "generate default bash completion"))
results = append(results, command(ClipCommand, "entry", "copy the entry's value into the clipboard"))
results = append(results, command(EnvCommand, "", "display environment variable information"))
results = append(results, command(HelpCommand, "", "show this usage information"))
@@ -202,7 +197,6 @@ func Usage(verbose bool, exe string) ([]string, error) {
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:", exe)}
if verbose {
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) != 23 {
t.Errorf("invalid usage, out of date? %d", len(u))
}
u, _ = app.Usage(true, "lb")
- if len(u) != 101 {
+ if len(u) != 100 {
t.Errorf("invalid verbose usage, out of date? %d", len(u))
}
for _, usage := range u {
diff --git a/internal/app/doc/bash b/internal/app/doc/bash
@@ -1,10 +1,14 @@
# {{ $.Executable }} completion
-_{{ $.Executable }}() {
+{{ $.Shell }}
+
+{{- range $idx, $profile := $.Profiles }}
+
+{{ $profile.Name }}() {
local cur opts
cur=${COMP_WORDS[COMP_CWORD]}
if [ "$COMP_CWORD" -eq 1 ]; then
-{{- range $idx, $value := $.Options }}
+{{- range $idx, $value := $profile.Options }}
opts="${opts}{{ $value }} "
{{- end}}
# shellcheck disable=SC2207
@@ -15,14 +19,14 @@ _{{ $.Executable }}() {
"{{ $.HelpCommand }}")
opts="{{ $.HelpAdvancedCommand }}"
;;
-{{- if not $.ReadOnly }}
-{{- if $.CanList }}
+{{- if not $profile.ReadOnly }}
+{{- if $profile.CanList }}
"{{ $.InsertCommand }}" | "{{ $.MultiLineCommand }}" | "{{ $.MoveCommand }}" | "{{ $.RemoveCommand }}")
opts="$opts $({{ $.DoList }})"
;;
{{- end}}
{{- end}}
-{{- if $.CanTOTP }}
+{{- if $profile.CanTOTP }}
"{{ $.TOTPCommand }}")
opts="{{ $.TOTPListCommand }} "
{{- range $key, $value := .TOTPSubCommands }}
@@ -30,25 +34,25 @@ _{{ $.Executable }}() {
{{- end}}
;;
{{- end}}
-{{- if $.CanList }}
- "{{ $.ShowCommand }}" | "{{ $.JSONCommand }}"{{ if $.CanClip }} | "{{ $.ClipCommand }}" {{end}})
+{{- if $profile.CanList }}
+ "{{ $.ShowCommand }}" | "{{ $.JSONCommand }}"{{ if $profile.CanClip }} | "{{ $.ClipCommand }}" {{end}})
opts=$({{ $.DoList }})
;;
{{- end}}
esac
-{{- if $.CanList }}
+{{- if $profile.CanList }}
else
if [ "$COMP_CWORD" -eq 3 ]; then
case "${COMP_WORDS[1]}" in
-{{- if not $.ReadOnly }}
+{{- if not $profile.ReadOnly }}
"{{ $.MoveCommand }}")
opts=$({{ $.DoList }})
;;
{{- end }}
-{{- if $.CanTOTP }}
+{{- if $profile.CanTOTP }}
"{{ $.TOTPCommand }}")
case "${COMP_WORDS[2]}" in
-{{- range $key, $value := .TOTPSubCommands }}
+{{- range $key, $value := $profile.TOTPSubCommands }}
"{{ $value }}")
opts=$({{ $.DoTOTPList }})
;;
@@ -66,5 +70,6 @@ _{{ $.Executable }}() {
fi
fi
}
+{{- end}}
complete -F _{{ $.Executable }} -o bashdefault {{ $.Executable }}
diff --git a/internal/app/doc/shell b/internal/app/doc/shell
@@ -0,0 +1,25 @@
+# the default profile
+# {{ $.DefaultProfile.Env }}
+
+{{- range $idx, $profile := $.Profiles }}
+{{ if not $profile.IsDefault }}
+# settings:
+{{ $profile.Comment }}
+# {{ $profile.Env }}
+{{- end}}
+{{- end}}
+
+_{{ $.Executable }}() {
+ case "{{ $.CompletionEnv }}" in
+{{- range $idx, $profile := $.Profiles }}
+{{- if not $profile.IsDefault }}
+ "{{ $profile.Display }}")
+ {{ $profile.Name }}
+ ;;
+{{- end}}
+{{- end}}
+ *)
+ {{ $.DefaultProfile.Name }}
+ ;;
+ esac
+}
diff --git a/internal/app/doc/zsh b/internal/app/doc/zsh
@@ -1,6 +1,10 @@
#compdef _{{ $.Executable }} {{ $.Executable }}
-
-_{{ $.Executable }}() {
+
+{{ $.Shell }}
+
+{{- range $idx, $profile := $.Profiles }}
+
+{{ $profile.Name }}() {
local curcontext="$curcontext" state len
typeset -A opt_args
@@ -11,7 +15,7 @@ _{{ $.Executable }}() {
len=${#words[@]}
case $state in
main)
- _arguments '1:main:({{ range $idx, $value := $.Options }}{{ if gt $idx 0}} {{ end }}{{ $value }}{{ end }})'
+ _arguments '1:main:({{ range $idx, $value := $profile.Options }}{{ if gt $idx 0}} {{ end }}{{ $value }}{{ end }})'
;;
*)
case $words[2] in
@@ -20,8 +24,8 @@ _{{ $.Executable }}() {
compadd "$@" "{{ $.HelpAdvancedCommand }}"
fi
;;
-{{- if not $.ReadOnly }}
-{{- if $.CanList }}
+{{- if not $profile.ReadOnly }}
+{{- if $profile.CanList }}
"{{ $.InsertCommand }}" | "{{ $.MultiLineCommand }}" | "{{ $.RemoveCommand }}")
if [ "$len" -eq 3 ]; then
compadd "$@" $({{ $.DoList }})
@@ -36,13 +40,13 @@ _{{ $.Executable }}() {
;;
{{- end}}
{{- end}}
-{{- if $.CanTOTP }}
+{{- if $profile.CanTOTP }}
"{{ $.TOTPCommand }}")
case "$len" in
3)
compadd "$@" {{ $.TOTPListCommand }}{{ range $key, $value := .TOTPSubCommands }} {{ $value }}{{ end }}
;;
-{{- if $.CanList }}
+{{- if $profile.CanList }}
4)
case $words[3] in
{{- range $key, $value := .TOTPSubCommands }}
@@ -55,8 +59,8 @@ _{{ $.Executable }}() {
esac
;;
{{- end}}
-{{- if $.CanList }}
- "{{ $.ShowCommand }}" | "{{ $.JSONCommand }}"{{ if $.CanClip }} | "{{ $.ClipCommand }}" {{end}})
+{{- if $profile.CanList }}
+ "{{ $.ShowCommand }}" | "{{ $.JSONCommand }}"{{ if $profile.CanClip }} | "{{ $.ClipCommand }}" {{end}})
if [ "$len" -eq 3 ]; then
compadd "$@" $({{ $.DoList }})
fi
@@ -65,3 +69,4 @@ _{{ $.Executable }}() {
esac
esac
}
+{{- end}}
diff --git a/internal/app/info.go b/internal/app/info.go
@@ -62,41 +62,14 @@ func info(command string, args []string) ([]string, error) {
}
return config.Environ(), nil
case BashCommand, ZshCommand:
- defaultFlag := BashDefaultsCommand
- if command == ZshCommand {
- defaultFlag = ZshDefaultsCommand
- }
- defaults, err := getInfoDefault(args, defaultFlag)
- if err != nil {
- return nil, err
+ if len(args) != 0 {
+ return nil, fmt.Errorf("invalid %s command", command)
}
exe, err := exeName()
if err != nil {
return nil, err
}
- return GenerateCompletions(command == BashCommand, defaults, exe)
+ return GenerateCompletions(command == BashCommand, exe)
}
return nil, nil
}
-
-func getInfoDefault(args []string, possibleArg string) (bool, error) {
- first := false
- invalid := false
- switch len(args) {
- case 0:
- break
- case 1:
- arg := args[0]
- if arg == possibleArg {
- first = true
- } else {
- invalid = true
- }
- default:
- invalid = true
- }
- if invalid {
- return false, errors.New("invalid argument")
- }
- return first, nil
-}
diff --git a/internal/app/info_test.go b/internal/app/info_test.go
@@ -53,21 +53,13 @@ func TestBashInfo(t *testing.T) {
if buf.String() == "" {
t.Error("nothing written")
}
- buf = bytes.Buffer{}
- ok, err = app.Info(&buf, "bash", []string{"defaults"})
- if !ok || err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" {
- t.Error("nothing written")
- }
- if _, err = app.Info(&buf, "bash", []string{"default"}); err.Error() != "invalid argument" {
+ 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 argument" {
+ 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 argument" {
+ if _, err = app.Info(&buf, "bash", []string{"short"}); err.Error() != "invalid bash command" {
t.Errorf("invalid error: %v", err)
}
}
@@ -108,18 +100,10 @@ func TestZshInfo(t *testing.T) {
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" {
+ 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 argument" {
+ if _, err = app.Info(&buf, "zsh", []string{"test", "default"}); err.Error() != "invalid zsh command" {
t.Errorf("invalid error: %v", err)
}
}
diff --git a/internal/config/vars.go b/internal/config/vars.go
@@ -25,10 +25,12 @@ const (
JSONDataOutputBlank JSONOutputMode = "empty"
// JSONDataOutputRaw means the RAW (unencrypted) value is displayed
JSONDataOutputRaw JSONOutputMode = "plaintext"
+ // EnvironmentCompletionKey controls which completion function to use
+ EnvironmentCompletionKey = prefixKey + "COMPLETION_FUNCTION"
)
var (
- registry = []printer{EnvStore, envKeyMode, envKey, EnvNoClip, EnvNoColor, EnvInteractive, EnvReadOnly, EnvTOTPToken, EnvFormatTOTP, EnvMaxTOTP, EnvTOTPColorBetween, EnvClipPaste, EnvClipCopy, EnvClipMax, EnvPlatform, EnvNoTOTP, EnvHookDir, EnvClipOSC52, EnvKeyFile, EnvModTime, EnvJSONDataOutput, EnvHashLength, EnvConfig, envConfigExpands}
+ registry = []printer{EnvStore, envKeyMode, envKey, EnvNoClip, EnvNoColor, EnvInteractive, EnvReadOnly, EnvTOTPToken, EnvFormatTOTP, EnvMaxTOTP, EnvTOTPColorBetween, EnvClipPaste, EnvClipCopy, EnvClipMax, EnvPlatform, EnvNoTOTP, EnvHookDir, EnvClipOSC52, EnvKeyFile, EnvModTime, EnvJSONDataOutput, EnvHashLength, EnvConfig, envConfigExpands, EnvCompletion}
// Platforms represent the platforms that lockbox understands to run on
Platforms = []string{MacOSPlatform, WindowsLinuxPlatform, LinuxXPlatform, LinuxWaylandPlatform}
// TOTPDefaultColorWindow is the default coloring rules for totp
@@ -76,7 +78,9 @@ var (
// EnvFormatTOTP supports formatting the TOTP tokens for generation of tokens
EnvFormatTOTP = EnvironmentFormatter{environmentBase: environmentBase{key: EnvTOTPToken.key + "_FORMAT", desc: "Override the otpauth url used to store totp tokens. It must have ONE format\nstring ('%s') to insert the totp base code."}, fxn: formatterTOTP, allowed: "otpauth//url/%s/args..."}
// EnvConfig is the location of the config file to read environment variables from
- EnvConfig = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "ENV", desc: fmt.Sprintf("Allows setting a specific file of environment variables for lockbox\nto read and use as configuration values (an '.env' file). The keyword\n'%s' will disable this functionality the keyword '%s' will search\nfor a file in the following paths in user's home directory matching\nthe first file found.\n\ndefault search paths:\n%v\n\nNote that this setting is not output as part of the environment.", noEnvironment, detectEnvironment, detectEnvironmentPaths)}, canDefault: true, defaultValue: detectEnvironment, allowed: []string{detectEnvironment, fileExample, noEnvironment}}
+ EnvConfig = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "ENV", desc: fmt.Sprintf("Allows setting a specific file of environment variables for lockbox\nto read and use as configuration values (an '.env' file). The keyword\n'%s' will disable this functionality the keyword '%s' will search\nfor a file in the following paths in user's home directory matching\nthe first file found.\n\ndefault search paths:\n%v\n\nNote that this setting is not output as part of the environment.", noEnvironment, detectEnvironment, detectEnvironmentPaths)}, canDefault: true, defaultValue: detectEnvironment, allowed: []string{detectEnvironment, fileExample, noEnvironment}}
+ // EnvCompletion is the completion method to use
+ EnvCompletion = EnvironmentString{environmentBase: environmentBase{key: EnvironmentCompletionKey, desc: "Use to select the non-default completions,\nplease review the generated shell completion files for more information.", requirement: "must be exported via a shell variable"}, canDefault: false}
envKeyMode = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "KEYMODE", requirement: "must be set to a valid mode when using a key", desc: fmt.Sprintf("How to retrieve the database store password.\nSet to '%s' when only using a key file\nSet to '%s' to ignore the set key value", noKeyMode, IgnoreKeyMode), whenUnset: string(DefaultKeyMode)}, allowed: []string{string(askKeyMode), string(commandKeyMode), string(IgnoreKeyMode), string(noKeyMode), string(plainKeyMode)}, canDefault: true, defaultValue: ""}
envKey = EnvironmentString{environmentBase: environmentBase{requirement: requiredKeyOrKeyFile, key: prefixKey + "KEY", desc: fmt.Sprintf("The database key ('%s' mode) or command to run ('%s' mode)\nto retrieve the database password.", plainKeyMode, commandKeyMode)}, allowed: []string{commandArgsExample, "password"}, canDefault: false}
envConfigExpands = EnvironmentInt{environmentBase: environmentBase{key: EnvConfig.key + "_EXPANDS", desc: "The maximum number of times to expand the input env to resolve variables,\nset to 0 to disable expansion. This value can NOT be an expansion itself\nif set in the env config file."}, shortDesc: "max expands", allowZero: true, defaultValue: 20}
diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go
@@ -89,7 +89,7 @@ func TestListVariables(t *testing.T) {
known[trim] = struct{}{}
}
l := len(known)
- if l != 24 {
+ if l != 25 {
t.Errorf("invalid env count, outdated? %d", l)
}
}