feat: splits --glob into --key and --value searches
This commit is contained in:
parent
1f4732823d
commit
5145816b0a
22 changed files with 275 additions and 188 deletions
17
cmd/del.go
17
cmd/del.go
|
|
@ -47,20 +47,16 @@ func del(cmd *cobra.Command, args []string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globPatterns, err := cmd.Flags().GetStringSlice("glob")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
separators, err := parseGlobSeparators(cmd)
|
||||
keyPatterns, err := cmd.Flags().GetStringSlice("key")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 0 && len(globPatterns) == 0 {
|
||||
if len(args) == 0 && len(keyPatterns) == 0 {
|
||||
return fmt.Errorf("cannot remove: no keys provided")
|
||||
}
|
||||
|
||||
targets, err := resolveDeleteTargets(store, args, globPatterns, separators)
|
||||
targets, err := resolveDeleteTargets(store, args, keyPatterns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -124,8 +120,7 @@ func del(cmd *cobra.Command, args []string) error {
|
|||
|
||||
func init() {
|
||||
delCmd.Flags().BoolP("interactive", "i", false, "Prompt yes/no for each deletion")
|
||||
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 '%s')", defaultGlobSeparatorsDisplay()))
|
||||
delCmd.Flags().StringSliceP("key", "k", nil, "Delete keys matching glob pattern (repeatable)")
|
||||
rootCmd.AddCommand(delCmd)
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +147,7 @@ func keyExists(store *Store, arg string) (bool, error) {
|
|||
return findEntry(entries, spec.Key) >= 0, nil
|
||||
}
|
||||
|
||||
func resolveDeleteTargets(store *Store, exactArgs []string, globPatterns []string, separators []rune) ([]resolvedTarget, error) {
|
||||
func resolveDeleteTargets(store *Store, exactArgs []string, globPatterns []string) ([]resolvedTarget, error) {
|
||||
targetSet := make(map[string]struct{})
|
||||
var targets []resolvedTarget
|
||||
|
||||
|
|
@ -202,7 +197,7 @@ func resolveDeleteTargets(store *Store, exactArgs []string, globPatterns []strin
|
|||
return nil, err
|
||||
}
|
||||
pattern := spec.Key
|
||||
m, err := glob.Compile(pattern, separators...)
|
||||
m, err := glob.Compile(pattern, defaultGlobSeparators...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot remove '%s': %v", raw, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ THE SOFTWARE.
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -41,7 +39,7 @@ var exportCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
exportCmd.Flags().StringSliceP("glob", "g", nil, "Filter keys with glob pattern (repeatable)")
|
||||
exportCmd.Flags().String("glob-sep", "", fmt.Sprintf("Characters treated as separators for globbing (default '%s')", defaultGlobSeparatorsDisplay()))
|
||||
exportCmd.Flags().StringSliceP("key", "k", nil, "Filter keys with glob pattern (repeatable)")
|
||||
exportCmd.Flags().StringSliceP("value", "v", nil, "Filter values with regex pattern (repeatable)")
|
||||
rootCmd.AddCommand(exportCmd)
|
||||
}
|
||||
|
|
|
|||
24
cmd/glob.go
24
cmd/glob.go
|
|
@ -27,34 +27,14 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"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
|
||||
}
|
||||
|
||||
func compileGlobMatchers(patterns []string, separators []rune) ([]glob.Glob, error) {
|
||||
func compileGlobMatchers(patterns []string) ([]glob.Glob, error) {
|
||||
var matchers []glob.Glob
|
||||
for _, pattern := range patterns {
|
||||
m, err := glob.Compile(strings.ToLower(pattern), separators...)
|
||||
m, err := glob.Compile(strings.ToLower(pattern), defaultGlobSeparators...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
30
cmd/list.go
30
cmd/list.go
|
|
@ -119,15 +119,20 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
columns = append(columns, columnTTL)
|
||||
}
|
||||
|
||||
globPatterns, err := cmd.Flags().GetStringSlice("glob")
|
||||
keyPatterns, err := cmd.Flags().GetStringSlice("key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
separators, err := parseGlobSeparators(cmd)
|
||||
matchers, err := compileGlobMatchers(keyPatterns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
matchers, err := compileGlobMatchers(globPatterns, separators)
|
||||
|
||||
valuePatterns, err := cmd.Flags().GetStringSlice("value")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
valueMatchers, err := compileValueMatchers(valuePatterns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
|
|
@ -148,16 +153,23 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
|
||||
// Filter by glob
|
||||
// Filter by key glob and value regex
|
||||
var filtered []Entry
|
||||
for _, e := range entries {
|
||||
if globMatch(matchers, e.Key) {
|
||||
if globMatch(matchers, e.Key) && valueMatch(valueMatchers, e) {
|
||||
filtered = append(filtered, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchers) > 0 && len(filtered) == 0 {
|
||||
return fmt.Errorf("cannot ls '%s': no matches for pattern %s", targetDB, formatGlobPatterns(globPatterns))
|
||||
if (len(matchers) > 0 || len(valueMatchers) > 0) && len(filtered) == 0 {
|
||||
switch {
|
||||
case len(matchers) > 0 && len(valueMatchers) > 0:
|
||||
return fmt.Errorf("cannot ls '%s': no matches for key pattern %s and value pattern %s", targetDB, formatGlobPatterns(keyPatterns), formatValuePatterns(valuePatterns))
|
||||
case len(valueMatchers) > 0:
|
||||
return fmt.Errorf("cannot ls '%s': no matches for value pattern %s", targetDB, formatValuePatterns(valuePatterns))
|
||||
default:
|
||||
return fmt.Errorf("cannot ls '%s': no matches for key pattern %s", targetDB, formatGlobPatterns(keyPatterns))
|
||||
}
|
||||
}
|
||||
|
||||
output := cmd.OutOrStdout()
|
||||
|
|
@ -467,7 +479,7 @@ func init() {
|
|||
listCmd.Flags().BoolVarP(&listFull, "full", "f", false, "show full values without truncation")
|
||||
listCmd.Flags().BoolVar(&listNoHeader, "no-header", false, "suppress the header row")
|
||||
listCmd.Flags().VarP(&listFormat, "format", "o", "output format (table|tsv|csv|markdown|html|ndjson)")
|
||||
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 '%s')", defaultGlobSeparatorsDisplay()))
|
||||
listCmd.Flags().StringSliceP("key", "k", nil, "Filter keys with glob pattern (repeatable)")
|
||||
listCmd.Flags().StringSliceP("value", "v", nil, "Filter values with regex pattern (repeatable)")
|
||||
rootCmd.AddCommand(listCmd)
|
||||
}
|
||||
|
|
|
|||
70
cmd/match.go
Normal file
70
cmd/match.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright © 2025 Lewis Wynne <lew@ily.rs>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
)
|
||||
|
||||
func compileValueMatchers(patterns []string) ([]glob.Glob, error) {
|
||||
var matchers []glob.Glob
|
||||
for _, pattern := range patterns {
|
||||
m, err := glob.Compile(strings.ToLower(pattern), defaultGlobSeparators...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matchers = append(matchers, m)
|
||||
}
|
||||
return matchers, nil
|
||||
}
|
||||
|
||||
func valueMatch(matchers []glob.Glob, e Entry) bool {
|
||||
if len(matchers) == 0 {
|
||||
return true
|
||||
}
|
||||
if e.Locked {
|
||||
return false
|
||||
}
|
||||
if !utf8.Valid(e.Value) {
|
||||
return false
|
||||
}
|
||||
s := strings.ToLower(string(e.Value))
|
||||
for _, m := range matchers {
|
||||
if m.Match(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatValuePatterns(patterns []string) string {
|
||||
quoted := make([]string, 0, len(patterns))
|
||||
for _, pattern := range patterns {
|
||||
quoted = append(quoted, fmt.Sprintf("'%s'", pattern))
|
||||
}
|
||||
return strings.Join(quoted, ", ")
|
||||
}
|
||||
|
|
@ -56,15 +56,11 @@ func restore(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
displayTarget := "@" + dbName
|
||||
|
||||
globPatterns, err := cmd.Flags().GetStringSlice("glob")
|
||||
keyPatterns, err := cmd.Flags().GetStringSlice("key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot restore '%s': %v", displayTarget, err)
|
||||
}
|
||||
separators, err := parseGlobSeparators(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot restore '%s': %v", displayTarget, err)
|
||||
}
|
||||
matchers, err := compileGlobMatchers(globPatterns, separators)
|
||||
matchers, err := compileGlobMatchers(keyPatterns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot restore '%s': %v", displayTarget, err)
|
||||
}
|
||||
|
|
@ -113,7 +109,7 @@ func restore(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
if len(matchers) > 0 && restored == 0 {
|
||||
return fmt.Errorf("cannot restore '%s': no matches for pattern %s", displayTarget, formatGlobPatterns(globPatterns))
|
||||
return fmt.Errorf("cannot restore '%s': no matches for key pattern %s", displayTarget, formatGlobPatterns(keyPatterns))
|
||||
}
|
||||
|
||||
okf("restored %d entries into @%s", restored, dbName)
|
||||
|
|
@ -208,8 +204,7 @@ func restoreEntries(decoder *json.Decoder, storePath string, opts restoreOpts) (
|
|||
|
||||
func init() {
|
||||
restoreCmd.Flags().StringP("file", "f", "", "Path to an NDJSON dump (defaults to stdin)")
|
||||
restoreCmd.Flags().StringSliceP("glob", "g", nil, "Restore keys matching glob pattern (repeatable)")
|
||||
restoreCmd.Flags().String("glob-sep", "", fmt.Sprintf("Characters treated as separators for globbing (default '%s')", defaultGlobSeparatorsDisplay()))
|
||||
restoreCmd.Flags().StringSliceP("key", "k", nil, "Restore keys matching glob pattern (repeatable)")
|
||||
restoreCmd.Flags().BoolP("interactive", "i", false, "Prompt before overwriting existing keys")
|
||||
restoreCmd.Flags().Bool("drop", false, "Drop existing entries before restoring (full replace)")
|
||||
rootCmd.AddCommand(restoreCmd)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue