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:
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")
+ }
+ }
+}