feat(commit): text templating in commit messages

This commit is contained in:
Lewis Wynne 2026-02-12 20:00:57 +00:00
parent 4e78cefd56
commit 2ca32769d5
30 changed files with 281 additions and 115 deletions

View file

@ -394,7 +394,7 @@ Values support effectively all of Go's `text/template` syntax. Templates are eva
`text/template` is a Turing-complete templating library that supports most of what you'd expect in a scripting language. Actions are given with ``{{ action }}`` syntax and support pipelines and nested templates, along with a lot more. I recommend reading the documentation if you want to do anything more complicated than described here. `text/template` is a Turing-complete templating library that supports most of what you'd expect in a scripting language. Actions are given with ``{{ action }}`` syntax and support pipelines and nested templates, along with a lot more. I recommend reading the documentation if you want to do anything more complicated than described here.
To fit `text/template` nicely into this tool, pda has a sparse set of additional functions built-in. For example, `default` values, `enum`s, `require`d values, `lists`, among others. To fit `text/template` nicely into this tool, pda has a sparse set of additional functions built-in. For example, `default` values, `enum`s, `require`d values, `time`, `lists`, among others. These same functions are also available in `git.default_commit_message` templates, along with `{{ summary }}` which returns the action that triggered the commit (e.g. "set foo", "removed bar").
Below is more detail on the extra functions added by this tool. Below is more detail on the extra functions added by this tool.
@ -438,6 +438,15 @@ pda get my_name
<p align="center"></p><!-- spacer --> <p align="center"></p><!-- spacer -->
`time` returns the current UTC time in RFC3339 format.
```bash
pda set note "Created at {{ time }}"
pda get note
# Created at 2025-01-15T12:00:00Z
```
<p align="center"></p><!-- spacer -->
`enum` restricts acceptable values. `enum` restricts acceptable values.
```bash ```bash
pda set level "Log level: {{ enum .LEVEL "info" "warn" "error" }}" pda set level "Log level: {{ enum .LEVEL "info" "warn" "error" }}"
@ -854,8 +863,9 @@ auto_fetch = false
auto_commit = false auto_commit = false
# auto push after committing # auto push after committing
auto_push = false auto_push = false
# commit message template ({{.Time}} is replaced with RFC3339 timestamp) # commit message if none manually specified
default_commit_message = "sync: {{.Time}}" # supports templates, see: #templates section
default_commit_message = "{{ summary }} {{ time }}"
``` ```
<p align="center"></p><!-- spacer --> <p align="center"></p><!-- spacer -->

48
cmd/commit_message.go Normal file
View file

@ -0,0 +1,48 @@
/*
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 (
"bytes"
"text/template"
)
// renderCommitMessage renders a commit message template. It extends the
// shared template FuncMap with {{ summary }}, which returns the action
// description for the current commit. On any error the raw template string
// is returned so that commits are never blocked by a bad template.
func renderCommitMessage(tmpl string, summary string) string {
funcMap := templateFuncMap()
funcMap["summary"] = func() string { return summary }
t, err := template.New("commit").Option("missingkey=zero").Funcs(funcMap).Parse(tmpl)
if err != nil {
return tmpl
}
var buf bytes.Buffer
if err := t.Execute(&buf, nil); err != nil {
return tmpl
}
return buf.String()
}

View file

@ -0,0 +1,53 @@
package cmd
import (
"strings"
"testing"
)
func TestRenderCommitMessage(t *testing.T) {
t.Run("summary and time", func(t *testing.T) {
msg := renderCommitMessage("{{ summary }} {{ time }}", "set foo")
if !strings.HasPrefix(msg, "set foo ") {
t.Errorf("expected prefix 'set foo ', got %q", msg)
}
parts := strings.SplitN(msg, " ", 3)
if len(parts) < 3 || !strings.Contains(parts[2], "T") {
t.Errorf("expected RFC3339 time, got %q", msg)
}
})
t.Run("empty summary", func(t *testing.T) {
msg := renderCommitMessage("{{ summary }} {{ time }}", "")
if !strings.HasPrefix(msg, " ") {
t.Errorf("expected leading space (empty summary), got %q", msg)
}
})
t.Run("default function", func(t *testing.T) {
msg := renderCommitMessage(`{{ default "sync" (summary) }}`, "")
if msg != "sync" {
t.Errorf("expected 'sync', got %q", msg)
}
msg = renderCommitMessage(`{{ default "sync" (summary) }}`, "set foo")
if msg != "set foo" {
t.Errorf("expected 'set foo', got %q", msg)
}
})
t.Run("env function", func(t *testing.T) {
t.Setenv("PDA_TEST_USER", "alice")
msg := renderCommitMessage(`{{ env "PDA_TEST_USER" }}: {{ summary }}`, "set foo")
if msg != "alice: set foo" {
t.Errorf("expected 'alice: set foo', got %q", msg)
}
})
t.Run("bad template returns raw", func(t *testing.T) {
raw := "{{ bad template"
msg := renderCommitMessage(raw, "test")
if msg != raw {
t.Errorf("expected raw %q, got %q", raw, msg)
}
})
}

View file

@ -117,7 +117,7 @@ func defaultConfig() Config {
AutoFetch: false, AutoFetch: false,
AutoCommit: false, AutoCommit: false,
AutoPush: false, AutoPush: false,
DefaultCommitMessage: "sync: {{.Time}}", DefaultCommitMessage: "{{ summary }} {{ time }}",
}, },
} }
} }

View file

@ -79,7 +79,7 @@ func delStore(cmd *cobra.Command, args []string) error {
if err := executeDeletion(path); err != nil { if err := executeDeletion(path); err != nil {
return err return err
} }
return autoSync() return autoSync(fmt.Sprintf("removed @%s", dbName))
} }
func executeDeletion(path string) error { func executeDeletion(path string) error {

View file

@ -107,6 +107,7 @@ func del(cmd *cobra.Command, args []string) error {
return nil return nil
} }
var removedNames []string
for _, dbName := range storeOrder { for _, dbName := range storeOrder {
st := byStore[dbName] st := byStore[dbName]
p, err := store.storePath(dbName) p, err := store.storePath(dbName)
@ -123,13 +124,14 @@ func del(cmd *cobra.Command, args []string) error {
return fmt.Errorf("cannot remove '%s': no such key", t.full) return fmt.Errorf("cannot remove '%s': no such key", t.full)
} }
entries = append(entries[:idx], entries[idx+1:]...) entries = append(entries[:idx], entries[idx+1:]...)
removedNames = append(removedNames, t.display)
} }
if err := writeStoreFile(p, entries, nil); err != nil { if err := writeStoreFile(p, entries, nil); err != nil {
return err return err
} }
} }
return autoSync() return autoSync("removed " + strings.Join(removedNames, ", "))
} }
func init() { func init() {

View file

@ -27,8 +27,6 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"slices"
"strconv"
"strings" "strings"
"text/template" "text/template"
@ -150,57 +148,11 @@ func applyTemplate(tplBytes []byte, substitutions []string) ([]byte, error) {
val := parts[1] val := parts[1]
vars[key] = val vars[key] = val
} }
funcMap := template.FuncMap{
"require": func(v any) (string, error) {
s := fmt.Sprint(v)
if s == "" {
return "", fmt.Errorf("required value is missing or empty")
}
return s, nil
},
"default": func(def string, v any) string {
s := fmt.Sprint(v)
if s == "" {
return def
}
return s
},
"env": os.Getenv,
"enum": func(v any, allowed ...string) (string, error) {
s := fmt.Sprint(v)
if s == "" {
return "", fmt.Errorf("enum value is missing or empty")
}
if slices.Contains(allowed, s) {
return s, nil
}
return "", fmt.Errorf("invalid value '%s', allowed: %v", s, allowed)
},
"int": func(v any) (int, error) {
s := fmt.Sprint(v)
i, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("cannot convert to int: %w", err)
}
return i, nil
},
"list": func(v any) []string {
s := fmt.Sprint(v)
if s == "" {
return nil
}
parts := strings.Split(s, ",")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
return parts
},
}
tpl, err := template.New("cmd"). tpl, err := template.New("cmd").
Delims("{{", "}}"). Delims("{{", "}}").
// Render missing map keys as zero values so the default helper can decide on fallbacks. // Render missing map keys as zero values so the default helper can decide on fallbacks.
Option("missingkey=zero"). Option("missingkey=zero").
Funcs(funcMap). Funcs(templateFuncMap()).
Parse(string(tplBytes)) Parse(string(tplBytes))
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -102,6 +102,7 @@ func mvStore(cmd *cobra.Command, args []string) error {
} }
copy, _ := cmd.Flags().GetBool("copy") copy, _ := cmd.Flags().GetBool("copy")
var summary string
if copy { if copy {
data, err := os.ReadFile(fromPath) data, err := os.ReadFile(fromPath)
if err != nil { if err != nil {
@ -111,13 +112,15 @@ func mvStore(cmd *cobra.Command, args []string) error {
return fmt.Errorf("cannot copy store '%s': %v", fromName, err) return fmt.Errorf("cannot copy store '%s': %v", fromName, err)
} }
okf("copied @%s to @%s", fromName, toName) okf("copied @%s to @%s", fromName, toName)
summary = fmt.Sprintf("copied @%s to @%s", fromName, toName)
} else { } else {
if err := os.Rename(fromPath, toPath); err != nil { if err := os.Rename(fromPath, toPath); err != nil {
return fmt.Errorf("cannot rename store '%s': %v", fromName, err) return fmt.Errorf("cannot rename store '%s': %v", fromName, err)
} }
okf("renamed @%s to @%s", fromName, toName) okf("renamed @%s to @%s", fromName, toName)
summary = fmt.Sprintf("moved @%s to @%s", fromName, toName)
} }
return autoSync() return autoSync(summary)
} }
func init() { func init() {

View file

@ -182,12 +182,15 @@ func mvImpl(cmd *cobra.Command, args []string, keepSource bool) error {
} }
} }
var summary string
if keepSource { if keepSource {
okf("copied %s to %s", fromSpec.Display(), toSpec.Display()) okf("copied %s to %s", fromSpec.Display(), toSpec.Display())
summary = "copied " + fromSpec.Display() + " to " + toSpec.Display()
} else { } else {
okf("renamed %s to %s", fromSpec.Display(), toSpec.Display()) okf("renamed %s to %s", fromSpec.Display(), toSpec.Display())
summary = "moved " + fromSpec.Display() + " to " + toSpec.Display()
} }
return autoSync() return autoSync(summary)
} }
func init() { func init() {

View file

@ -127,6 +127,7 @@ func restore(cmd *cobra.Command, args []string) error {
// When a specific store is given, all entries go there (original behaviour). // When a specific store is given, all entries go there (original behaviour).
// Otherwise, route entries to their original store via the "store" field. // Otherwise, route entries to their original store via the "store" field.
var summary string
if explicitStore { if explicitStore {
p, err := store.storePath(targetDB) p, err := store.storePath(targetDB)
if err != nil { if err != nil {
@ -140,6 +141,7 @@ func restore(cmd *cobra.Command, args []string) error {
return err return err
} }
okf("restored %d entries into @%s", restored, targetDB) okf("restored %d entries into @%s", restored, targetDB)
summary = fmt.Sprintf("imported %d entries into @%s", restored, targetDB)
} else { } else {
restored, err := restoreEntries(decoder, nil, targetDB, opts) restored, err := restoreEntries(decoder, nil, targetDB, opts)
if err != nil { if err != nil {
@ -149,9 +151,10 @@ func restore(cmd *cobra.Command, args []string) error {
return err return err
} }
okf("restored %d entries", restored) okf("restored %d entries", restored)
summary = fmt.Sprintf("imported %d entries", restored)
} }
return autoSync() return autoSync(summary)
} }
func reportRestoreFilters(displayTarget string, restored int, matchers []glob.Glob, keyPatterns []string, storeMatchers []glob.Glob, storePatterns []string) error { func reportRestoreFilters(displayTarget string, restored int, matchers []glob.Glob, keyPatterns []string, storeMatchers []glob.Glob, storePatterns []string) error {

View file

@ -176,7 +176,7 @@ func set(cmd *cobra.Command, args []string) error {
return fmt.Errorf("cannot set '%s': %v", args[0], err) return fmt.Errorf("cannot set '%s': %v", args[0], err)
} }
return autoSync() return autoSync("set " + spec.Display())
} }
func init() { func init() {

View file

@ -23,9 +23,6 @@ THE SOFTWARE.
package cmd package cmd
import ( import (
"strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -35,7 +32,7 @@ var syncCmd = &cobra.Command{
SilenceUsage: true, SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
msg, _ := cmd.Flags().GetString("message") msg, _ := cmd.Flags().GetString("message")
return sync(true, msg) return sync(true, msg, "sync")
}, },
} }
@ -44,7 +41,7 @@ func init() {
rootCmd.AddCommand(syncCmd) rootCmd.AddCommand(syncCmd)
} }
func sync(manual bool, customMsg string) error { func sync(manual bool, customMsg string, summary string) error {
repoDir, err := ensureVCSInitialized() repoDir, err := ensureVCSInitialized()
if err != nil { if err != nil {
return err return err
@ -66,7 +63,7 @@ func sync(manual bool, customMsg string) error {
if changed { if changed {
msg := customMsg msg := customMsg
if msg == "" { if msg == "" {
msg = strings.ReplaceAll(config.Git.DefaultCommitMessage, "{{.Time}}", time.Now().UTC().Format(time.RFC3339)) msg = renderCommitMessage(config.Git.DefaultCommitMessage, summary)
if manual { if manual {
printHint("use -m to set a custom commit message") printHint("use -m to set a custom commit message")
} }
@ -123,12 +120,12 @@ func sync(manual bool, customMsg string) error {
return nil return nil
} }
func autoSync() error { func autoSync(summary string) error {
if !config.Git.AutoCommit { if !config.Git.AutoCommit {
return nil return nil
} }
if _, err := ensureVCSInitialized(); err != nil { if _, err := ensureVCSInitialized(); err != nil {
return nil return nil
} }
return sync(false, "") return sync(false, "", summary)
} }

85
cmd/template.go Normal file
View file

@ -0,0 +1,85 @@
/*
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"
"os"
"slices"
"strconv"
"strings"
"text/template"
"time"
)
// templateFuncMap returns the shared FuncMap used by both value templates
// (pda get) and commit message templates.
func templateFuncMap() template.FuncMap {
return template.FuncMap{
"require": func(v any) (string, error) {
s := fmt.Sprint(v)
if s == "" {
return "", fmt.Errorf("required value is missing or empty")
}
return s, nil
},
"default": func(def string, v any) string {
s := fmt.Sprint(v)
if s == "" {
return def
}
return s
},
"env": os.Getenv,
"enum": func(v any, allowed ...string) (string, error) {
s := fmt.Sprint(v)
if s == "" {
return "", fmt.Errorf("enum value is missing or empty")
}
if slices.Contains(allowed, s) {
return s, nil
}
return "", fmt.Errorf("invalid value '%s', allowed: %v", s, allowed)
},
"int": func(v any) (int, error) {
s := fmt.Sprint(v)
i, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("cannot convert to int: %w", err)
}
return i, nil
},
"list": func(v any) []string {
s := fmt.Sprint(v)
if s == "" {
return nil
}
parts := strings.Split(s, ",")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
return parts
},
"time": func() string { return time.Now().UTC().Format(time.RFC3339) },
}
}

View file

@ -36,20 +36,6 @@ import (
var update = flag.Bool("update", false, "update test files with results") var update = flag.Bool("update", false, "update test files with results")
func TestMain(t *testing.T) { func TestMain(t *testing.T) {
t.Setenv("PDA_DATA", t.TempDir())
configDir := t.TempDir()
t.Setenv("PDA_CONFIG", configDir)
// Pre-create an age identity so encryption tests don't print a
// creation message with a non-deterministic path.
id, err := age.GenerateX25519Identity()
if err != nil {
t.Fatalf("generate identity: %v", err)
}
if err := os.WriteFile(filepath.Join(configDir, "identity.txt"), []byte(id.String()+"\n"), 0o600); err != nil {
t.Fatalf("write identity: %v", err)
}
ts, err := cmdtest.Read("testdata") ts, err := cmdtest.Read("testdata")
if err != nil { if err != nil {
t.Fatalf("read testdata: %v", err) t.Fatalf("read testdata: %v", err)
@ -59,5 +45,29 @@ func TestMain(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
ts.Commands["pda"] = cmdtest.Program(bin) ts.Commands["pda"] = cmdtest.Program(bin)
// Each .ct file gets its own isolated data and config directories
// inside its ROOTDIR, so tests cannot leak state to each other.
ts.Setup = func(rootDir string) error {
dataDir := filepath.Join(rootDir, "data")
configDir := filepath.Join(rootDir, "config")
if err := os.MkdirAll(dataDir, 0o755); err != nil {
return err
}
if err := os.MkdirAll(configDir, 0o755); err != nil {
return err
}
os.Setenv("PDA_DATA", dataDir)
os.Setenv("PDA_CONFIG", configDir)
// Pre-create an age identity so encryption tests don't print
// a creation message with a non-deterministic path.
id, err := age.GenerateX25519Identity()
if err != nil {
return err
}
return os.WriteFile(filepath.Join(configDir, "identity.txt"), []byte(id.String()+"\n"), 0o600)
}
ts.Run(t, *update) ts.Run(t, *update)
} }

View file

@ -1,6 +1,6 @@
# Init creates a config file # Init creates a config file
$ pda config init $ pda config init
ok generated config: /tmp/TestMain2533282848/002/config.toml ok generated config: ${ROOTDIR}/config/config.toml
# Second init fails # Second init fails
$ pda config init --> FAIL $ pda config init --> FAIL
@ -9,7 +9,7 @@ hint use '--update' to update your config, or '--new' to get a fresh copy
# Init --new overwrites # Init --new overwrites
$ pda config init --new $ pda config init --new
ok generated config: /tmp/TestMain2533282848/002/config.toml ok generated config: ${ROOTDIR}/config/config.toml
# --update preserves user changes # --update preserves user changes
$ pda config set list.always_show_all_stores false $ pda config set list.always_show_all_stores false
@ -18,7 +18,7 @@ $ pda config get list.always_show_all_stores
false false
$ pda config init --update $ pda config init --update
$ pda config get list.always_show_all_stores $ pda config get list.always_show_all_stores
ok updated config: /tmp/TestMain2533282848/002/config.toml ok updated config: ${ROOTDIR}/config/config.toml
false false
# --new and --update are mutually exclusive # --new and --update are mutually exclusive
@ -27,4 +27,4 @@ FAIL --new and --update are mutually exclusive
# Reset for other tests # Reset for other tests
$ pda config init --new $ pda config init --new
ok generated config: /tmp/TestMain2533282848/002/config.toml ok generated config: ${ROOTDIR}/config/config.toml

View file

@ -15,4 +15,4 @@ list.default_columns = key,store,value,ttl
git.auto_fetch = false git.auto_fetch = false
git.auto_commit = false git.auto_commit = false
git.auto_push = false git.auto_push = false
git.default_commit_message = sync: {{.Time}} git.default_commit_message = {{ summary }} {{ time }}

12
testdata/list-all.ct vendored
View file

@ -3,8 +3,8 @@ $ pda set lax@laa 1
$ pda set lax@lab 2 $ pda set lax@lab 2
$ pda ls --key "lax" --format tsv $ pda ls --key "lax" --format tsv
Key Store Value TTL Key Store Value TTL
lax laa 1 no expiry lax laa 1 none
lax lab 2 no expiry lax lab 2 none
$ pda ls --key "lax" --count $ pda ls --key "lax" --count
2 2
$ pda ls --key "lax" --format json $ pda ls --key "lax" --format json
@ -12,15 +12,15 @@ $ pda ls --key "lax" --format json
# Positional arg narrows to one store # Positional arg narrows to one store
$ pda ls laa --key "lax" --format tsv $ pda ls laa --key "lax" --format tsv
Key Store Value TTL Key Store Value TTL
lax laa 1 no expiry lax laa 1 none
# --store glob filter # --store glob filter
$ pda ls --store "la?" --key "lax" --format tsv $ pda ls --store "la?" --key "lax" --format tsv
Key Store Value TTL Key Store Value TTL
lax laa 1 no expiry lax laa 1 none
lax lab 2 no expiry lax lab 2 none
$ pda ls --store "laa" --key "lax" --format tsv $ pda ls --store "laa" --key "lax" --format tsv
Key Store Value TTL Key Store Value TTL
lax laa 1 no expiry lax laa 1 none
# --store cannot be combined with positional arg # --store cannot be combined with positional arg
$ pda ls --store "laa" laa --> FAIL $ pda ls --store "laa" laa --> FAIL
FAIL cannot use --store with a store argument FAIL cannot use --store with a store argument

View file

@ -3,7 +3,7 @@ $ pda config set list.always_hide_header true
$ pda set a@lchh 1 $ pda set a@lchh 1
$ pda ls lchh --format tsv $ pda ls lchh --format tsv
ok list.always_hide_header set to 'true' ok list.always_hide_header set to 'true'
a lchh 1 no expiry a lchh 1 none
# Reset # Reset
$ pda config set list.always_hide_header false $ pda config set list.always_hide_header false

View file

@ -3,5 +3,5 @@ $ pda set a@csv 1
$ pda set b@csv 2 $ pda set b@csv 2
$ pda ls csv --format csv $ pda ls csv --format csv
Key,Store,Value,TTL Key,Store,Value,TTL
a,csv,1,no expiry a,csv,1,none
b,csv,2,no expiry b,csv,2,none

View file

@ -4,5 +4,5 @@ $ pda set b@md 2
$ pda ls md --format markdown $ pda ls md --format markdown
| Key | Store | Value | TTL | | Key | Store | Value | TTL |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| a | md | 1 | no expiry | | a | md | 1 | none |
| b | md | 2 | no expiry | | b | md | 2 | none |

View file

@ -3,10 +3,10 @@ $ pda set a2@lg 2
$ pda set b1@lg 3 $ pda set b1@lg 3
$ pda ls lg --key "a*" --format tsv $ pda ls lg --key "a*" --format tsv
Key Store Value TTL Key Store Value TTL
a1 lg 1 no expiry a1 lg 1 none
a2 lg 2 no expiry a2 lg 2 none
$ pda ls lg --key "b*" --format tsv $ pda ls lg --key "b*" --format tsv
Key Store Value TTL Key Store Value TTL
b1 lg 3 no expiry b1 lg 3 none
$ pda ls lg --key "c*" --> FAIL $ pda ls lg --key "c*" --> FAIL
FAIL cannot ls '@lg': no matches for key pattern 'c*' FAIL cannot ls '@lg': no matches for key pattern 'c*'

View file

@ -3,9 +3,9 @@ $ pda set apiurl@kv https://api.example.com
$ pda set dbpass@kv s3cret $ pda set dbpass@kv s3cret
$ pda ls kv -k "db*" -v "**localhost**" --format tsv $ pda ls kv -k "db*" -v "**localhost**" --format tsv
Key Store Value TTL Key Store Value TTL
dburl kv postgres://localhost:5432 no expiry dburl kv postgres://localhost:5432 none
$ pda ls kv -k "*url*" -v "**example**" --format tsv $ pda ls kv -k "*url*" -v "**example**" --format tsv
Key Store Value TTL Key Store Value TTL
apiurl kv https://api.example.com no expiry apiurl kv https://api.example.com none
$ pda ls kv -k "db*" -v "**nomatch**" --> FAIL $ pda ls kv -k "db*" -v "**nomatch**" --> FAIL
FAIL cannot ls '@kv': no matches for key pattern 'db*' and value pattern '**nomatch**' FAIL cannot ls '@kv': no matches for key pattern 'db*' and value pattern '**nomatch**'

View file

@ -1,4 +1,4 @@
# --no-header suppresses the header row # --no-header suppresses the header row
$ pda set a@nh 1 $ pda set a@nh 1
$ pda ls nh --format tsv --no-header $ pda ls nh --format tsv --no-header
a nh 1 no expiry a nh 1 none

View file

@ -2,4 +2,4 @@
$ pda set a@nk 1 $ pda set a@nk 1
$ pda ls nk --format tsv --no-keys $ pda ls nk --format tsv --no-keys
Store Value TTL Store Value TTL
nk 1 no expiry nk 1 none

View file

@ -2,4 +2,4 @@
$ pda set a@nv 1 $ pda set a@nv 1
$ pda ls nv --format tsv --no-values $ pda ls nv --format tsv --no-values
Key Store TTL Key Store TTL
a nv no expiry a nv none

View file

@ -3,7 +3,7 @@ $ pda set a@lsalpha 1
$ pda set b@lsbeta 2 $ pda set b@lsbeta 2
$ pda ls lsalpha --format tsv $ pda ls lsalpha --format tsv
Key Store Value TTL Key Store Value TTL
a lsalpha 1 no expiry a lsalpha 1 none
$ pda ls lsbeta --format tsv $ pda ls lsbeta --format tsv
Key Store Value TTL Key Store Value TTL
b lsbeta 2 no expiry b lsbeta 2 none

View file

@ -4,12 +4,12 @@ $ pda set greeting@vt < tmpval
$ pda set number@vt 42 $ pda set number@vt 42
$ pda ls vt --value "**world**" --format tsv $ pda ls vt --value "**world**" --format tsv
Key Store Value TTL Key Store Value TTL
greeting vt hello world (..1 more chars) no expiry greeting vt hello world (..1 more chars) none
$ pda ls vt --value "**https**" --format tsv $ pda ls vt --value "**https**" --format tsv
Key Store Value TTL Key Store Value TTL
url vt https://example.com no expiry url vt https://example.com none
$ pda ls vt --value "*" --format tsv $ pda ls vt --value "*" --format tsv
Key Store Value TTL Key Store Value TTL
number vt 42 no expiry number vt 42 none
$ pda ls vt --value "**nomatch**" --> FAIL $ pda ls vt --value "**nomatch**" --> FAIL
FAIL cannot ls '@vt': no matches for value pattern '**nomatch**' FAIL cannot ls '@vt': no matches for value pattern '**nomatch**'

View file

@ -4,5 +4,5 @@ $ pda set greeting@vm < tmpval
$ pda set number@vm 42 $ pda set number@vm 42
$ pda ls vm --value "**world**" --value "42" --format tsv $ pda ls vm --value "**world**" --value "42" --format tsv
Key Store Value TTL Key Store Value TTL
greeting vm hello world (..1 more chars) no expiry greeting vm hello world (..1 more chars) none
number vm 42 no expiry number vm 42 none

View file

@ -7,4 +7,4 @@ $ pda get x@ms2
y y
$ pda ls ms2 --format tsv $ pda ls ms2 --format tsv
Key Store Value TTL Key Store Value TTL
x ms2 y no expiry x ms2 y none

View file

@ -3,8 +3,8 @@ $ pda set foo@rdd 1
$ pda set bar@rdd 2 $ pda set bar@rdd 2
$ pda ls rdd --format tsv $ pda ls rdd --format tsv
Key Store Value TTL Key Store Value TTL
bar rdd 2 no expiry bar rdd 2 none
foo rdd 1 no expiry foo rdd 1 none
$ pda rm foo@rdd --key "*@rdd" -y $ pda rm foo@rdd --key "*@rdd" -y
$ pda get bar@rdd --> FAIL $ pda get bar@rdd --> FAIL
FAIL cannot get 'bar@rdd': no such key FAIL cannot get 'bar@rdd': no such key