diff --git a/cmd/del.go b/cmd/del.go index 852dbf1..13f8508 100644 --- a/cmd/del.go +++ b/cmd/del.go @@ -27,6 +27,7 @@ import ( "strings" "github.com/dgraph-io/badger/v4" + "github.com/gobwas/glob" "github.com/spf13/cobra" ) @@ -47,21 +48,14 @@ func del(cmd *cobra.Command, args []string) error { if err != nil { return err } + useGlob, err := cmd.Flags().GetBool("glob") + if err != nil { + return err + } - targetKeys := make([]string, 0, len(args)) - for _, arg := range args { - exists, err := keyExists(store, arg) - if err != nil { - return fmt.Errorf("cannot remove '%s': %v", arg, err) - } - if !exists { - return fmt.Errorf("cannot remove '%s': No such key", arg) - } - targetKey, err := formatKeyForPrompt(store, arg) - if err != nil { - return err - } - targetKeys = append(targetKeys, targetKey) + targetKeys, deleteTargets, err := resolveDeleteTargets(store, args, useGlob) + if err != nil { + return err } if !force { @@ -80,18 +74,17 @@ func del(cmd *cobra.Command, args []string) error { } } - for _, arg := range args { - arg := arg + for _, target := range deleteTargets { trans := TransactionArgs{ - key: arg, + key: target, readonly: false, sync: false, transact: func(tx *badger.Txn, k []byte) error { if err := tx.Delete(k); errors.Is(err, badger.ErrKeyNotFound) { - return fmt.Errorf("cannot remove '%s': No such key", arg) + return fmt.Errorf("cannot remove '%s': No such key", target) } if err != nil { - return fmt.Errorf("cannot remove '%s': %v", arg, err) + return fmt.Errorf("cannot remove '%s': %v", target, err) } return nil }, @@ -107,6 +100,7 @@ func del(cmd *cobra.Command, args []string) error { func init() { delCmd.Flags().BoolP("force", "f", false, "Force delete without confirmation") + delCmd.Flags().BoolP("glob", "g", false, "Treat KEY arguments as glob patterns") rootCmd.AddCommand(delCmd) } @@ -144,3 +138,97 @@ func formatKeyForPrompt(store *Store, arg string) (string, error) { } return fmt.Sprintf("%s@%s", arg, db), nil } + +func resolveDeleteTargets(store *Store, args []string, useGlob bool) ([]string, []string, error) { + if !useGlob { + targetKeys := make([]string, 0, len(args)) + for _, arg := range args { + exists, err := keyExists(store, arg) + if err != nil { + return nil, nil, fmt.Errorf("cannot remove '%s': %v", arg, err) + } + if !exists { + return nil, nil, fmt.Errorf("cannot remove '%s': No such key", arg) + } + targetKey, err := formatKeyForPrompt(store, arg) + if err != nil { + return nil, nil, err + } + targetKeys = append(targetKeys, targetKey) + } + return targetKeys, args, nil + } + + type compiledPattern struct { + rawArg string + db string + matcher glob.Glob + pattern string + } + + var compiled []compiledPattern + for _, arg := range args { + kb, db, err := store.parse(arg, true) + if err != nil { + return nil, nil, err + } + pattern := string(kb) + m, err := glob.Compile(pattern) + if err != nil { + return nil, nil, fmt.Errorf("cannot remove '%s': %v", arg, err) + } + compiled = append(compiled, compiledPattern{ + rawArg: arg, + db: db, + matcher: m, + pattern: pattern, + }) + } + + keysByDB := make(map[string][]string) + getKeys := func(db string) ([]string, error) { + if keys, ok := keysByDB[db]; ok { + return keys, nil + } + keys, err := store.Keys(db) + if err != nil { + return nil, err + } + keysByDB[db] = keys + return keys, nil + } + + targetSet := make(map[string]struct{}) + var targetKeys []string + var deleteTargets []string + + for _, p := range compiled { + keys, err := getKeys(p.db) + if err != nil { + return nil, nil, fmt.Errorf("cannot remove '%s': %v", p.rawArg, err) + } + var matched []string + for _, k := range keys { + if p.matcher.Match(k) { + matched = append(matched, fmt.Sprintf("%s@%s", k, p.db)) + } + } + if len(matched) == 0 { + return nil, nil, fmt.Errorf("cannot remove '%s': No matches for pattern", p.rawArg) + } + for _, full := range matched { + if _, seen := targetSet[full]; seen { + continue + } + targetSet[full] = struct{}{} + display, err := formatKeyForPrompt(store, full) + if err != nil { + return nil, nil, err + } + targetKeys = append(targetKeys, display) + deleteTargets = append(deleteTargets, full) + } + } + + return targetKeys, deleteTargets, nil +} diff --git a/cmd/shared.go b/cmd/shared.go index 560c6aa..4b14f50 100644 --- a/cmd/shared.go +++ b/cmd/shared.go @@ -253,3 +253,26 @@ func formatExpiry(expiresAt uint64) string { } return fmt.Sprintf("%s (in %s)", expiry.Format(time.RFC3339), remaining.Round(time.Second)) } + +// Keys returns all keys for the provided database name (or default if empty). +// Keys are returned in lowercase to mirror stored key format. +func (s *Store) Keys(dbName string) ([]string, error) { + db, err := s.open(dbName) + if err != nil { + return nil, err + } + defer db.Close() + + tx := db.NewTransaction(false) + defer tx.Discard() + + it := tx.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() + + var keys []string + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + keys = append(keys, string(item.Key())) + } + return keys, nil +} diff --git a/go.mod b/go.mod index 12fc386..ec708da 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.3 require ( github.com/agnivade/levenshtein v1.2.1 github.com/dgraph-io/badger/v4 v4.8.0 + github.com/gobwas/glob v0.2.3 github.com/google/go-cmdtest v0.4.0 github.com/jedib0t/go-pretty/v6 v6.7.0 github.com/muesli/go-app-paths v0.2.2 diff --git a/go.sum b/go.sum index f1ab205..71da71c 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmdtest v0.4.0 h1:ToXh6W5spLp3npJV92tk6d5hIpUPYEzHLkD+rncbyhI= diff --git a/testdata/del__glob__ok.ct b/testdata/del__glob__ok.ct new file mode 100644 index 0000000..f7b195b --- /dev/null +++ b/testdata/del__glob__ok.ct @@ -0,0 +1,10 @@ +$ pda set a1 1 +$ pda set a2 2 +$ pda set b1 3 +$ pda del --glob a* --force +$ pda get a1 --> FAIL +Error: cannot get 'a1': Key not found +$ pda get a2 --> FAIL +Error: cannot get 'a2': Key not found +$ pda get b1 +3 diff --git a/testdata/help__del__ok.ct b/testdata/help__del__ok.ct index 668595f..38d5e32 100644 --- a/testdata/help__del__ok.ct +++ b/testdata/help__del__ok.ct @@ -10,6 +10,7 @@ Aliases: Flags: -f, --force Force delete without confirmation + -g, --glob Treat KEY arguments as glob patterns -h, --help help for del Delete one or more keys. Optionally specify a db. @@ -21,4 +22,5 @@ Aliases: Flags: -f, --force Force delete without confirmation + -g, --glob Treat KEY arguments as glob patterns -h, --help help for del