commit a447ba980ef18e7176344067fe0932f86f69eea2
parent 73db5099d11ea637afb80b55196a2b0a308ca6be
Author: Sean Enck <sean@ttypty.com>
Date: Sun, 7 Sep 2025 09:04:44 -0400
add 'fields' command which gets groups+allowed fields - improves insert completion
Diffstat:
12 files changed, 180 insertions(+), 23 deletions(-)
diff --git a/cmd/lb/main.go b/cmd/lb/main.go
@@ -82,8 +82,14 @@ func run() error {
return app.Health(p)
case commands.ReKey:
return app.ReKey(p)
- case commands.List, commands.Groups:
- return app.List(p, command == commands.Groups)
+ case commands.List, commands.Groups, commands.Fields:
+ mode := app.ListEntriesMode
+ if command == commands.Groups {
+ mode = app.ListGroupsMode
+ } else if command == commands.Fields {
+ mode = app.ListFieldsMode
+ }
+ return app.List(p, mode)
case commands.Unset:
return app.Unset(p)
case commands.Move:
diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go
@@ -217,6 +217,7 @@ func test(profile string) error {
r.section("listing/outputs")
r.run("", "ls")
r.run("", "groups")
+ r.run("", "fields")
r.run("echo y |", "rm test2/key1")
r.logAppend("echo")
r.run("", "ls")
@@ -254,6 +255,7 @@ func test(profile string) error {
r.run("", "ls")
r.run("", "groups")
r.run("", "groups 'test9/**/*'")
+ r.run("", "fields 'test9/**/*'")
r.run("echo y |", "unset test8/unset/password")
r.logAppend("echo")
r.run("", "ls")
@@ -267,6 +269,7 @@ func test(profile string) error {
r.run("", "mv test9/key2/sub1 test9/sub3")
r.run("", "ls")
r.run("", "groups")
+ r.run("", "fields")
r.run("echo y |", "rm test9/*")
r.logAppend("echo")
diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log
@@ -43,6 +43,50 @@ test8/unset
test9/key1/sub1
test9/key1/sub2
test9/key2/sub1
+test1/key1/notes
+test1/key1/otp
+test1/key1/password
+test1/key1/url
+test2/key1/notes
+test2/key1/otp
+test2/key1/password
+test2/key1/url
+test4/multiline/notes
+test4/multiline/otp
+test4/multiline/password
+test4/multiline/url
+test5/multiline/notes
+test5/multiline/otp
+test5/multiline/password
+test5/multiline/url
+test6/multiline/notes
+test6/multiline/otp
+test6/multiline/password
+test6/multiline/url
+test7/deeper/root/notes
+test7/deeper/root/otp
+test7/deeper/root/password
+test7/deeper/root/url
+test7/deeper/rooted/notes
+test7/deeper/rooted/otp
+test7/deeper/rooted/password
+test7/deeper/rooted/url
+test8/unset/notes
+test8/unset/otp
+test8/unset/password
+test8/unset/url
+test9/key1/sub1/notes
+test9/key1/sub1/otp
+test9/key1/sub1/password
+test9/key1/sub1/url
+test9/key1/sub2/notes
+test9/key1/sub2/otp
+test9/key1/sub2/password
+test9/key1/sub2/url
+test9/key2/sub1/notes
+test9/key2/sub1/otp
+test9/key2/sub1/password
+test9/key2/sub1/url
delete entry? (y/N)
test1/key1/password
test4/multiline/notes
@@ -224,6 +268,18 @@ test9/key2/sub1
test9/key1/sub1
test9/key1/sub2
test9/key2/sub1
+test9/key1/sub1/notes
+test9/key1/sub1/otp
+test9/key1/sub1/password
+test9/key1/sub1/url
+test9/key1/sub2/notes
+test9/key1/sub2/otp
+test9/key1/sub2/password
+test9/key1/sub2/url
+test9/key2/sub1/notes
+test9/key2/sub1/otp
+test9/key2/sub1/password
+test9/key2/sub1/url
unset: test8/unset/password? (y/N) clearing value from: test8/unset/password
test4/multiline/notes
@@ -273,6 +329,30 @@ test6/multiline
test9/sub1
test9/sub2
test9/sub3
+test4/multiline/notes
+test4/multiline/otp
+test4/multiline/password
+test4/multiline/url
+test5/multiline/notes
+test5/multiline/otp
+test5/multiline/password
+test5/multiline/url
+test6/multiline/notes
+test6/multiline/otp
+test6/multiline/password
+test6/multiline/url
+test9/sub1/notes
+test9/sub1/otp
+test9/sub1/password
+test9/sub1/url
+test9/sub2/notes
+test9/sub2/otp
+test9/sub2/password
+test9/sub2/url
+test9/sub3/notes
+test9/sub3/otp
+test9/sub3/password
+test9/sub3/url
selected entities:
test9/sub1
test9/sub2
diff --git a/internal/app/commands/core.go b/internal/app/commands/core.go
@@ -66,6 +66,7 @@ const (
TOTPSeed = "seed"
// Health will show health information (for debugging/troubleshooting)
Health = "health"
+ Fields = "fields"
)
var (
diff --git a/internal/app/completions/core.go b/internal/app/completions/core.go
@@ -28,6 +28,7 @@ type (
TOTPCommand string
DoTOTPList string
DoList string
+ DoFields string
DoGroups string
Executable string
JSONCommand string
@@ -66,6 +67,7 @@ func Generate(completionType, exe string) ([]string, error) {
DoGroups: fmt.Sprintf("%s %s", exe, commands.Groups),
DoTOTPList: fmt.Sprintf("%s %s %s", exe, commands.TOTP, commands.TOTPList),
ExportCommand: fmt.Sprintf("%s %s %s", exe, commands.Env, commands.Completions),
+ DoFields: fmt.Sprintf("%s %s", exe, commands.Fields),
}
c.Options = commands.AllowedInReadOnly(commands.Help, commands.List, commands.Show, commands.Version, commands.JSON, commands.Groups, commands.Move, commands.Remove, commands.Insert, commands.Unset)
diff --git a/internal/app/completions/shell/bash.sh b/internal/app/completions/shell/bash.sh
@@ -31,7 +31,10 @@ _{{ $.Executable }}() {
"{{ $.MoveCommand }}" | "{{ $.RemoveCommand }}")
opts="$opts $({{ $.DoGroups }})"
;;
- "{{ $.UnsetCommand }}" | "{{ $.InsertCommand }}")
+ "{{ $.InsertCommand }}")
+ opts="$opts $({{ $.DoFields }})"
+ ;;
+ "{{ $.UnsetCommand }}")
opts="$opts $({{ $.DoList }})"
;;
"{{ $.TOTPCommand }}")
diff --git a/internal/app/completions/shell/zsh.sh b/internal/app/completions/shell/zsh.sh
@@ -45,7 +45,12 @@ _{{ $.Executable }}() {
compadd "$@" $({{ $.DoGroups }})
fi
;;
- "{{ $.UnsetCommand }}" | "{{ $.InsertCommand }}")
+ "{{ $.InsertCommand }}")
+ if [ "$len" -eq 3 ]; then
+ compadd "$@" $({{ $.DoFields }})
+ fi
+ ;;
+ "{{ $.UnsetCommand }}")
if [ "$len" -eq 3 ]; then
compadd "$@" $({{ $.DoList }})
fi
diff --git a/internal/app/help/core.go b/internal/app/help/core.go
@@ -96,6 +96,7 @@ func Usage(verbose bool, exe string) ([]string, error) {
results = append(results, command(commands.JSON, isFilter, "display detailed information"))
results = append(results, command(commands.List, isFilter, "list entries"))
results = append(results, command(commands.Groups, isFilter, "list groups"))
+ results = append(results, command(commands.Fields, isFilter, "list groups with all allowed field names"))
results = append(results, command(commands.Show, isEntry, "show the entry's value"))
results = append(results, command(commands.TOTP, "<command>", "display an updating totp generated code"))
results = append(results, subCommand(commands.TOTP, commands.TOTPClip, isEntry, "copy totp code to clipboard"))
diff --git a/internal/app/help/core_test.go b/internal/app/help/core_test.go
@@ -9,11 +9,11 @@ import (
func TestUsage(t *testing.T) {
u, _ := help.Usage(false, "lb")
- if len(u) != 28 {
+ if len(u) != 29 {
t.Errorf("invalid usage, out of date? %d", len(u))
}
u, _ = help.Usage(true, "lb")
- if len(u) != 129 {
+ if len(u) != 130 {
t.Errorf("invalid verbose usage, out of date? %d", len(u))
}
for _, usage := range u {
diff --git a/internal/app/list.go b/internal/app/list.go
@@ -10,8 +10,16 @@ import (
"github.com/enckse/lockbox/internal/kdbx"
)
+type ListMode int
+
+const (
+ ListEntriesMode ListMode = iota
+ ListGroupsMode
+ ListFieldsMode
+)
+
// List will list/find entries
-func List(cmd CommandOptions, groups bool) error {
+func List(cmd CommandOptions, mode ListMode) error {
args := cmd.Args()
filter := ""
switch len(args) {
@@ -23,10 +31,10 @@ func List(cmd CommandOptions, groups bool) error {
return errors.New("too many arguments (none or filter)")
}
- return doList("", filter, cmd, groups)
+ return doList("", filter, cmd, mode)
}
-func doList(attr, filter string, cmd CommandOptions, groups bool) error {
+func doList(attr, filter string, cmd CommandOptions, mode ListMode) error {
hasFilter, selector := createFilter(filter)
opts := kdbx.QueryOptions{}
opts.Mode = kdbx.ListMode
@@ -42,17 +50,35 @@ func doList(attr, filter string, cmd CommandOptions, groups bool) error {
}
w := cmd.Writer()
attrFilter := attr != ""
+ isFields := mode == ListFieldsMode
+ allowedFields := []string{}
+ if isFields {
+ for _, allowed := range kdbx.AllowedFields {
+ allowedFields = append(allowedFields, strings.ToLower(allowed))
+ }
+ sort.Strings(allowedFields)
+ }
+ isGroups := mode == ListGroupsMode || isFields
for f, err := range e {
if err != nil {
return err
}
- if groups {
+ if isGroups {
ok, err := allowed(f.Path)
if err != nil {
return err
}
if ok {
- fmt.Fprintf(w, "%s\n", f.Path)
+ output := []string{f.Path}
+ if isFields {
+ output = []string{}
+ for _, allowed := range allowedFields {
+ output = append(output, kdbx.NewPath(f.Path, allowed))
+ }
+ }
+ for _, out := range output {
+ fmt.Fprintf(w, "%s\n", out)
+ }
}
continue
}
diff --git a/internal/app/list_test.go b/internal/app/list_test.go
@@ -42,14 +42,14 @@ func setup(t *testing.T) *kdbx.Transaction {
func TestList(t *testing.T) {
m := newMockCommand(t)
- if err := app.List(m, false); err != nil {
+ if err := app.List(m, app.ListEntriesMode); err != nil {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() == "" {
t.Error("nothing listed")
}
m.args = []string{"test", "test2"}
- if err := app.List(m, false); err == nil || err.Error() != "too many arguments (none or filter)" {
+ if err := app.List(m, app.ListEntriesMode); err == nil || err.Error() != "too many arguments (none or filter)" {
t.Errorf("invalid error: %v", err)
}
}
@@ -57,7 +57,7 @@ func TestList(t *testing.T) {
func TestFind(t *testing.T) {
m := newMockCommand(t)
m.args = []string{"a/["}
- if err := app.List(m, false); err == nil || !strings.Contains(err.Error(), "syntax error in pattern") {
+ if err := app.List(m, app.ListEntriesMode); err == nil || !strings.Contains(err.Error(), "syntax error in pattern") {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() != "" {
@@ -65,7 +65,7 @@ func TestFind(t *testing.T) {
}
m.buf.Reset()
m.args = []string{"[zzzzzz]"}
- if err := app.List(m, false); err != nil {
+ if err := app.List(m, app.ListEntriesMode); err != nil {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() != "" {
@@ -73,7 +73,7 @@ func TestFind(t *testing.T) {
}
m.buf.Reset()
m.args = []string{"test"}
- if err := app.List(m, false); err != nil {
+ if err := app.List(m, app.ListEntriesMode); err != nil {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() == "" {
@@ -81,7 +81,7 @@ func TestFind(t *testing.T) {
}
m.buf.Reset()
m.args = []string{"test"}
- if err := app.List(m, true); err != nil {
+ if err := app.List(m, app.ListGroupsMode); err != nil {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() == "" {
@@ -89,7 +89,7 @@ func TestFind(t *testing.T) {
}
m.buf.Reset()
m.args = []string{"test/**/**/*"}
- if err := app.List(m, false); err != nil {
+ if err := app.List(m, app.ListEntriesMode); err != nil {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() == "" {
@@ -97,7 +97,7 @@ func TestFind(t *testing.T) {
}
m.buf.Reset()
m.args = []string{"test/**/*"}
- if err := app.List(m, true); err != nil {
+ if err := app.List(m, app.ListGroupsMode); err != nil {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() == "" {
@@ -105,7 +105,23 @@ func TestFind(t *testing.T) {
}
m.buf.Reset()
m.args = []string{"[zzzz]"}
- if err := app.List(m, true); err != nil {
+ if err := app.List(m, app.ListGroupsMode); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if m.buf.String() != "" {
+ t.Error("something listed")
+ }
+ m.buf.Reset()
+ m.args = []string{"test/**/*"}
+ if err := app.List(m, app.ListFieldsMode); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if m.buf.String() == "" {
+ t.Error("nothing listed")
+ }
+ m.buf.Reset()
+ m.args = []string{"[zzzz]"}
+ if err := app.List(m, app.ListFieldsMode); err != nil {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() != "" {
@@ -115,14 +131,28 @@ func TestFind(t *testing.T) {
func TestGroups(t *testing.T) {
m := newMockCommand(t)
- if err := app.List(m, true); err != nil {
+ if err := app.List(m, app.ListGroupsMode); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if m.buf.String() == "" {
+ t.Errorf("nothing listed: %s", m.buf.String())
+ }
+ m.args = []string{"test", "test2"}
+ if err := app.List(m, app.ListGroupsMode); err == nil || err.Error() != "too many arguments (none or filter)" {
+ t.Errorf("invalid error: %v", err)
+ }
+}
+
+func TestFields(t *testing.T) {
+ m := newMockCommand(t)
+ if err := app.List(m, app.ListFieldsMode); err != nil {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() == "" {
t.Errorf("nothing listed: %s", m.buf.String())
}
m.args = []string{"test", "test2"}
- if err := app.List(m, true); err == nil || err.Error() != "too many arguments (none or filter)" {
+ if err := app.List(m, app.ListFieldsMode); err == nil || err.Error() != "too many arguments (none or filter)" {
t.Errorf("invalid error: %v", err)
}
}
diff --git a/internal/app/totp.go b/internal/app/totp.go
@@ -178,7 +178,7 @@ func (args *TOTPArguments) Do(opts TOTPOptions) error {
return errors.New("invalid option functions")
}
if args.Mode == commands.TOTPList {
- return doList(kdbx.OTPField, args.Entry, opts.app, false)
+ return doList(kdbx.OTPField, args.Entry, opts.app, ListEntriesMode)
}
return args.display(opts)
}