feat(del): adds glob support for deletion
This commit is contained in:
parent
14897ba587
commit
badbf3b6bb
6 changed files with 145 additions and 19 deletions
122
cmd/del.go
122
cmd/del.go
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
"github.com/dgraph-io/badger/v4"
|
||||||
|
"github.com/gobwas/glob"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -47,21 +48,14 @@ func del(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
useGlob, err := cmd.Flags().GetBool("glob")
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
targetKeys = append(targetKeys, targetKey)
|
|
||||||
|
targetKeys, deleteTargets, err := resolveDeleteTargets(store, args, useGlob)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !force {
|
if !force {
|
||||||
|
|
@ -80,18 +74,17 @@ func del(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, arg := range args {
|
for _, target := range deleteTargets {
|
||||||
arg := arg
|
|
||||||
trans := TransactionArgs{
|
trans := TransactionArgs{
|
||||||
key: arg,
|
key: target,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
sync: false,
|
sync: false,
|
||||||
transact: func(tx *badger.Txn, k []byte) error {
|
transact: func(tx *badger.Txn, k []byte) error {
|
||||||
if err := tx.Delete(k); errors.Is(err, badger.ErrKeyNotFound) {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot remove '%s': %v", arg, err)
|
return fmt.Errorf("cannot remove '%s': %v", target, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
@ -107,6 +100,7 @@ func del(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
delCmd.Flags().BoolP("force", "f", false, "Force delete without confirmation")
|
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)
|
rootCmd.AddCommand(delCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,3 +138,97 @@ func formatKeyForPrompt(store *Store, arg string) (string, error) {
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s@%s", arg, db), nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -253,3 +253,26 @@ func formatExpiry(expiresAt uint64) string {
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s (in %s)", expiry.Format(time.RFC3339), remaining.Round(time.Second))
|
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
1
go.mod
|
|
@ -5,6 +5,7 @@ go 1.25.3
|
||||||
require (
|
require (
|
||||||
github.com/agnivade/levenshtein v1.2.1
|
github.com/agnivade/levenshtein v1.2.1
|
||||||
github.com/dgraph-io/badger/v4 v4.8.0
|
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/google/go-cmdtest v0.4.0
|
||||||
github.com/jedib0t/go-pretty/v6 v6.7.0
|
github.com/jedib0t/go-pretty/v6 v6.7.0
|
||||||
github.com/muesli/go-app-paths v0.2.2
|
github.com/muesli/go-app-paths v0.2.2
|
||||||
|
|
|
||||||
2
go.sum
2
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/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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
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 h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
|
||||||
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
github.com/google/go-cmdtest v0.4.0 h1:ToXh6W5spLp3npJV92tk6d5hIpUPYEzHLkD+rncbyhI=
|
github.com/google/go-cmdtest v0.4.0 h1:ToXh6W5spLp3npJV92tk6d5hIpUPYEzHLkD+rncbyhI=
|
||||||
|
|
|
||||||
10
testdata/del__glob__ok.ct
vendored
Normal file
10
testdata/del__glob__ok.ct
vendored
Normal 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
|
||||||
2
testdata/help__del__ok.ct
vendored
2
testdata/help__del__ok.ct
vendored
|
|
@ -10,6 +10,7 @@ Aliases:
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-f, --force Force delete without confirmation
|
-f, --force Force delete without confirmation
|
||||||
|
-g, --glob Treat KEY arguments as glob patterns
|
||||||
-h, --help help for del
|
-h, --help help for del
|
||||||
Delete one or more keys. Optionally specify a db.
|
Delete one or more keys. Optionally specify a db.
|
||||||
|
|
||||||
|
|
@ -21,4 +22,5 @@ Aliases:
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-f, --force Force delete without confirmation
|
-f, --force Force delete without confirmation
|
||||||
|
-g, --glob Treat KEY arguments as glob patterns
|
||||||
-h, --help help for del
|
-h, --help help for del
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue