feat: splits --glob into --key and --value searches

This commit is contained in:
Lewis Wynne 2026-02-11 15:21:05 +00:00
parent 1f4732823d
commit 5145816b0a
22 changed files with 275 additions and 188 deletions

110
README.md
View file

@ -20,12 +20,12 @@
`pda!` is a command-line key-value store tool with: `pda!` is a command-line key-value store tool with:
- [templates](https://github.com/Llywelwyn/pda#templates), - [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), - [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). 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) - [Get Started](https://github.com/Llywelwyn/pda#get-started)
- [Git-backed version control](https://github.com/Llywelwyn/pda#git) - [Git-backed version control](https://github.com/Llywelwyn/pda#git)
- [Templates](https://github.com/Llywelwyn/pda#templates) - [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) - [TTL](https://github.com/Llywelwyn/pda#ttl)
- [Binary](https://github.com/Llywelwyn/pda#binary) - [Binary](https://github.com/Llywelwyn/pda#binary)
- [Encryption](https://github.com/Llywelwyn/pda#encryption) - [Encryption](https://github.com/Llywelwyn/pda#encryption)
@ -174,12 +174,11 @@ pda rm kitty
# Remove multiple keys, within the same or different stores. # Remove multiple keys, within the same or different stores.
pda rm kitty dog@animals pda rm kitty dog@animals
# Mix exact keys with globs. # Mix exact keys with glob patterns.
pda set cog "cogs" pda set cog "cogs"
pda set dog "doggy" pda set dog "doggy"
pda set kitty "cat" pda set kitty "cat"
pda rm kitty --glob ?og pda rm kitty --key "?og"
# Default glob separators: "/-_.@: " (space included). Override with --glob-sep.
# Opt in to a confirmation prompt with --interactive/-i (or always_prompt_delete in config). # Opt in to a confirmation prompt with --interactive/-i (or always_prompt_delete in config).
pda rm kitty -i 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. Long values are truncated to fit the terminal. Use `--full`/`-f` to show the complete value.
```bash ```bash
pda ls pda ls
# Key Value TTL # Key Value TTL
# note this is a very long (..30 more chars) no expiry # note this is a very long (..30 more chars) no expiry
pda ls --full pda ls --full
# Key Value TTL # Key Value TTL
# note this is a very long value that keeps on going and going no expiry # 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 pda export > my_backup
# Export only matching keys. # Export only matching keys.
pda export --glob a* pda export --key "a*"
# Export only entries whose values contain a URL.
pda export --value "**https**"
``` ```
<p align="center"></p><!-- spacer --> <p align="center"></p><!-- spacer -->
@ -241,7 +243,7 @@ pda import < my_backup
# ok restored 2 entries into @default # ok restored 2 entries into @default
# Import only matching keys. # Import only matching keys.
pda import --glob a* -f my_backup pda import --key "a*" -f my_backup
``` ```
<p align="center"></p><!-- spacer --> <p align="center"></p><!-- spacer -->
@ -257,7 +259,7 @@ pda list-stores
# @birthdays # @birthdays
# Check out a specific store. # Check out a specific store.
pda ls @birthdays pda ls @birthdays --no-header --no-ttl
# alice 11/11/1998 # alice 11/11/1998
# bob 05/12/1980 # bob 05/12/1980
@ -407,17 +409,17 @@ pda get hello --no-template
<p align="center"></p><!-- spacer --> <p align="center"></p><!-- spacer -->
### 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.
<p align="center"></p><!-- spacer --> <p align="center"></p><!-- spacer -->
`*` 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 ```bash
pda ls --no-values pda ls --no-values --no-header
# cat # cat
# dog # dog
# cog # cog
@ -425,16 +427,16 @@ pda ls --no-values
# mouse house # mouse house
# foo.bar.baz # foo.bar.baz
pda ls --glob "*" pda ls --key "*"
# cat # cat
# dog # dog
# cog # cog
pda ls --glob "* *" pda ls --key "* *"
# mouse hotdog # mouse hotdog
# mouse house # mouse house
pda ls --glob "foo.*.baz" pda ls --key "foo.*.baz"
# foo.bar.baz # foo.bar.baz
``` ```
@ -442,10 +444,10 @@ pda ls --glob "foo.*.baz"
`**` super-wildcards ignore word boundaries. `**` super-wildcards ignore word boundaries.
```bash ```bash
pda ls --glob "foo**" pda ls --key "foo**"
# foo.bar.baz # foo.bar.baz
pda ls --glob "**g" pda ls --key "**g"
# dog # dog
# cog # cog
# mouse hotdog # mouse hotdog
@ -455,7 +457,7 @@ pda ls --glob "**g"
`?` wildcards a single letter. `?` wildcards a single letter.
```bash ```bash
pda ls --glob ?og pda ls --key "?og"
# dog # dog
# cog # cog
# frog --> fail # frog --> fail
@ -466,13 +468,13 @@ pda ls --glob ?og
`[abc]` must match one of the characters in the brackets. `[abc]` must match one of the characters in the brackets.
```bash ```bash
pda ls --glob [dc]og pda ls --key "[dc]og"
# dog # dog
# cog # cog
# bog --> fail # bog --> fail
# Can be negated with '!' # Can be negated with '!'
pda ls --glob [!dc]og pda ls --key "[!dc]og"
# dog --> fail # dog --> fail
# cog --> fail # cog --> fail
# bog # bog
@ -480,20 +482,20 @@ pda ls --glob [!dc]og
<p align="center"></p><!-- spacer --> <p align="center"></p><!-- spacer -->
`[a-c]` must fall within the range given in the brackets `[a-c]` must fall within the range given in the brackets.
```bash ```bash
pda ls --glob [a-g]ag pda ls --key "[a-g]ag"
# bag # bag
# gag # gag
# wag --> fail # wag --> fail
# Can be negated with '!' # Can be negated with '!'
pda ls --glob [!a-g]ag pda ls --key "[!a-g]ag"
# bag --> fail # bag --> fail
# gag --> fail # gag --> fail
# wag # wag
pda ls --glob 19[90-99] pda ls --key "19[90-99]"
# 1991 # 1991
# 1992 # 1992
# 2001 --> fail # 2001 --> fail
@ -502,39 +504,33 @@ pda ls --glob 19[90-99]
<p align="center"></p><!-- spacer --> <p align="center"></p><!-- spacer -->
Globs can be arbitrarily complex, and can be combined with strict matches. `--value` filters by value content using the same glob syntax.
```bash ```bash
pda ls --no-keys pda ls --value "**localhost**"
# cat # db-url postgres://localhost:5432 no expiry
# mouse trap
# dog house
# cat flap
# cogwheel
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
```
<p align="center"></p><!-- spacer -->
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) # ??? remove 'cat'? (y/n)
# ==> y # ==> y
# ??? remove 'mouse trap'? (y/n) # ??? remove 'mouse trap'? (y/n)
# ... # ...
``` ```
<p align="center"></p><!-- spacer --> Locked (encrypted without an available identity) and non-UTF-8 (binary) entries are silently excluded from `--value` matching.
`--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
```
<p align="center"></p><!-- spacer --> <p align="center"></p><!-- spacer -->

View file

@ -47,20 +47,16 @@ func del(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
globPatterns, err := cmd.Flags().GetStringSlice("glob") keyPatterns, err := cmd.Flags().GetStringSlice("key")
if err != nil {
return err
}
separators, err := parseGlobSeparators(cmd)
if err != nil { if err != nil {
return err return err
} }
if len(args) == 0 && len(globPatterns) == 0 { if len(args) == 0 && len(keyPatterns) == 0 {
return fmt.Errorf("cannot remove: no keys provided") return fmt.Errorf("cannot remove: no keys provided")
} }
targets, err := resolveDeleteTargets(store, args, globPatterns, separators) targets, err := resolveDeleteTargets(store, args, keyPatterns)
if err != nil { if err != nil {
return err return err
} }
@ -124,8 +120,7 @@ func del(cmd *cobra.Command, args []string) error {
func init() { func init() {
delCmd.Flags().BoolP("interactive", "i", false, "Prompt yes/no for each deletion") 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().StringSliceP("key", "k", nil, "Delete keys matching glob pattern (repeatable)")
delCmd.Flags().String("glob-sep", "", fmt.Sprintf("Characters treated as separators for globbing (default '%s')", defaultGlobSeparatorsDisplay()))
rootCmd.AddCommand(delCmd) rootCmd.AddCommand(delCmd)
} }
@ -152,7 +147,7 @@ func keyExists(store *Store, arg string) (bool, error) {
return findEntry(entries, spec.Key) >= 0, nil 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{}) targetSet := make(map[string]struct{})
var targets []resolvedTarget var targets []resolvedTarget
@ -202,7 +197,7 @@ func resolveDeleteTargets(store *Store, exactArgs []string, globPatterns []strin
return nil, err return nil, err
} }
pattern := spec.Key pattern := spec.Key
m, err := glob.Compile(pattern, separators...) m, err := glob.Compile(pattern, defaultGlobSeparators...)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot remove '%s': %v", raw, err) return nil, fmt.Errorf("cannot remove '%s': %v", raw, err)
} }

View file

@ -23,8 +23,6 @@ THE SOFTWARE.
package cmd package cmd
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -41,7 +39,7 @@ var exportCmd = &cobra.Command{
} }
func init() { func init() {
exportCmd.Flags().StringSliceP("glob", "g", nil, "Filter keys with glob pattern (repeatable)") exportCmd.Flags().StringSliceP("key", "k", 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("value", "v", nil, "Filter values with regex pattern (repeatable)")
rootCmd.AddCommand(exportCmd) rootCmd.AddCommand(exportCmd)
} }

View file

@ -27,34 +27,14 @@ import (
"strings" "strings"
"github.com/gobwas/glob" "github.com/gobwas/glob"
"github.com/spf13/cobra"
) )
var defaultGlobSeparators = []rune{'/', '-', '_', '.', '@', ':', ' '} var defaultGlobSeparators = []rune{'/', '-', '_', '.', '@', ':', ' '}
func defaultGlobSeparatorsDisplay() string { func compileGlobMatchers(patterns []string) ([]glob.Glob, error) {
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) {
var matchers []glob.Glob var matchers []glob.Glob
for _, pattern := range patterns { for _, pattern := range patterns {
m, err := glob.Compile(strings.ToLower(pattern), separators...) m, err := glob.Compile(strings.ToLower(pattern), defaultGlobSeparators...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -119,15 +119,20 @@ func list(cmd *cobra.Command, args []string) error {
columns = append(columns, columnTTL) columns = append(columns, columnTTL)
} }
globPatterns, err := cmd.Flags().GetStringSlice("glob") keyPatterns, err := cmd.Flags().GetStringSlice("key")
if err != nil { if err != nil {
return fmt.Errorf("cannot ls '%s': %v", targetDB, err) return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
} }
separators, err := parseGlobSeparators(cmd) matchers, err := compileGlobMatchers(keyPatterns)
if err != nil { if err != nil {
return fmt.Errorf("cannot ls '%s': %v", targetDB, err) 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 { if err != nil {
return fmt.Errorf("cannot ls '%s': %v", targetDB, err) 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) return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
} }
// Filter by glob // Filter by key glob and value regex
var filtered []Entry var filtered []Entry
for _, e := range entries { for _, e := range entries {
if globMatch(matchers, e.Key) { if globMatch(matchers, e.Key) && valueMatch(valueMatchers, e) {
filtered = append(filtered, e) filtered = append(filtered, e)
} }
} }
if len(matchers) > 0 && len(filtered) == 0 { if (len(matchers) > 0 || len(valueMatchers) > 0) && len(filtered) == 0 {
return fmt.Errorf("cannot ls '%s': no matches for pattern %s", targetDB, formatGlobPatterns(globPatterns)) 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() output := cmd.OutOrStdout()
@ -467,7 +479,7 @@ func init() {
listCmd.Flags().BoolVarP(&listFull, "full", "f", false, "show full values without truncation") 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().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().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().StringSliceP("key", "k", 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("value", "v", nil, "Filter values with regex pattern (repeatable)")
rootCmd.AddCommand(listCmd) rootCmd.AddCommand(listCmd)
} }

70
cmd/match.go Normal file
View 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, ", ")
}

View file

@ -56,15 +56,11 @@ func restore(cmd *cobra.Command, args []string) error {
} }
displayTarget := "@" + dbName displayTarget := "@" + dbName
globPatterns, err := cmd.Flags().GetStringSlice("glob") keyPatterns, err := cmd.Flags().GetStringSlice("key")
if err != nil { if err != nil {
return fmt.Errorf("cannot restore '%s': %v", displayTarget, err) return fmt.Errorf("cannot restore '%s': %v", displayTarget, err)
} }
separators, err := parseGlobSeparators(cmd) matchers, err := compileGlobMatchers(keyPatterns)
if err != nil {
return fmt.Errorf("cannot restore '%s': %v", displayTarget, err)
}
matchers, err := compileGlobMatchers(globPatterns, separators)
if err != nil { if err != nil {
return fmt.Errorf("cannot restore '%s': %v", displayTarget, err) 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 { 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) okf("restored %d entries into @%s", restored, dbName)
@ -208,8 +204,7 @@ func restoreEntries(decoder *json.Decoder, storePath string, opts restoreOpts) (
func init() { func init() {
restoreCmd.Flags().StringP("file", "f", "", "Path to an NDJSON dump (defaults to stdin)") 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().StringSliceP("key", "k", 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().BoolP("interactive", "i", false, "Prompt before overwriting existing keys") restoreCmd.Flags().BoolP("interactive", "i", false, "Prompt before overwriting existing keys")
restoreCmd.Flags().Bool("drop", false, "Drop existing entries before restoring (full replace)") restoreCmd.Flags().Bool("drop", false, "Drop existing entries before restoring (full replace)")
rootCmd.AddCommand(restoreCmd) rootCmd.AddCommand(restoreCmd)

View file

@ -1,8 +1,8 @@
$ pda set a1 1 $ pda set a1 1
$ pda set a2 2 $ pda set a2 2
$ pda set b1 3 $ pda set b1 3
$ pda dump --glob a* $ pda dump --key "a*"
{"key":"a1","value":"1","encoding":"text"} {"key":"a1","value":"1","encoding":"text"}
{"key":"a2","value":"2","encoding":"text"} {"key":"a2","value":"2","encoding":"text"}
$ pda dump --glob c* --> FAIL $ pda dump --key "c*" --> FAIL
FAIL cannot ls '@default': no matches for pattern 'c*' FAIL cannot ls '@default': no matches for key pattern 'c*'

8
testdata/dump__value__ok.ct vendored Normal file
View file

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

View file

@ -9,9 +9,9 @@ Aliases:
export, dump export, dump
Flags: Flags:
-g, --glob strings Filter keys with glob pattern (repeatable) -h, --help help for export
--glob-sep string Characters treated as separators for globbing (default '/-_.@: ') -k, --key strings Filter keys with glob pattern (repeatable)
-h, --help help for export -v, --value strings Filter values with regex pattern (repeatable)
Export store as NDJSON (alias for list --format ndjson) Export store as NDJSON (alias for list --format ndjson)
Usage: Usage:
@ -21,6 +21,6 @@ Aliases:
export, dump export, dump
Flags: Flags:
-g, --glob strings Filter keys with glob pattern (repeatable) -h, --help help for export
--glob-sep string Characters treated as separators for globbing (default '/-_.@: ') -k, --key strings Filter keys with glob pattern (repeatable)
-h, --help help for export -v, --value strings Filter values with regex pattern (repeatable)

View file

@ -9,16 +9,16 @@ Aliases:
list, ls list, ls
Flags: Flags:
-b, --base64 view binary data as base64 -b, --base64 view binary data as base64
-o, --format format output format (table|tsv|csv|markdown|html|ndjson) (default table) -o, --format format output format (table|tsv|csv|markdown|html|ndjson) (default table)
-f, --full show full values without truncation -f, --full show full values without truncation
-g, --glob strings Filter keys with glob pattern (repeatable) -h, --help help for list
--glob-sep string Characters treated as separators for globbing (default '/-_.@: ') -k, --key strings Filter keys with glob pattern (repeatable)
-h, --help help for list --no-header suppress the header row
--no-header suppress the header row --no-keys suppress the key column
--no-keys suppress the key column --no-ttl suppress the TTL column
--no-ttl suppress the TTL column --no-values suppress the value column
--no-values suppress the value column -v, --value strings Filter values with regex pattern (repeatable)
List the contents of a store List the contents of a store
Usage: Usage:
@ -28,13 +28,13 @@ Aliases:
list, ls list, ls
Flags: Flags:
-b, --base64 view binary data as base64 -b, --base64 view binary data as base64
-o, --format format output format (table|tsv|csv|markdown|html|ndjson) (default table) -o, --format format output format (table|tsv|csv|markdown|html|ndjson) (default table)
-f, --full show full values without truncation -f, --full show full values without truncation
-g, --glob strings Filter keys with glob pattern (repeatable) -h, --help help for list
--glob-sep string Characters treated as separators for globbing (default '/-_.@: ') -k, --key strings Filter keys with glob pattern (repeatable)
-h, --help help for list --no-header suppress the header row
--no-header suppress the header row --no-keys suppress the key column
--no-keys suppress the key column --no-ttl suppress the TTL column
--no-ttl suppress the TTL column --no-values suppress the value column
--no-values suppress the value column -v, --value strings Filter values with regex pattern (repeatable)

View file

@ -9,10 +9,9 @@ Aliases:
remove, rm remove, rm
Flags: Flags:
-g, --glob strings Delete keys matching glob pattern (repeatable) -h, --help help for remove
--glob-sep string Characters treated as separators for globbing (default '/-_.@: ') -i, --interactive Prompt yes/no for each deletion
-h, --help help for remove -k, --key strings Delete keys matching glob pattern (repeatable)
-i, --interactive Prompt yes/no for each deletion
Delete one or more keys Delete one or more keys
Usage: Usage:
@ -22,7 +21,6 @@ Aliases:
remove, rm remove, rm
Flags: Flags:
-g, --glob strings Delete keys matching glob pattern (repeatable) -h, --help help for remove
--glob-sep string Characters treated as separators for globbing (default '/-_.@: ') -i, --interactive Prompt yes/no for each deletion
-h, --help help for remove -k, --key strings Delete keys matching glob pattern (repeatable)
-i, --interactive Prompt yes/no for each deletion

View file

@ -9,12 +9,11 @@ Aliases:
import, restore import, restore
Flags: Flags:
--drop Drop existing entries before restoring (full replace) --drop Drop existing entries before restoring (full replace)
-f, --file string Path to an NDJSON dump (defaults to stdin) -f, --file string Path to an NDJSON dump (defaults to stdin)
-g, --glob strings Restore keys matching glob pattern (repeatable) -h, --help help for import
--glob-sep string Characters treated as separators for globbing (default '/-_.@: ') -i, --interactive Prompt before overwriting existing keys
-h, --help help for import -k, --key strings Restore keys matching glob pattern (repeatable)
-i, --interactive Prompt before overwriting existing keys
Restore key/value pairs from an NDJSON dump Restore key/value pairs from an NDJSON dump
Usage: Usage:
@ -24,9 +23,8 @@ Aliases:
import, restore import, restore
Flags: Flags:
--drop Drop existing entries before restoring (full replace) --drop Drop existing entries before restoring (full replace)
-f, --file string Path to an NDJSON dump (defaults to stdin) -f, --file string Path to an NDJSON dump (defaults to stdin)
-g, --glob strings Restore keys matching glob pattern (repeatable) -h, --help help for import
--glob-sep string Characters treated as separators for globbing (default '/-_.@: ') -i, --interactive Prompt before overwriting existing keys
-h, --help help for import -k, --key strings Restore keys matching glob pattern (repeatable)
-i, --interactive Prompt before overwriting existing keys

View file

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

12
testdata/list__key__ok.ct vendored Normal file
View file

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

11
testdata/list__key__value__ok.ct vendored Normal file
View file

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

8
testdata/list__value__multi__ok.ct vendored Normal file
View file

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

15
testdata/list__value__ok.ct vendored Normal file
View file

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

View file

@ -1,16 +1,19 @@
$ pda set foo 1 $ pda set foo 1
$ pda set bar 2 $ pda set bar 2
$ pda ls $ pda ls
Key Value TTL Key Value TTL
a echo hello (..1 more chars) no expiry a echo hello (..1 more chars) no expiry
a1 1 no expiry a1 1 no expiry
a2 2 no expiry a2 2 no expiry
b1 3 no expiry b1 3 no expiry
bar 2 no expiry bar 2 no expiry
copied-key hidden-value no expiry copied-key hidden-value no expiry
foo 1 no expiry foo 1 no expiry
moved-key hidden-value no expiry greeting hello world (..1 more chars) no expiry
$ pda rm foo --glob "*" moved-key hidden-value no expiry
number 42 no expiry
url https://example.com no expiry
$ pda rm foo --key "*"
$ pda get bar --> FAIL $ pda get bar --> FAIL
FAIL cannot get 'bar': no such key FAIL cannot get 'bar': no such key
$ pda get foo --> FAIL $ pda get foo --> FAIL

View file

@ -1,7 +1,7 @@
$ pda set foo 1 $ pda set foo 1
$ pda set bar1 2 $ pda set bar1 2
$ pda set bar2 3 $ pda set bar2 3
$ pda rm foo --glob bar* $ pda rm foo --key "bar*"
$ pda get foo --> FAIL $ pda get foo --> FAIL
FAIL cannot get 'foo': no such key FAIL cannot get 'foo': no such key
$ pda get bar1 --> FAIL $ pda get bar1 --> FAIL

View file

@ -1,7 +1,7 @@
$ pda set a1 1 $ pda set a1 1
$ pda set a2 2 $ pda set a2 2
$ pda set b1 3 $ pda set b1 3
$ pda rm --glob a* $ pda rm --key "a*"
$ pda get a1 --> FAIL $ pda get a1 --> FAIL
FAIL cannot get 'a1': no such key FAIL cannot get 'a1': no such key
hint did you mean 'b1'? hint did you mean 'b1'?

View file

@ -3,7 +3,7 @@ $ pda set a2 2
$ pda set b1 3 $ pda set b1 3
$ fecho dumpfile {"key":"a1","value":"1","encoding":"text"} {"key":"a2","value":"2","encoding":"text"} {"key":"b1","value":"3","encoding":"text"} $ 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 rm a1 a2 b1
$ pda restore --glob a* --file dumpfile $ pda restore --key "a*" --file dumpfile
ok restored 2 entries into @default ok restored 2 entries into @default
$ pda get a1 $ pda get a1
1 1
@ -12,5 +12,5 @@ $ pda get a2
$ pda get b1 --> FAIL $ pda get b1 --> FAIL
FAIL cannot get 'b1': no such key FAIL cannot get 'b1': no such key
hint did you mean 'a1'? hint did you mean 'a1'?
$ pda restore --glob c* --file dumpfile --> FAIL $ pda restore --key "c*" --file dumpfile --> FAIL
FAIL cannot restore '@default': no matches for pattern 'c*' FAIL cannot restore '@default': no matches for key pattern 'c*'