feat(del): adds glob support for deletion

This commit is contained in:
Lewis Wynne 2025-12-17 18:37:10 +00:00
parent 14897ba587
commit badbf3b6bb
6 changed files with 145 additions and 19 deletions

View file

@ -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
}
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)
useGlob, err := cmd.Flags().GetBool("glob")
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
}

View file

@ -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
}

1
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

10
testdata/del__glob__ok.ct vendored Normal file
View file

@ -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

View file

@ -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