lockbox

password manager
Log | Files | Refs | README | LICENSE

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:
Minternal/app/completions.go | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Minternal/app/completions_test.go | 120++++++++++++++++++++++++++++++++++++-------------------------------------------
Minternal/app/core.go | 6------
Minternal/app/core_test.go | 4++--
Minternal/app/doc/bash | 27++++++++++++++++-----------
Ainternal/app/doc/shell | 25+++++++++++++++++++++++++
Minternal/app/doc/zsh | 23++++++++++++++---------
Minternal/app/info.go | 33+++------------------------------
Minternal/app/info_test.go | 26+++++---------------------
Minternal/config/vars.go | 8++++++--
Minternal/config/vars_test.go | 2+-
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) } }