lockbox

password manager
Log | Files | Refs | README | LICENSE

commit f0da2e7d9f80a0111e880ab09c1c2ab715ffdab2
parent 840604dc78a10f6e6db85385633113af37785258
Author: Sean Enck <sean@ttypty.com>
Date:   Sat,  7 Jun 2025 16:23:31 -0400

groups/ls should do find on their own

Diffstat:
Mcmd/lb/main.go | 4++--
Mcmd/lb/main_test.go | 6++++--
Mcmd/lb/tests/expected.log | 19++++++++++++++++---
Minternal/app/commands/core.go | 4----
Minternal/app/completions/core.go | 4+---
Minternal/app/completions/shell/bash.sh | 1-
Minternal/app/completions/shell/zsh.sh | 1-
Minternal/app/help/core.go | 8+++-----
Minternal/app/help/core_test.go | 4++--
Minternal/app/list.go | 19+++++++------------
Minternal/app/list_test.go | 37+++++++++----------------------------
Minternal/app/totp.go | 16++++++++--------
Minternal/app/totp_test.go | 12++++++------
13 files changed, 58 insertions(+), 77 deletions(-)

diff --git a/cmd/lb/main.go b/cmd/lb/main.go @@ -82,8 +82,8 @@ func run() error { switch command { case commands.ReKey: return app.ReKey(p) - case commands.List, commands.Find, commands.Groups: - return app.List(p, command == commands.Find, command == commands.Groups) + case commands.List, commands.Groups: + return app.List(p, command == commands.Groups) case commands.Unset: return app.Unset(p) case commands.Move: diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go @@ -207,7 +207,7 @@ func test(profile string) error { for _, k := range []string{"insert test4/multiline/notes", "insert test5/multiline/notes", "insert test5/multiline/otp", "insert test5/multiline/password"} { r.run(`printf "testing3\ntesting4\n" |`, k) } - for _, k := range []string{"insert test6/multiline/password", "insert test6/multiline/notes", "insert test7/deeper/rooted/notes", "insert test8/unset/password", "insert test8/unset/notes", "insert test9/key1/sub1/password", "insert test9/key1/sub2/password", "insert test9/key2/sub1/password"} { + for _, k := range []string{"insert test6/multiline/password", "insert test6/multiline/notes", "insert test7/deeper/rooted/notes", "insert test7/deeper/rooted/otp", "insert test8/unset/password", "insert test8/unset/notes", "insert test9/key1/sub1/password", "insert test9/key1/sub2/password", "insert test9/key2/sub1/password"} { r.run(`printf "testing5" |`, k) r.run("", fmt.Sprintf("show %s", strings.ReplaceAll(k, "insert ", ""))) } @@ -216,12 +216,13 @@ func test(profile string) error { r.run("echo y |", "rm test2/key1") r.logAppend("echo") r.run("", "ls") + r.run("", "ls multiline") r.run("", "json") r.run("", "json 'multiline'") r.logAppend("echo") r.run("echo 5ae472abqdekjqykoyxk7hvc2leklq5n |", "insert test6/multiline/otp") r.run("", "totp ls") - r.run("", "totp find multiline") + r.run("", "totp ls rooted") r.run("", "totp show test6/multiline/otp") r.run("", "totp once test6/multiline/otp") r.run("", "totp minimal test6/multiline/otp") @@ -235,6 +236,7 @@ func test(profile string) error { r.logAppend("echo") r.run("", "ls") r.run("", "groups") + r.run("", "groups test9") r.run("echo y |", "unset test8/unset/password") r.logAppend("echo") r.run("", "ls") diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -7,6 +7,7 @@ password can NOT be multi-line testing5 testing5 testing5 +otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=testing5 testing5 testing5 testing5 @@ -20,6 +21,7 @@ test5/multiline/notes test6/multiline/notes test6/multiline/password test7/deeper/rooted/notes +test7/deeper/rooted/otp test8/unset/notes test8/unset/password test9/key1/sub1/password @@ -42,11 +44,16 @@ test5/multiline/notes test6/multiline/notes test6/multiline/password test7/deeper/rooted/notes +test7/deeper/rooted/otp test8/unset/notes test8/unset/password test9/key1/sub1/password test9/key1/sub2/password test9/key2/sub1/password +test4/multiline/notes +test5/multiline/notes +test6/multiline/notes +test6/multiline/password { "test1/key1": { "modtime": "XXXX-XX-XX", @@ -67,7 +74,8 @@ test9/key2/sub1/password }, "test7/deeper/rooted": { "modtime": "XXXX-XX-XX", - "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", + "otp": "3d126ef25692664fd102e72f4ebaebef79c0cf6a8ea442af099dd47d0a89fab9c9b2cb9b77e605b1d00d91c666c1f7d651a9bb2e3a06406fdd1f6155861eae55" }, "test8/unset": { "modtime": "XXXX-XX-XX", @@ -104,7 +112,8 @@ test9/key2/sub1/password } test6/multiline/otp -test6/multiline/otp +test7/deeper/rooted/otp +test7/deeper/rooted/otp XXXXXX XXXXXX XXXXXX @@ -128,7 +137,8 @@ XXXXXX } "test7/deeper/rooted": { "modtime": "XXXX-XX-XX", - "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", + "otp": "3d126ef25692664fd102e72f4ebaebef79c0cf6a8ea442af099dd47d0a89fab9c9b2cb9b77e605b1d00d91c666c1f7d651a9bb2e3a06406fdd1f6155861eae55" } "test8/unset": { "modtime": "XXXX-XX-XX", @@ -169,6 +179,9 @@ test8/unset test9/key1/sub1 test9/key1/sub2 test9/key2/sub1 +test9/key1/sub1 +test9/key1/sub2 +test9/key2/sub1 unset: test8/unset/password? (y/N) clearing value from: test8/unset/password test4/multiline/notes diff --git a/internal/app/commands/core.go b/internal/app/commands/core.go @@ -10,8 +10,6 @@ const ( Clear = "clear" // Clip will copy values to the clipboard Clip = "clip" - // Find is for simplistic searching of entries - Find = "find" // Insert adds a value Insert = "insert" // List lists all entries @@ -38,8 +36,6 @@ const ( TOTPMinimal = "minimal" // TOTPList will list the totp-enabled entries TOTPList = List - // TOTPFind allows a filter search of TOTP listed entries - TOTPFind = Find // TOTPOnce will perform like a normal totp request but not refresh TOTPOnce = "once" // CompletionsBash is the command to generate bash completions diff --git a/internal/app/completions/core.go b/internal/app/completions/core.go @@ -19,7 +19,6 @@ type ( Template struct { InsertCommand string TOTPListCommand string - TOTPFindCommand string RemoveCommand string UnsetCommand string ClipCommand string @@ -108,7 +107,6 @@ func Generate(completionType, exe string) ([]string, error) { UnsetCommand: commands.Unset, RemoveCommand: commands.Remove, TOTPListCommand: commands.TOTPList, - TOTPFindCommand: commands.TOTPFind, ClipCommand: commands.Clip, ShowCommand: commands.Show, JSONCommand: commands.JSON, @@ -124,7 +122,7 @@ func Generate(completionType, exe string) ([]string, error) { } c.Conditionals = NewConditionals() - c.Options = c.newGenOptions([]string{commands.Help, commands.List, commands.Show, commands.Version, commands.JSON, commands.Find, commands.Groups}, + c.Options = c.newGenOptions([]string{commands.Help, commands.List, commands.Show, commands.Version, commands.JSON, commands.Groups}, map[string]string{ commands.Clip: c.Conditionals.Not.CanClip, commands.TOTP: c.Conditionals.Not.CanTOTP, diff --git a/internal/app/completions/shell/bash.sh b/internal/app/completions/shell/bash.sh @@ -45,7 +45,6 @@ _{{ $.Executable }}() { ;; "{{ $.TOTPCommand }}") opts="{{ $.TOTPListCommand }} " - opts="{{ $.TOTPFindCommand }} " {{- range $key, $value := .TOTPSubCommands }} if {{ $value.Conditional }}; then opts="$opts {{ $value.Key }}" diff --git a/internal/app/completions/shell/zsh.sh b/internal/app/completions/shell/zsh.sh @@ -72,7 +72,6 @@ _{{ $.Executable }}() { case "$len" in 3) compadd "$@" {{ $.TOTPListCommand }} - compadd "$@" {{ $.TOTPFindCommand }} {{- range $key, $value := .TOTPSubCommands }} if {{ $value.Conditional }}; then compadd "$@" {{ $value.Key }} diff --git a/internal/app/help/core.go b/internal/app/help/core.go @@ -81,9 +81,8 @@ func Usage(verbose bool, exe string) ([]string, error) { results = append(results, command(commands.Insert, isEntry, "insert a new entry into the store")) results = append(results, command(commands.Unset, isEntry, "clear an entry value")) results = append(results, command(commands.JSON, isFilter, "display detailed information")) - results = append(results, command(commands.List, "", "list entries")) - results = append(results, command(commands.Groups, "", "list groups")) - results = append(results, command(commands.Find, isFilter, "find matching entries")) + results = append(results, command(commands.List, isFilter, "list entries")) + results = append(results, command(commands.Groups, isFilter, "list groups")) results = append(results, command(commands.Move, "src dst", "move a group from source to destination")) results = append(results, command(commands.PasswordGenerate, "", "generate a password")) results = append(results, command(commands.ReKey, "", "rekey/reinitialize the database credentials")) @@ -91,11 +90,10 @@ func Usage(verbose bool, exe string) ([]string, error) { results = append(results, command(commands.Show, isEntry, "show the entry's value")) results = append(results, command(commands.TOTP, isEntry, "display an updating totp generated code")) results = append(results, subCommand(commands.TOTP, commands.TOTPClip, isEntry, "copy totp code to clipboard")) - results = append(results, subCommand(commands.TOTP, commands.TOTPList, "", "list entries with totp settings")) + results = append(results, subCommand(commands.TOTP, commands.TOTPList, isFilter, "list entries with totp settings")) results = append(results, subCommand(commands.TOTP, commands.TOTPOnce, isEntry, "display the first generated code")) results = append(results, subCommand(commands.TOTP, commands.TOTPMinimal, isEntry, "display one generated code (no details)")) results = append(results, subCommand(commands.TOTP, commands.TOTPShow, isEntry, "show the totp entry")) - results = append(results, subCommand(commands.TOTP, commands.TOTPFind, isFilter, "find matching entries with totp settings")) results = append(results, command(commands.Version, "", "display version information")) sort.Strings(results) usage := []string{fmt.Sprintf("%s usage:", exe)} 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) != 26 { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = help.Usage(true, "lb") - if len(u) != 92 { + if len(u) != 90 { 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 @@ -11,21 +11,16 @@ import ( ) // List will list/find entries -func List(cmd CommandOptions, isFind, groups bool) error { - if isFind && groups { - return errors.New("groups+find not supported") - } +func List(cmd CommandOptions, groups bool) error { args := cmd.Args() filter := "" - if isFind { - if len(args) != 1 { - return errors.New("find requires one argument") - } + switch len(args) { + case 0: + break + case 1: filter = args[0] - } else { - if len(args) != 0 { - return errors.New("arguments not supported") - } + default: + return errors.New("too many arguments (none or filter)") } return doList("", filter, cmd, groups) diff --git a/internal/app/list_test.go b/internal/app/list_test.go @@ -43,37 +43,22 @@ func setup(t *testing.T) *backend.Transaction { func TestList(t *testing.T) { m := newMockCommand(t) - if err := app.List(m, false, false); err != nil { + if err := app.List(m, false); err != nil { t.Errorf("invalid error: %v", err) } if m.buf.String() == "" { t.Error("nothing listed") } - m.args = []string{"test"} - if err := app.List(m, false, false); err == nil || err.Error() != "arguments not supported" { + m.args = []string{"test", "test2"} + if err := app.List(m, false); err == nil || err.Error() != "too many arguments (none or filter)" { t.Errorf("invalid error: %v", err) } } func TestFind(t *testing.T) { m := newMockCommand(t) - if err := app.List(m, true, false); err == nil || err.Error() != "find requires one argument" { - t.Errorf("invalid error: %v", err) - } - if m.buf.String() != "" { - t.Error("something listed") - } - m.buf.Reset() m.args = []string{"["} - if err := app.List(m, true, false); err == nil || !strings.Contains(err.Error(), "missing closing") { - t.Errorf("invalid error: %v", err) - } - if m.buf.String() != "" { - t.Error("something listed") - } - m.buf.Reset() - m.args = []string{"test", "1"} - if err := app.List(m, true, false); err == nil || err.Error() != "find requires one argument" { + if err := app.List(m, false); err == nil || !strings.Contains(err.Error(), "missing closing") { t.Errorf("invalid error: %v", err) } if m.buf.String() != "" { @@ -81,7 +66,7 @@ func TestFind(t *testing.T) { } m.buf.Reset() m.args = []string{"[zzzzzz]"} - if err := app.List(m, true, false); err != nil { + if err := app.List(m, false); err != nil { t.Errorf("invalid error: %v", err) } if m.buf.String() != "" { @@ -89,7 +74,7 @@ func TestFind(t *testing.T) { } m.buf.Reset() m.args = []string{"test"} - if err := app.List(m, true, false); err != nil { + if err := app.List(m, false); err != nil { t.Errorf("invalid error: %v", err) } if m.buf.String() == "" { @@ -99,18 +84,14 @@ func TestFind(t *testing.T) { func TestGroups(t *testing.T) { m := newMockCommand(t) - if err := app.List(m, false, true); err != nil { + if err := app.List(m, true); err != nil { t.Errorf("invalid error: %v", err) } if m.buf.String() == "" { t.Errorf("nothing listed: %s", m.buf.String()) } - m.args = []string{"test"} - if err := app.List(m, false, true); err == nil || err.Error() != "arguments not supported" { - t.Errorf("invalid error: %v", err) - } - m.args = []string{} - if err := app.List(m, true, true); err == nil || err.Error() != "groups+find not supported" { + m.args = []string{"test", "test2"} + if err := app.List(m, true); 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 @@ -57,8 +57,6 @@ const ( MinimalTOTPMode // ListTOTPMode lists the available tokens ListTOTPMode - // FindTOTPMode is list but with a regexp filter - FindTOTPMode // OnceTOTPMode will only show the token once and exit OnceTOTPMode ) @@ -220,7 +218,7 @@ func (args *TOTPArguments) Do(opts TOTPOptions) error { if !opts.CanTOTP() { return ErrNoTOTP } - if args.Mode == ListTOTPMode || args.Mode == FindTOTPMode { + if args.Mode == ListTOTPMode { return doList(backend.OTP, args.Entry, opts.app, false) } return args.display(opts) @@ -234,15 +232,17 @@ func NewTOTPArguments(args []string) (*TOTPArguments, error) { opts := &TOTPArguments{Mode: UnknownTOTPMode} sub := args[0] needs := true + length := len(args) switch sub { case commands.TOTPList: needs = false - if len(args) != 1 { - return nil, errors.New("list takes no arguments") + if length != 1 { + needs = true + if length != 2 { + return nil, errors.New("list takes only a filter (if any)") + } } opts.Mode = ListTOTPMode - case commands.TOTPFind: - opts.Mode = FindTOTPMode case commands.TOTPShow: opts.Mode = ShowTOTPMode case commands.TOTPClip: @@ -255,7 +255,7 @@ func NewTOTPArguments(args []string) (*TOTPArguments, error) { return nil, ErrUnknownTOTPMode } if needs { - if len(args) != 2 { + if length != 2 { return nil, errors.New("invalid arguments") } opts.Entry = args[1] diff --git a/internal/app/totp_test.go b/internal/app/totp_test.go @@ -83,7 +83,7 @@ func TestNewTOTPArgumentsErrors(t *testing.T) { if _, err := app.NewTOTPArguments([]string{"test"}); err == nil || err.Error() != "unknown totp mode" { t.Errorf("invalid error: %v", err) } - if _, err := app.NewTOTPArguments([]string{"ls", "test"}); err == nil || err.Error() != "list takes no arguments" { + if _, err := app.NewTOTPArguments([]string{"ls", "test", "xxx"}); err == nil || err.Error() != "list takes only a filter (if any)" { t.Errorf("invalid error: %v", err) } if _, err := app.NewTOTPArguments([]string{"show"}); err == nil || err.Error() != "invalid arguments" { @@ -96,8 +96,8 @@ func TestNewTOTPArguments(t *testing.T) { if args.Mode != app.ListTOTPMode || args.Entry != "" { t.Error("invalid args") } - args, _ = app.NewTOTPArguments([]string{"find", "tesst"}) - if args.Mode != app.FindTOTPMode || args.Entry == "" { + args, _ = app.NewTOTPArguments([]string{"ls", "xyz"}) + if args.Mode != app.ListTOTPMode || args.Entry != "xyz" { t.Error("invalid args") } args, _ = app.NewTOTPArguments([]string{"show", "test"}) @@ -269,9 +269,9 @@ func TestParseWindows(t *testing.T) { } } -func TestTOTPFind(t *testing.T) { +func TestTOTPListFilter(t *testing.T) { setupTOTP(t) - args, _ := app.NewTOTPArguments([]string{"find", "test"}) + args, _ := app.NewTOTPArguments([]string{"ls", "test"}) m, opts := newMock(t) if err := args.Do(opts); err != nil { t.Errorf("invalid error: %v", err) @@ -280,7 +280,7 @@ func TestTOTPFind(t *testing.T) { t.Errorf("invalid list: %s", m.buf.String()) } m.buf.Reset() - args, _ = app.NewTOTPArguments([]string{"find", "[zzzz]"}) + args, _ = app.NewTOTPArguments([]string{"ls", "[zzzz]"}) if err := args.Do(opts); err != nil { t.Errorf("invalid error: %v", err) }