feat(globs): glob support extended to ls and documented in README
This commit is contained in:
parent
badbf3b6bb
commit
95c9ac8fca
8 changed files with 316 additions and 74 deletions
104
cmd/del.go
104
cmd/del.go
|
|
@ -36,7 +36,7 @@ var delCmd = &cobra.Command{
|
|||
Use: "del KEY[@DB] [KEY[@DB] ...]",
|
||||
Short: "Delete one or more keys. Optionally specify a db.",
|
||||
Aliases: []string{"delete", "rm", "remove"},
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: del,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
|
@ -48,12 +48,20 @@ func del(cmd *cobra.Command, args []string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
useGlob, err := cmd.Flags().GetBool("glob")
|
||||
globPatterns, err := cmd.Flags().GetStringSlice("glob")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
separators, err := parseGlobSeparators(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetKeys, deleteTargets, err := resolveDeleteTargets(store, args, useGlob)
|
||||
if len(args) == 0 && len(globPatterns) == 0 {
|
||||
return fmt.Errorf("cannot remove: no keys provided")
|
||||
}
|
||||
|
||||
targetKeys, deleteTargets, err := resolveDeleteTargets(store, args, globPatterns, separators)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -100,7 +108,8 @@ 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")
|
||||
delCmd.Flags().StringSliceP("glob", "g", nil, "Delete keys matching glob pattern (repeatable)")
|
||||
delCmd.Flags().String("glob-sep", "", fmt.Sprintf("Characters treated as separators for globbing (default %q)", defaultGlobSeparatorsDisplay()))
|
||||
rootCmd.AddCommand(delCmd)
|
||||
}
|
||||
|
||||
|
|
@ -139,49 +148,55 @@ 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)
|
||||
func resolveDeleteTargets(store *Store, exactArgs []string, globPatterns []string, separators []rune) ([]string, []string, error) {
|
||||
targetSet := make(map[string]struct{})
|
||||
var targetKeys []string
|
||||
var deleteTargets []string
|
||||
|
||||
for _, arg := range exactArgs {
|
||||
exists, err := keyExists(store, arg)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot remove '%s': %v", arg, err)
|
||||
}
|
||||
return targetKeys, args, nil
|
||||
if !exists {
|
||||
return nil, nil, fmt.Errorf("cannot remove '%s': No such key", arg)
|
||||
}
|
||||
formatted, err := formatKeyForPrompt(store, arg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if _, seen := targetSet[arg]; !seen {
|
||||
targetSet[arg] = struct{}{}
|
||||
targetKeys = append(targetKeys, formatted)
|
||||
deleteTargets = append(deleteTargets, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(globPatterns) == 0 {
|
||||
return targetKeys, deleteTargets, 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)
|
||||
for _, raw := range globPatterns {
|
||||
kb, db, err := store.parse(raw, true)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pattern := string(kb)
|
||||
m, err := glob.Compile(pattern)
|
||||
m, err := glob.Compile(pattern, separators...)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot remove '%s': %v", arg, err)
|
||||
return nil, nil, fmt.Errorf("cannot remove '%s': %v", raw, err)
|
||||
}
|
||||
compiled = append(compiled, compiledPattern{
|
||||
rawArg: arg,
|
||||
rawArg: raw,
|
||||
db: db,
|
||||
matcher: m,
|
||||
pattern: pattern,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -198,36 +213,31 @@ func resolveDeleteTargets(store *Store, args []string, useGlob bool) ([]string,
|
|||
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
|
||||
var matched bool
|
||||
for _, k := range keys {
|
||||
if p.matcher.Match(k) {
|
||||
matched = append(matched, fmt.Sprintf("%s@%s", k, p.db))
|
||||
matched = true
|
||||
full := fmt.Sprintf("%s@%s", k, p.db)
|
||||
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)
|
||||
}
|
||||
}
|
||||
if len(matched) == 0 {
|
||||
if !matched {
|
||||
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
|
||||
|
|
|
|||
28
cmd/glob.go
Normal file
28
cmd/glob.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var defaultGlobSeparators = []rune{'/', '-', '_', '.', '@', ':', ' '}
|
||||
|
||||
func defaultGlobSeparatorsDisplay() string {
|
||||
var b strings.Builder
|
||||
for _, r := range defaultGlobSeparators {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func parseGlobSeparators(cmd *cobra.Command) ([]rune, error) {
|
||||
sepStr, err := cmd.Flags().GetString("glob-sep")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sepStr == "" {
|
||||
return defaultGlobSeparators, nil
|
||||
}
|
||||
return []rune(sepStr), nil
|
||||
}
|
||||
42
cmd/list.go
42
cmd/list.go
|
|
@ -24,8 +24,10 @@ package cmd
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -63,6 +65,35 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
|
||||
globPatterns, err := cmd.Flags().GetStringSlice("glob")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
separators, err := parseGlobSeparators(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
var matchers []glob.Glob
|
||||
for _, pattern := range globPatterns {
|
||||
m, err := glob.Compile(strings.ToLower(pattern), separators...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
matchers = append(matchers, m)
|
||||
}
|
||||
|
||||
matchesKey := func(k string) bool {
|
||||
if len(matchers) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, m := range matchers {
|
||||
if m.Match(k) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
columnKinds, err := requireColumns(flags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
|
|
@ -89,6 +120,7 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
placeholder := "**********"
|
||||
var matchedCount int
|
||||
trans := TransactionArgs{
|
||||
key: targetDB,
|
||||
readonly: true,
|
||||
|
|
@ -103,6 +135,10 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
for it.Rewind(); it.Valid(); it.Next() {
|
||||
item := it.Item()
|
||||
key := string(item.KeyCopy(nil))
|
||||
if !matchesKey(key) {
|
||||
continue
|
||||
}
|
||||
matchedCount++
|
||||
meta := item.UserMeta()
|
||||
isSecret := meta&metaSecret != 0
|
||||
|
||||
|
|
@ -143,6 +179,10 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if len(matchers) > 0 && matchedCount == 0 {
|
||||
return fmt.Errorf("cannot ls '%s': No matches for pattern", targetDB)
|
||||
}
|
||||
|
||||
applyColumnConstraints(tw, columnKinds, output, maxContentWidths)
|
||||
|
||||
flags.render(tw)
|
||||
|
|
@ -157,5 +197,7 @@ func init() {
|
|||
listCmd.Flags().BoolVarP(&ttl, "ttl", "t", false, "append a TTL column when entries expire")
|
||||
listCmd.Flags().BoolVar(&header, "header", false, "include header row")
|
||||
listCmd.Flags().VarP(&format, "format", "o", "output format (table|tsv|csv|markdown|html)")
|
||||
listCmd.Flags().StringSliceP("glob", "g", nil, "Filter keys with glob pattern (repeatable)")
|
||||
listCmd.Flags().String("glob-sep", "", fmt.Sprintf("Characters treated as separators for globbing (default %q)", defaultGlobSeparatorsDisplay()))
|
||||
rootCmd.AddCommand(listCmd)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue