diff --git a/README.md b/README.md index 7bb0622..83a6125 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ `pda!` is a command-line key-value store tool with: - [templates](https://github.com/Llywelwyn/pda#templates), -- search and filtering with [globs](https://github.com/Llywelwyn/pda#globs), -- Git-backed [version control](https://github.com/Llywelwyn/pda#git), -- plaintext exports in multiple formats, -- support for [binary data](https://github.com/Llywelwyn/pda#binary), -- [time-to-live](https://github.com/Llywelwyn/pda#ttl) support, - [encryption](https://github.com/Llywelwyn/pda#encryption) at rest using [age](https://github.com/FiloSottile/age), +- Git-backed [version control](https://github.com/Llywelwyn/pda#git), +- [search and filtering](https://github.com/Llywelwyn/pda#filtering) by key and/or value, +- plaintext exports in multiple formats, +- support for all [binary data](https://github.com/Llywelwyn/pda#binary), +- [time-to-live](https://github.com/Llywelwyn/pda#ttl)/expiry support, and more, written in pure Go, and inspired by [skate](https://github.com/charmbracelet/skate) and [nb](https://github.com/xwmx/nb). @@ -52,7 +52,7 @@ and more, written in pure Go, and inspired by [skate](https://github.com/charmbr - [Get Started](https://github.com/Llywelwyn/pda#get-started) - [Git-backed version control](https://github.com/Llywelwyn/pda#git) - [Templates](https://github.com/Llywelwyn/pda#templates) -- [Globs](https://github.com/Llywelwyn/pda#globs) +- [Filtering](https://github.com/Llywelwyn/pda#filtering) - [TTL](https://github.com/Llywelwyn/pda#ttl) - [Binary](https://github.com/Llywelwyn/pda#binary) - [Encryption](https://github.com/Llywelwyn/pda#encryption) @@ -174,12 +174,11 @@ pda rm kitty # Remove multiple keys, within the same or different stores. pda rm kitty dog@animals -# Mix exact keys with globs. +# Mix exact keys with glob patterns. pda set cog "cogs" pda set dog "doggy" pda set kitty "cat" -pda rm kitty --glob ?og -# Default glob separators: "/-_.@: " (space included). Override with --glob-sep. +pda rm kitty --key "?og" # Opt in to a confirmation prompt with --interactive/-i (or always_prompt_delete in config). pda rm kitty -i @@ -210,11 +209,11 @@ pda ls --format csv Long values are truncated to fit the terminal. Use `--full`/`-f` to show the complete value. ```bash pda ls -# Key Value TTL -# note this is a very long (..30 more chars) no expiry +# Key Value TTL +# note this is a very long (..30 more chars) no expiry pda ls --full -# Key Value TTL +# Key Value TTL # note this is a very long value that keeps on going and going no expiry ``` @@ -225,7 +224,10 @@ pda ls --full pda export > my_backup # Export only matching keys. -pda export --glob a* +pda export --key "a*" + +# Export only entries whose values contain a URL. +pda export --value "**https**" ```

@@ -241,7 +243,7 @@ pda import < my_backup # ok restored 2 entries into @default # Import only matching keys. -pda import --glob a* -f my_backup +pda import --key "a*" -f my_backup ```

@@ -257,7 +259,7 @@ pda list-stores # @birthdays # Check out a specific store. -pda ls @birthdays +pda ls @birthdays --no-header --no-ttl # alice 11/11/1998 # bob 05/12/1980 @@ -407,17 +409,17 @@ pda get hello --no-template

-### Globs +### Filtering -Globs can be used in a few commands where their use makes sense. `gobwas/glob` is used for matching. +`--key`/`-k` and `--value`/`-v` can be used as filters with glob support. `gobwas/glob` is used for matching. Both flags are repeatable, with results matching one-or-more of the keys and one-or-more of the values passed. If a `--key` and `--value` are passed, results must match both of them. If multiple are passed, results must match at least one `--key` and `--value` pattern. -Searching for globs is inherently slower than looking for direct matches, so globs are opt-in via a repeatable `--glob/-g` flag by default rather than having every string treated as a glob by default. Realistically the performance impact will be negligible unless you have many thousands of entries in the same store. +`--key` and `--value` filters work with `list`, `remove`, `export`, and `import` commands.

-`*` wildcards a word or series of characters. +`*` wildcards a word or series of characters, stopping at separator boundaries (the default separators are `/-_.@:` and space). ```bash -pda ls --no-values +pda ls --no-values --no-header # cat # dog # cog @@ -425,16 +427,16 @@ pda ls --no-values # mouse house # foo.bar.baz -pda ls --glob "*" +pda ls --key "*" # cat # dog # cog -pda ls --glob "* *" +pda ls --key "* *" # mouse hotdog # mouse house -pda ls --glob "foo.*.baz" +pda ls --key "foo.*.baz" # foo.bar.baz ``` @@ -442,10 +444,10 @@ pda ls --glob "foo.*.baz" `**` super-wildcards ignore word boundaries. ```bash -pda ls --glob "foo**" +pda ls --key "foo**" # foo.bar.baz -pda ls --glob "**g" +pda ls --key "**g" # dog # cog # mouse hotdog @@ -455,7 +457,7 @@ pda ls --glob "**g" `?` wildcards a single letter. ```bash -pda ls --glob ?og +pda ls --key "?og" # dog # cog # frog --> fail @@ -466,13 +468,13 @@ pda ls --glob ?og `[abc]` must match one of the characters in the brackets. ```bash -pda ls --glob [dc]og +pda ls --key "[dc]og" # dog # cog # bog --> fail # Can be negated with '!' -pda ls --glob [!dc]og +pda ls --key "[!dc]og" # dog --> fail # cog --> fail # bog @@ -480,20 +482,20 @@ pda ls --glob [!dc]og

-`[a-c]` must fall within the range given in the brackets +`[a-c]` must fall within the range given in the brackets. ```bash -pda ls --glob [a-g]ag +pda ls --key "[a-g]ag" # bag # gag # wag --> fail # Can be negated with '!' -pda ls --glob [!a-g]ag +pda ls --key "[!a-g]ag" # bag --> fail # gag --> fail # wag -pda ls --glob 19[90-99] +pda ls --key "19[90-99]" # 1991 # 1992 # 2001 --> fail @@ -502,39 +504,33 @@ pda ls --glob 19[90-99]

-Globs can be arbitrarily complex, and can be combined with strict matches. +`--value` filters by value content using the same glob syntax. ```bash -pda ls --no-keys -# cat -# mouse trap -# dog house -# cat flap -# cogwheel +pda ls --value "**localhost**" +# db-url postgres://localhost:5432 no expiry -pda rm cat --glob "{mouse,[cd]og}**" +# Combine key and value filters. +pda ls --key "db*" --value "**localhost**" +# db-url postgres://localhost:5432 no expiry + +# Multiple --value patterns are OR'd. +pda ls --value "**world**" --value "42" +# greeting hello world no expiry +# number 42 no expiry +``` + +

+ +Globs can be arbitrarily complex, and `--key` can be combined with exact positional args on `rm`. +```bash +pda rm cat --key "{mouse,[cd]og}**" # ??? remove 'cat'? (y/n) # ==> y # ??? remove 'mouse trap'? (y/n) # ... ``` -

- -`--glob-sep` can be used to change the default list of separators used to determine word boundaries. Separators default to a somewhat reasonable list of common alphanumeric characters so should be usable in most usual situations. -```bash -pda ls --no-keys -# foo%baz - -pda ls --glob "*" -# foo%baz - -pda ls --glob "*" --glob-sep "%" -# foo%baz --> fail -# % is considered a word boundary, so "*" no longer matches. - -pda ls --glob "*%*" --glob-sep "%" -# foo%baz -``` +Locked (encrypted without an available identity) and non-UTF-8 (binary) entries are silently excluded from `--value` matching.

diff --git a/cmd/del.go b/cmd/del.go index 02c48a3..62d23ee 100644 --- a/cmd/del.go +++ b/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) } diff --git a/cmd/export.go b/cmd/export.go index 968dd3f..b1a0898 100644 --- a/cmd/export.go +++ b/cmd/export.go @@ -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) } diff --git a/cmd/glob.go b/cmd/glob.go index 092c9b9..5c8c57f 100644 --- a/cmd/glob.go +++ b/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 } diff --git a/cmd/list.go b/cmd/list.go index 7a3dab3..348879d 100644 --- a/cmd/list.go +++ b/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) } diff --git a/cmd/match.go b/cmd/match.go new file mode 100644 index 0000000..c04595b --- /dev/null +++ b/cmd/match.go @@ -0,0 +1,70 @@ +/* +Copyright © 2025 Lewis Wynne + +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, ", ") +} diff --git a/cmd/restore.go b/cmd/restore.go index d8ae2c2..11756fa 100644 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -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) diff --git a/testdata/dump__glob__ok.ct b/testdata/dump__key__ok.ct similarity index 53% rename from testdata/dump__glob__ok.ct rename to testdata/dump__key__ok.ct index 999d094..10569d2 100644 --- a/testdata/dump__glob__ok.ct +++ b/testdata/dump__key__ok.ct @@ -1,8 +1,8 @@ $ pda set a1 1 $ pda set a2 2 $ pda set b1 3 -$ pda dump --glob a* +$ pda dump --key "a*" {"key":"a1","value":"1","encoding":"text"} {"key":"a2","value":"2","encoding":"text"} -$ pda dump --glob c* --> FAIL -FAIL cannot ls '@default': no matches for pattern 'c*' +$ pda dump --key "c*" --> FAIL +FAIL cannot ls '@default': no matches for key pattern 'c*' diff --git a/testdata/dump__value__ok.ct b/testdata/dump__value__ok.ct new file mode 100644 index 0000000..bab0072 --- /dev/null +++ b/testdata/dump__value__ok.ct @@ -0,0 +1,8 @@ +$ pda set url https://example.com +$ fecho tmpval hello world +$ pda set greeting < tmpval +$ pda set number 42 +$ pda dump --value "**https**" +{"key":"url","value":"https://example.com","encoding":"text"} +$ pda dump --value "**world**" +{"key":"greeting","value":"hello world\n","encoding":"text"} diff --git a/testdata/help__dump__ok.ct b/testdata/help__dump__ok.ct index 891cbc2..626e89d 100644 --- a/testdata/help__dump__ok.ct +++ b/testdata/help__dump__ok.ct @@ -9,9 +9,9 @@ Aliases: export, dump Flags: - -g, --glob strings Filter keys with glob pattern (repeatable) - --glob-sep string Characters treated as separators for globbing (default '/-_.@: ') - -h, --help help for export + -h, --help help for export + -k, --key strings Filter keys with glob pattern (repeatable) + -v, --value strings Filter values with regex pattern (repeatable) Export store as NDJSON (alias for list --format ndjson) Usage: @@ -21,6 +21,6 @@ Aliases: export, dump Flags: - -g, --glob strings Filter keys with glob pattern (repeatable) - --glob-sep string Characters treated as separators for globbing (default '/-_.@: ') - -h, --help help for export + -h, --help help for export + -k, --key strings Filter keys with glob pattern (repeatable) + -v, --value strings Filter values with regex pattern (repeatable) diff --git a/testdata/help__list__ok.ct b/testdata/help__list__ok.ct index 5cbf7d2..5e817c7 100644 --- a/testdata/help__list__ok.ct +++ b/testdata/help__list__ok.ct @@ -9,16 +9,16 @@ Aliases: list, ls Flags: - -b, --base64 view binary data as base64 - -o, --format format output format (table|tsv|csv|markdown|html|ndjson) (default table) - -f, --full show full values without truncation - -g, --glob strings Filter keys with glob pattern (repeatable) - --glob-sep string Characters treated as separators for globbing (default '/-_.@: ') - -h, --help help for list - --no-header suppress the header row - --no-keys suppress the key column - --no-ttl suppress the TTL column - --no-values suppress the value column + -b, --base64 view binary data as base64 + -o, --format format output format (table|tsv|csv|markdown|html|ndjson) (default table) + -f, --full show full values without truncation + -h, --help help for list + -k, --key strings Filter keys with glob pattern (repeatable) + --no-header suppress the header row + --no-keys suppress the key column + --no-ttl suppress the TTL column + --no-values suppress the value column + -v, --value strings Filter values with regex pattern (repeatable) List the contents of a store Usage: @@ -28,13 +28,13 @@ Aliases: list, ls Flags: - -b, --base64 view binary data as base64 - -o, --format format output format (table|tsv|csv|markdown|html|ndjson) (default table) - -f, --full show full values without truncation - -g, --glob strings Filter keys with glob pattern (repeatable) - --glob-sep string Characters treated as separators for globbing (default '/-_.@: ') - -h, --help help for list - --no-header suppress the header row - --no-keys suppress the key column - --no-ttl suppress the TTL column - --no-values suppress the value column + -b, --base64 view binary data as base64 + -o, --format format output format (table|tsv|csv|markdown|html|ndjson) (default table) + -f, --full show full values without truncation + -h, --help help for list + -k, --key strings Filter keys with glob pattern (repeatable) + --no-header suppress the header row + --no-keys suppress the key column + --no-ttl suppress the TTL column + --no-values suppress the value column + -v, --value strings Filter values with regex pattern (repeatable) diff --git a/testdata/help__remove__ok.ct b/testdata/help__remove__ok.ct index 19f0992..fe2bef6 100644 --- a/testdata/help__remove__ok.ct +++ b/testdata/help__remove__ok.ct @@ -9,10 +9,9 @@ Aliases: remove, rm Flags: - -g, --glob strings Delete keys matching glob pattern (repeatable) - --glob-sep string Characters treated as separators for globbing (default '/-_.@: ') - -h, --help help for remove - -i, --interactive Prompt yes/no for each deletion + -h, --help help for remove + -i, --interactive Prompt yes/no for each deletion + -k, --key strings Delete keys matching glob pattern (repeatable) Delete one or more keys Usage: @@ -22,7 +21,6 @@ Aliases: remove, rm Flags: - -g, --glob strings Delete keys matching glob pattern (repeatable) - --glob-sep string Characters treated as separators for globbing (default '/-_.@: ') - -h, --help help for remove - -i, --interactive Prompt yes/no for each deletion + -h, --help help for remove + -i, --interactive Prompt yes/no for each deletion + -k, --key strings Delete keys matching glob pattern (repeatable) diff --git a/testdata/help__restore__ok.ct b/testdata/help__restore__ok.ct index 140e160..2bbadff 100644 --- a/testdata/help__restore__ok.ct +++ b/testdata/help__restore__ok.ct @@ -9,12 +9,11 @@ Aliases: import, restore Flags: - --drop Drop existing entries before restoring (full replace) - -f, --file string Path to an NDJSON dump (defaults to stdin) - -g, --glob strings Restore keys matching glob pattern (repeatable) - --glob-sep string Characters treated as separators for globbing (default '/-_.@: ') - -h, --help help for import - -i, --interactive Prompt before overwriting existing keys + --drop Drop existing entries before restoring (full replace) + -f, --file string Path to an NDJSON dump (defaults to stdin) + -h, --help help for import + -i, --interactive Prompt before overwriting existing keys + -k, --key strings Restore keys matching glob pattern (repeatable) Restore key/value pairs from an NDJSON dump Usage: @@ -24,9 +23,8 @@ Aliases: import, restore Flags: - --drop Drop existing entries before restoring (full replace) - -f, --file string Path to an NDJSON dump (defaults to stdin) - -g, --glob strings Restore keys matching glob pattern (repeatable) - --glob-sep string Characters treated as separators for globbing (default '/-_.@: ') - -h, --help help for import - -i, --interactive Prompt before overwriting existing keys + --drop Drop existing entries before restoring (full replace) + -f, --file string Path to an NDJSON dump (defaults to stdin) + -h, --help help for import + -i, --interactive Prompt before overwriting existing keys + -k, --key strings Restore keys matching glob pattern (repeatable) diff --git a/testdata/list__glob__ok.ct b/testdata/list__glob__ok.ct deleted file mode 100644 index d6f3564..0000000 --- a/testdata/list__glob__ok.ct +++ /dev/null @@ -1,12 +0,0 @@ -$ pda set a1@lg 1 -$ pda set a2@lg 2 -$ pda set b1@lg 3 -$ pda ls lg --glob a* --format tsv -Key Value TTL -a1 1 no expiry -a2 2 no expiry -$ pda ls lg --glob b* --format tsv -Key Value TTL -b1 3 no expiry -$ pda ls lg --glob c* --> FAIL -FAIL cannot ls '@lg': no matches for pattern 'c*' diff --git a/testdata/list__key__ok.ct b/testdata/list__key__ok.ct new file mode 100644 index 0000000..a686456 --- /dev/null +++ b/testdata/list__key__ok.ct @@ -0,0 +1,12 @@ +$ pda set a1@lg 1 +$ pda set a2@lg 2 +$ pda set b1@lg 3 +$ pda ls lg --key "a*" --format tsv +Key Value TTL +a1 1 no expiry +a2 2 no expiry +$ pda ls lg --key "b*" --format tsv +Key Value TTL +b1 3 no expiry +$ pda ls lg --key "c*" --> FAIL +FAIL cannot ls '@lg': no matches for key pattern 'c*' diff --git a/testdata/list__key__value__ok.ct b/testdata/list__key__value__ok.ct new file mode 100644 index 0000000..9d55fcb --- /dev/null +++ b/testdata/list__key__value__ok.ct @@ -0,0 +1,11 @@ +$ pda set dburl@kv postgres://localhost:5432 +$ pda set apiurl@kv https://api.example.com +$ pda set dbpass@kv s3cret +$ pda ls kv -k "db*" -v "**localhost**" --format tsv +Key Value TTL +dburl postgres://localhost:5432 no expiry +$ pda ls kv -k "*url*" -v "**example**" --format tsv +Key Value TTL +apiurl https://api.example.com no expiry +$ pda ls kv -k "db*" -v "**nomatch**" --> FAIL +FAIL cannot ls '@kv': no matches for key pattern 'db*' and value pattern '**nomatch**' diff --git a/testdata/list__value__multi__ok.ct b/testdata/list__value__multi__ok.ct new file mode 100644 index 0000000..a57e2ce --- /dev/null +++ b/testdata/list__value__multi__ok.ct @@ -0,0 +1,8 @@ +$ pda set url@vm https://example.com +$ fecho tmpval hello world +$ pda set greeting@vm < tmpval +$ pda set number@vm 42 +$ pda ls vm --value "**world**" --value "42" --format tsv +Key Value TTL +greeting hello world (..1 more chars) no expiry +number 42 no expiry diff --git a/testdata/list__value__ok.ct b/testdata/list__value__ok.ct new file mode 100644 index 0000000..ee0ca02 --- /dev/null +++ b/testdata/list__value__ok.ct @@ -0,0 +1,15 @@ +$ pda set url@vt https://example.com +$ fecho tmpval hello world +$ pda set greeting@vt < tmpval +$ pda set number@vt 42 +$ pda ls vt --value "**world**" --format tsv +Key Value TTL +greeting hello world (..1 more chars) no expiry +$ pda ls vt --value "**https**" --format tsv +Key Value TTL +url https://example.com no expiry +$ pda ls vt --value "*" --format tsv +Key Value TTL +number 42 no expiry +$ pda ls vt --value "**nomatch**" --> FAIL +FAIL cannot ls '@vt': no matches for value pattern '**nomatch**' diff --git a/testdata/remove__dedupe__ok.ct b/testdata/remove__dedupe__ok.ct index cfd5433..afa50a7 100644 --- a/testdata/remove__dedupe__ok.ct +++ b/testdata/remove__dedupe__ok.ct @@ -1,16 +1,19 @@ $ pda set foo 1 $ pda set bar 2 $ pda ls -Key Value TTL -a echo hello (..1 more chars) no expiry -a1 1 no expiry -a2 2 no expiry -b1 3 no expiry -bar 2 no expiry -copied-key hidden-value no expiry -foo 1 no expiry -moved-key hidden-value no expiry -$ pda rm foo --glob "*" +Key Value TTL +a echo hello (..1 more chars) no expiry +a1 1 no expiry +a2 2 no expiry +b1 3 no expiry +bar 2 no expiry +copied-key hidden-value no expiry +foo 1 no expiry +greeting hello world (..1 more chars) no expiry +moved-key hidden-value no expiry +number 42 no expiry +url https://example.com no expiry +$ pda rm foo --key "*" $ pda get bar --> FAIL FAIL cannot get 'bar': no such key $ pda get foo --> FAIL diff --git a/testdata/remove__glob__mixed__ok.ct b/testdata/remove__key__mixed__ok.ct similarity index 89% rename from testdata/remove__glob__mixed__ok.ct rename to testdata/remove__key__mixed__ok.ct index 3f5fa6b..20a870b 100644 --- a/testdata/remove__glob__mixed__ok.ct +++ b/testdata/remove__key__mixed__ok.ct @@ -1,7 +1,7 @@ $ pda set foo 1 $ pda set bar1 2 $ pda set bar2 3 -$ pda rm foo --glob bar* +$ pda rm foo --key "bar*" $ pda get foo --> FAIL FAIL cannot get 'foo': no such key $ pda get bar1 --> FAIL diff --git a/testdata/remove__glob__ok.ct b/testdata/remove__key__ok.ct similarity index 90% rename from testdata/remove__glob__ok.ct rename to testdata/remove__key__ok.ct index 7ad2534..ce7fb2a 100644 --- a/testdata/remove__glob__ok.ct +++ b/testdata/remove__key__ok.ct @@ -1,7 +1,7 @@ $ pda set a1 1 $ pda set a2 2 $ pda set b1 3 -$ pda rm --glob a* +$ pda rm --key "a*" $ pda get a1 --> FAIL FAIL cannot get 'a1': no such key hint did you mean 'b1'? diff --git a/testdata/restore__glob__ok.ct b/testdata/restore__key__ok.ct similarity index 69% rename from testdata/restore__glob__ok.ct rename to testdata/restore__key__ok.ct index 9f5f5da..544d033 100644 --- a/testdata/restore__glob__ok.ct +++ b/testdata/restore__key__ok.ct @@ -3,7 +3,7 @@ $ pda set a2 2 $ pda set b1 3 $ fecho dumpfile {"key":"a1","value":"1","encoding":"text"} {"key":"a2","value":"2","encoding":"text"} {"key":"b1","value":"3","encoding":"text"} $ pda rm a1 a2 b1 -$ pda restore --glob a* --file dumpfile +$ pda restore --key "a*" --file dumpfile ok restored 2 entries into @default $ pda get a1 1 @@ -12,5 +12,5 @@ $ pda get a2 $ pda get b1 --> FAIL FAIL cannot get 'b1': no such key hint did you mean 'a1'? -$ pda restore --glob c* --file dumpfile --> FAIL -FAIL cannot restore '@default': no matches for pattern 'c*' +$ pda restore --key "c*" --file dumpfile --> FAIL +FAIL cannot restore '@default': no matches for key pattern 'c*'