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

104
README.md
View file

@ -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
@ -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**"
```
<p align="center"></p><!-- spacer -->
@ -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
```
<p align="center"></p><!-- spacer -->
@ -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
<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 -->
`*` 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
<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
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]
<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
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
```
<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)
# ==> y
# ??? remove 'mouse trap'? (y/n)
# ...
```
<p align="center"></p><!-- spacer -->
`--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.
<p align="center"></p><!-- spacer -->

View file

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

View file

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

View file

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

View file

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

View file

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

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
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
-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
-k, --key strings Filter keys with glob pattern (repeatable)
-v, --value strings Filter values with regex pattern (repeatable)

View file

@ -12,13 +12,13 @@ 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
-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:
@ -31,10 +31,10 @@ 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
-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)

View file

@ -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
-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
-k, --key strings Delete keys matching glob pattern (repeatable)

View file

@ -11,10 +11,9 @@ Aliases:
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
-k, --key strings Restore keys matching glob pattern (repeatable)
Restore key/value pairs from an NDJSON dump
Usage:
@ -26,7 +25,6 @@ Aliases:
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
-k, --key strings Restore keys matching glob pattern (repeatable)

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

@ -9,8 +9,11 @@ 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
$ pda rm foo --glob "*"
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

View file

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

View file

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

View file

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