lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 70698f57783c8b360ecea63b9e0bb82dffb0958b
parent 3444cba24890566c1890cc67cff42895131b2e08
Author: Sean Enck <sean@ttypty.com>
Date:   Sat, 15 Oct 2022 12:00:17 -0400

completions can now be generated by lb

Diffstat:
Mcmd/main.go | 50+++++++++++++++++++++++++++++++++-----------------
Minternal/cli/completions.bash | 80++++++++++++++++++++++++++++++-------------------------------------------------
Minternal/cli/core.go | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/cli/core_test.go | 36+++++++++++++++++++++++++++++++++++-
4 files changed, 188 insertions(+), 68 deletions(-)

diff --git a/cmd/main.go b/cmd/main.go @@ -49,31 +49,47 @@ func main() { } } +func getInfoDefault(args []string, possibleArg string) (bool, error) { + defaults := false + invalid := false + switch len(args) { + case 2: + break + case 3: + if args[2] == possibleArg { + defaults = true + } else { + invalid = true + } + default: + invalid = true + } + if invalid { + return false, errors.New("invalid argument") + } + return defaults, nil +} + func processInfoCommands(command string, args []string) ([]string, error) { switch command { case cli.HelpCommand: return cli.Usage(), nil case cli.VersionCommand: return []string{fmt.Sprintf("version: %s", strings.TrimSpace(version))}, nil - case cli.EnvCommand: - printValues := true - invalid := false - switch len(args) { - case 2: - break - case 3: - if args[2] == cli.EnvDefaultsCommand { - printValues = false - } else { - invalid = true - } - default: - invalid = true + case cli.EnvCommand, cli.BashCommand: + defaultFlag := cli.BashDefaultsCommand + isEnv := command == cli.EnvCommand + if isEnv { + defaultFlag = cli.EnvDefaultsCommand + } + defaults, err := getInfoDefault(args, defaultFlag) + if err != nil { + return nil, err } - if invalid { - return nil, errors.New("invalid argument") + if isEnv { + return inputs.ListEnvironmentVariables(!defaults), nil } - return inputs.ListEnvironmentVariables(printValues), nil + return cli.BashCompletions(defaults) } return nil, nil } diff --git a/internal/cli/completions.bash b/internal/cli/completions.bash @@ -1,80 +1,60 @@ # bash completion for lb -*- shell-script -*- -_is_clip() { - if [ "$1" == "${2}clip" ]; then - echo 1 - else - echo 0 - fi -} - _lb() { - local cur opts clip_enabled needs readwrite - clip_enabled=" clip" - if [ -n "$LOCKBOX_NOCLIP" ]; then - if [ "$LOCKBOX_NOCLIP" == "yes" ]; then - clip_enabled="" - fi - fi - readwrite=" insert rm mv" - if [ -n "$LOCKBOX_READONLY" ]; then - if [ "$LOCKBOX_READONLY" == "yes" ]; then - readwrite="" - fi - fi + local cur opts needs cur=${COMP_WORDS[COMP_CWORD]} if [ "$COMP_CWORD" -eq 1 ]; then - opts="version help ls show env totp$readwrite find$clip_enabled" + {{range $idx, $value := $.Options }} + opts="${opts}{{ $value }} "{{end}} # shellcheck disable=SC2207 COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) else if [ "$COMP_CWORD" -eq 2 ]; then case ${COMP_WORDS[1]} in - "insert") - opts="-multi $(lb ls)" +{{ if not $.ReadOnly }} + "{{ $.InsertCommand }}") + opts="{{ $.InsertMultiCommand }} $({{ $.DoList }})" ;; - "mv") - opts=$(lb ls) + "{{ $.MoveCommand }}") + opts=$({{ $.DoList }}) ;; - "totp") - opts="-once -short "$(lb totp -list) - if [ -n "$clip_enabled" ]; then - opts="$opts -clip" - fi +{{end}} + "{{ $.TOTPCommand }}") + opts="{{ $.TOTPShortCommand }} {{ $.TOTPOnceCommand }} "$({{ $.DoTOTPList }}) +{{ if $.CanClip }} + opts="$opts {{ $.TOTPClipCommand }}" +{{end}} ;; - "show" | "rm" | "clip") - opts=$(lb ls) - if [ $(_is_clip "${COMP_WORDS[1]}" "") == 1 ]; then - if [ -z "$clip_enabled" ]; then - opts="" - fi - fi + "{{ $.ShowCommand }}" {{ if not $.ReadOnly }}| "{{ $.RemoveCommand }}" {{end}} {{ if $.CanClip }} | "{{ $.ClipCommand }}" {{end}}) + opts=$({{ $.DoList }}) ;; esac fi if [ "$COMP_CWORD" -eq 3 ]; then case "${COMP_WORDS[1]}" in - "insert") - if [ "${COMP_WORDS[2]}" == "-multi" ]; then - opts=$(lb ls) +{{ if not $.ReadOnly }} + "{{ $.InsertCommand }}") + if [ "${COMP_WORDS[2]}" == "{{ $.InsertMultiCommand }}" ]; then + opts=$({{ $.DoList }}) fi ;; - "mv") - opts=$(lb ls) + "{{ $.MoveCommand }}") + opts=$({{ $.DoList }}) ;; - "totp") +{{end}} + "{{ $.TOTPCommand }}") needs=0 - if [ "${COMP_WORDS[2]}" == "-once" ] || [ "${COMP_WORDS[2]}" == "-short" ]; then + if [ "${COMP_WORDS[2]}" == "{{ $.TOTPOnceCommand }}" ] || [ "${COMP_WORDS[2]}" == "{{ $.TOTPShortCommand }}" ]; then needs=1 else - if [ -n "$clip_enabled" ]; then - if [ $(_is_clip "${COMP_WORDS[2]}" "-") == 1 ]; then - needs=1 - fi +{{ if $.CanClip }} + if [ "${COMP_WORDS[2]}" == "{{ $.TOTPClipCommand }}" ]; then + needs=1 fi +{{end}} fi if [ $needs -eq 1 ]; then - opts=$(lb totp -list) + opts=$({{ $.DoTOTPList }}) fi ;; esac diff --git a/internal/cli/core.go b/internal/cli/core.go @@ -2,8 +2,13 @@ package cli import ( + "bytes" + _ "embed" "fmt" "sort" + "text/template" + + "github.com/enckse/lockbox/internal/inputs" ) const ( @@ -45,6 +50,36 @@ const ( TOTPOnceCommand = "-once" // EnvDefaultsCommand will display the default env variables, not those set EnvDefaultsCommand = "-defaults" + // BashCommand is the command to generate bash completions + BashCommand = "bash" + // BashDefaultsCommand will generate environment agnostic completions + BashDefaultsCommand = "-defaults" +) + +var ( + //go:embed "completions.bash" + bashCompletions string +) + +type ( + // Completions handles the inputs to completions for templating + Completions struct { + Options []string + CanClip bool + ReadOnly bool + InsertCommand string + TOTPShortCommand string + TOTPOnceCommand string + TOTPClipCommand string + InsertMultiCommand string + RemoveCommand string + ClipCommand string + ShowCommand string + MoveCommand string + TOTPCommand string + DoTOTPList string + DoList string + } ) func subCommand(parent, name, args, desc string) string { @@ -63,9 +98,64 @@ func commandText(args, name, desc string) string { return fmt.Sprintf(" %-15s %-10s %s", name, arguments, desc) } +// BashCompletions handles creating bash completion outputs +func BashCompletions(defaults bool) ([]string, error) { + c := Completions{ + InsertCommand: InsertCommand, + RemoveCommand: RemoveCommand, + TOTPShortCommand: TOTPShortCommand, + TOTPClipCommand: TOTPClipCommand, + TOTPOnceCommand: TOTPOnceCommand, + ClipCommand: ClipCommand, + ShowCommand: ShowCommand, + InsertMultiCommand: InsertMultiCommand, + TOTPCommand: TOTPCommand, + MoveCommand: MoveCommand, + DoList: fmt.Sprintf("lb %s", ListCommand), + DoTOTPList: fmt.Sprintf("lb %s %s", TOTPCommand, TOTPListCommand), + } + isReadOnly := false + isClip := true + if !defaults { + ro, err := inputs.IsReadOnly() + if err != nil { + return nil, err + } + isReadOnly = ro + noClip, err := inputs.IsNoClipEnabled() + if err != nil { + return nil, err + } + if noClip { + isClip = false + } + } + c.CanClip = isClip + c.ReadOnly = isReadOnly + options := []string{EnvCommand, FindCommand, HelpCommand, ListCommand, ShowCommand, TOTPCommand, VersionCommand} + if c.CanClip { + options = append(options, ClipCommand) + } + if !c.ReadOnly { + options = append(options, MoveCommand, RemoveCommand, InsertCommand) + } + c.Options = options + t, err := template.New("t").Parse(bashCompletions) + if err != nil { + return nil, err + } + var buf bytes.Buffer + if err := t.Execute(&buf, c); err != nil { + return nil, err + } + return []string{buf.String()}, nil +} + // Usage return usage information func Usage() []string { var results []string + results = append(results, command(BashCommand, "", "generate bash completions")) + results = append(results, subCommand(BashCommand, BashDefaultsCommand, "", "generate default bash completion, not user environment specific")) 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(FindCommand, "criteria", "perform a simplistic text search over the entry keys")) diff --git a/internal/cli/core_test.go b/internal/cli/core_test.go @@ -1,6 +1,7 @@ package cli_test import ( + "os" "testing" "github.com/enckse/lockbox/internal/cli" @@ -8,7 +9,40 @@ import ( func TestUsage(t *testing.T) { u := cli.Usage() - if len(u) != 17 { + if len(u) != 19 { t.Errorf("invalid usage, out of date? %d", len(u)) } } + +func TestCompletionsBash(t *testing.T) { + os.Setenv("LOCKBOX_READONLY", "yes") + os.Setenv("LOCKBOX_NOCLIP", "yes") + defaults, _ := cli.BashCompletions(true) + roNoClip, _ := cli.BashCompletions(false) + if roNoClip[0] == defaults[0] { + t.Error("should not equal defaults") + } + os.Setenv("LOCKBOX_READONLY", "") + os.Setenv("LOCKBOX_NOCLIP", "yes") + noClip, _ := cli.BashCompletions(false) + if roNoClip[0] == noClip[0] || noClip[0] == defaults[0] { + t.Error("readonly/noclip != noclip (nor defaults)") + } + os.Setenv("LOCKBOX_READONLY", "yes") + os.Setenv("LOCKBOX_NOCLIP", "") + ro, _ := cli.BashCompletions(false) + if roNoClip[0] == ro[0] || noClip[0] == ro[0] || ro[0] == defaults[0] { + t.Error("readonly/noclip != ro (nor ro == noclip, nor ro == defaults)") + } + os.Setenv("LOCKBOX_READONLY", "") + os.Setenv("LOCKBOX_NOCLIP", "") + isDefaultsToo, _ := cli.BashCompletions(false) + if isDefaultsToo[0] != defaults[0] { + t.Error("defaults should match env defaults") + } + for _, confirm := range [][]string{defaults, roNoClip, noClip, ro, isDefaultsToo} { + if len(confirm) != 1 { + t.Error("completions returned an invalid array") + } + } +}