From 2ca32769d5032beef2c2a129b8f1b37e9975df8b Mon Sep 17 00:00:00 2001 From: lew Date: Thu, 12 Feb 2026 20:00:57 +0000 Subject: [PATCH] feat(commit): text templating in commit messages --- README.md | 16 +++++- cmd/commit_message.go | 48 ++++++++++++++++ cmd/commit_message_test.go | 53 ++++++++++++++++++ cmd/config.go | 2 +- cmd/del-db.go | 2 +- cmd/del.go | 4 +- cmd/get.go | 50 +---------------- cmd/mv-db.go | 5 +- cmd/mv.go | 5 +- cmd/restore.go | 5 +- cmd/set.go | 2 +- cmd/sync.go | 13 ++--- cmd/template.go | 85 +++++++++++++++++++++++++++++ main_test.go | 38 ++++++++----- testdata/config-init.ct | 8 +-- testdata/config-list.ct | 2 +- testdata/list-all.ct | 12 ++-- testdata/list-config-hide-header.ct | 2 +- testdata/list-format-csv.ct | 4 +- testdata/list-format-markdown.ct | 4 +- testdata/list-key-filter.ct | 6 +- testdata/list-key-value-filter.ct | 4 +- testdata/list-no-header.ct | 2 +- testdata/list-no-keys.ct | 2 +- testdata/list-no-values.ct | 2 +- testdata/list-stores.ct | 4 +- testdata/list-value-filter.ct | 6 +- testdata/list-value-multi-filter.ct | 4 +- testdata/multistore.ct | 2 +- testdata/remove-dedupe.ct | 4 +- 30 files changed, 281 insertions(+), 115 deletions(-) create mode 100644 cmd/commit_message.go create mode 100644 cmd/commit_message_test.go create mode 100644 cmd/template.go diff --git a/README.md b/README.md index 82acb99..550b72d 100644 --- a/README.md +++ b/README.md @@ -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. -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. @@ -438,6 +438,15 @@ pda get my_name

+`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 +``` + +

+ `enum` restricts acceptable values. ```bash pda set level "Log level: {{ enum .LEVEL "info" "warn" "error" }}" @@ -854,8 +863,9 @@ auto_fetch = false auto_commit = false # auto push after committing auto_push = false -# commit message template ({{.Time}} is replaced with RFC3339 timestamp) -default_commit_message = "sync: {{.Time}}" +# commit message if none manually specified +# supports templates, see: #templates section +default_commit_message = "{{ summary }} {{ time }}" ```

diff --git a/cmd/commit_message.go b/cmd/commit_message.go new file mode 100644 index 0000000..4b3c0f5 --- /dev/null +++ b/cmd/commit_message.go @@ -0,0 +1,48 @@ +/* +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 ( + "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() +} diff --git a/cmd/commit_message_test.go b/cmd/commit_message_test.go new file mode 100644 index 0000000..2c75d14 --- /dev/null +++ b/cmd/commit_message_test.go @@ -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) + } + }) +} diff --git a/cmd/config.go b/cmd/config.go index 6a42a97..da42dcf 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -117,7 +117,7 @@ func defaultConfig() Config { AutoFetch: false, AutoCommit: false, AutoPush: false, - DefaultCommitMessage: "sync: {{.Time}}", + DefaultCommitMessage: "{{ summary }} {{ time }}", }, } } diff --git a/cmd/del-db.go b/cmd/del-db.go index b1ccd2b..31fe227 100644 --- a/cmd/del-db.go +++ b/cmd/del-db.go @@ -79,7 +79,7 @@ func delStore(cmd *cobra.Command, args []string) error { if err := executeDeletion(path); err != nil { return err } - return autoSync() + return autoSync(fmt.Sprintf("removed @%s", dbName)) } func executeDeletion(path string) error { diff --git a/cmd/del.go b/cmd/del.go index e91d392..01f35a6 100644 --- a/cmd/del.go +++ b/cmd/del.go @@ -107,6 +107,7 @@ func del(cmd *cobra.Command, args []string) error { return nil } + var removedNames []string for _, dbName := range storeOrder { st := byStore[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) } entries = append(entries[:idx], entries[idx+1:]...) + removedNames = append(removedNames, t.display) } if err := writeStoreFile(p, entries, nil); err != nil { return err } } - return autoSync() + return autoSync("removed " + strings.Join(removedNames, ", ")) } func init() { diff --git a/cmd/get.go b/cmd/get.go index 116404f..5a3a85d 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -27,8 +27,6 @@ import ( "fmt" "os" "os/exec" - "slices" - "strconv" "strings" "text/template" @@ -150,57 +148,11 @@ func applyTemplate(tplBytes []byte, substitutions []string) ([]byte, error) { val := parts[1] 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"). Delims("{{", "}}"). // Render missing map keys as zero values so the default helper can decide on fallbacks. Option("missingkey=zero"). - Funcs(funcMap). + Funcs(templateFuncMap()). Parse(string(tplBytes)) if err != nil { return nil, err diff --git a/cmd/mv-db.go b/cmd/mv-db.go index 22c3cdc..f3a360e 100644 --- a/cmd/mv-db.go +++ b/cmd/mv-db.go @@ -102,6 +102,7 @@ func mvStore(cmd *cobra.Command, args []string) error { } copy, _ := cmd.Flags().GetBool("copy") + var summary string if copy { data, err := os.ReadFile(fromPath) 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) } okf("copied @%s to @%s", fromName, toName) + summary = fmt.Sprintf("copied @%s to @%s", fromName, toName) } else { if err := os.Rename(fromPath, toPath); err != nil { return fmt.Errorf("cannot rename store '%s': %v", fromName, err) } okf("renamed @%s to @%s", fromName, toName) + summary = fmt.Sprintf("moved @%s to @%s", fromName, toName) } - return autoSync() + return autoSync(summary) } func init() { diff --git a/cmd/mv.go b/cmd/mv.go index 1d549db..ecc5e92 100644 --- a/cmd/mv.go +++ b/cmd/mv.go @@ -182,12 +182,15 @@ func mvImpl(cmd *cobra.Command, args []string, keepSource bool) error { } } + var summary string if keepSource { okf("copied %s to %s", fromSpec.Display(), toSpec.Display()) + summary = "copied " + fromSpec.Display() + " to " + toSpec.Display() } else { okf("renamed %s to %s", fromSpec.Display(), toSpec.Display()) + summary = "moved " + fromSpec.Display() + " to " + toSpec.Display() } - return autoSync() + return autoSync(summary) } func init() { diff --git a/cmd/restore.go b/cmd/restore.go index 90d7e3a..ba4a577 100644 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -127,6 +127,7 @@ func restore(cmd *cobra.Command, args []string) error { // When a specific store is given, all entries go there (original behaviour). // Otherwise, route entries to their original store via the "store" field. + var summary string if explicitStore { p, err := store.storePath(targetDB) if err != nil { @@ -140,6 +141,7 @@ func restore(cmd *cobra.Command, args []string) error { return err } okf("restored %d entries into @%s", restored, targetDB) + summary = fmt.Sprintf("imported %d entries into @%s", restored, targetDB) } else { restored, err := restoreEntries(decoder, nil, targetDB, opts) if err != nil { @@ -149,9 +151,10 @@ func restore(cmd *cobra.Command, args []string) error { return err } 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 { diff --git a/cmd/set.go b/cmd/set.go index 4f8c88d..e39586f 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -176,7 +176,7 @@ func set(cmd *cobra.Command, args []string) error { return fmt.Errorf("cannot set '%s': %v", args[0], err) } - return autoSync() + return autoSync("set " + spec.Display()) } func init() { diff --git a/cmd/sync.go b/cmd/sync.go index 0353470..937e2be 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -23,9 +23,6 @@ THE SOFTWARE. package cmd import ( - "strings" - "time" - "github.com/spf13/cobra" ) @@ -35,7 +32,7 @@ var syncCmd = &cobra.Command{ SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { msg, _ := cmd.Flags().GetString("message") - return sync(true, msg) + return sync(true, msg, "sync") }, } @@ -44,7 +41,7 @@ func init() { rootCmd.AddCommand(syncCmd) } -func sync(manual bool, customMsg string) error { +func sync(manual bool, customMsg string, summary string) error { repoDir, err := ensureVCSInitialized() if err != nil { return err @@ -66,7 +63,7 @@ func sync(manual bool, customMsg string) error { if changed { msg := customMsg if msg == "" { - msg = strings.ReplaceAll(config.Git.DefaultCommitMessage, "{{.Time}}", time.Now().UTC().Format(time.RFC3339)) + msg = renderCommitMessage(config.Git.DefaultCommitMessage, summary) if manual { printHint("use -m to set a custom commit message") } @@ -123,12 +120,12 @@ func sync(manual bool, customMsg string) error { return nil } -func autoSync() error { +func autoSync(summary string) error { if !config.Git.AutoCommit { return nil } if _, err := ensureVCSInitialized(); err != nil { return nil } - return sync(false, "") + return sync(false, "", summary) } diff --git a/cmd/template.go b/cmd/template.go new file mode 100644 index 0000000..0bd1209 --- /dev/null +++ b/cmd/template.go @@ -0,0 +1,85 @@ +/* +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" + "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) }, + } +} diff --git a/main_test.go b/main_test.go index 72c9d4e..0eee94b 100644 --- a/main_test.go +++ b/main_test.go @@ -36,20 +36,6 @@ import ( var update = flag.Bool("update", false, "update test files with results") 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") if err != nil { t.Fatalf("read testdata: %v", err) @@ -59,5 +45,29 @@ func TestMain(t *testing.T) { t.Fatal(err) } 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) } diff --git a/testdata/config-init.ct b/testdata/config-init.ct index 5d5eb85..ce99235 100644 --- a/testdata/config-init.ct +++ b/testdata/config-init.ct @@ -1,6 +1,6 @@ # Init creates a config file $ pda config init - ok generated config: /tmp/TestMain2533282848/002/config.toml + ok generated config: ${ROOTDIR}/config/config.toml # Second init fails $ 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 $ pda config init --new - ok generated config: /tmp/TestMain2533282848/002/config.toml + ok generated config: ${ROOTDIR}/config/config.toml # --update preserves user changes $ pda config set list.always_show_all_stores false @@ -18,7 +18,7 @@ $ pda config get list.always_show_all_stores false $ pda config init --update $ pda config get list.always_show_all_stores - ok updated config: /tmp/TestMain2533282848/002/config.toml + ok updated config: ${ROOTDIR}/config/config.toml false # --new and --update are mutually exclusive @@ -27,4 +27,4 @@ FAIL --new and --update are mutually exclusive # Reset for other tests $ pda config init --new - ok generated config: /tmp/TestMain2533282848/002/config.toml + ok generated config: ${ROOTDIR}/config/config.toml diff --git a/testdata/config-list.ct b/testdata/config-list.ct index 6a909d7..1bce175 100644 --- a/testdata/config-list.ct +++ b/testdata/config-list.ct @@ -15,4 +15,4 @@ list.default_columns = key,store,value,ttl git.auto_fetch = false git.auto_commit = false git.auto_push = false -git.default_commit_message = sync: {{.Time}} +git.default_commit_message = {{ summary }} {{ time }} diff --git a/testdata/list-all.ct b/testdata/list-all.ct index 1694b4f..d6a4023 100644 --- a/testdata/list-all.ct +++ b/testdata/list-all.ct @@ -3,8 +3,8 @@ $ pda set lax@laa 1 $ pda set lax@lab 2 $ pda ls --key "lax" --format tsv Key Store Value TTL -lax laa 1 no expiry -lax lab 2 no expiry +lax laa 1 none +lax lab 2 none $ pda ls --key "lax" --count 2 $ pda ls --key "lax" --format json @@ -12,15 +12,15 @@ $ pda ls --key "lax" --format json # Positional arg narrows to one store $ pda ls laa --key "lax" --format tsv Key Store Value TTL -lax laa 1 no expiry +lax laa 1 none # --store glob filter $ pda ls --store "la?" --key "lax" --format tsv Key Store Value TTL -lax laa 1 no expiry -lax lab 2 no expiry +lax laa 1 none +lax lab 2 none $ pda ls --store "laa" --key "lax" --format tsv Key Store Value TTL -lax laa 1 no expiry +lax laa 1 none # --store cannot be combined with positional arg $ pda ls --store "laa" laa --> FAIL FAIL cannot use --store with a store argument diff --git a/testdata/list-config-hide-header.ct b/testdata/list-config-hide-header.ct index 6f8b61e..6a2c4f9 100644 --- a/testdata/list-config-hide-header.ct +++ b/testdata/list-config-hide-header.ct @@ -3,7 +3,7 @@ $ pda config set list.always_hide_header true $ pda set a@lchh 1 $ pda ls lchh --format tsv ok list.always_hide_header set to 'true' -a lchh 1 no expiry +a lchh 1 none # Reset $ pda config set list.always_hide_header false diff --git a/testdata/list-format-csv.ct b/testdata/list-format-csv.ct index 6d5fd73..e0cff1f 100644 --- a/testdata/list-format-csv.ct +++ b/testdata/list-format-csv.ct @@ -3,5 +3,5 @@ $ pda set a@csv 1 $ pda set b@csv 2 $ pda ls csv --format csv Key,Store,Value,TTL -a,csv,1,no expiry -b,csv,2,no expiry +a,csv,1,none +b,csv,2,none diff --git a/testdata/list-format-markdown.ct b/testdata/list-format-markdown.ct index c27f045..67da1f2 100644 --- a/testdata/list-format-markdown.ct +++ b/testdata/list-format-markdown.ct @@ -4,5 +4,5 @@ $ pda set b@md 2 $ pda ls md --format markdown | Key | Store | Value | TTL | | --- | --- | --- | --- | -| a | md | 1 | no expiry | -| b | md | 2 | no expiry | +| a | md | 1 | none | +| b | md | 2 | none | diff --git a/testdata/list-key-filter.ct b/testdata/list-key-filter.ct index d10e229..57e931e 100644 --- a/testdata/list-key-filter.ct +++ b/testdata/list-key-filter.ct @@ -3,10 +3,10 @@ $ pda set a2@lg 2 $ pda set b1@lg 3 $ pda ls lg --key "a*" --format tsv Key Store Value TTL -a1 lg 1 no expiry -a2 lg 2 no expiry +a1 lg 1 none +a2 lg 2 none $ pda ls lg --key "b*" --format tsv Key Store Value TTL -b1 lg 3 no expiry +b1 lg 3 none $ pda ls lg --key "c*" --> FAIL FAIL cannot ls '@lg': no matches for key pattern 'c*' diff --git a/testdata/list-key-value-filter.ct b/testdata/list-key-value-filter.ct index 5fde71a..1a9d094 100644 --- a/testdata/list-key-value-filter.ct +++ b/testdata/list-key-value-filter.ct @@ -3,9 +3,9 @@ $ pda set apiurl@kv https://api.example.com $ pda set dbpass@kv s3cret $ pda ls kv -k "db*" -v "**localhost**" --format tsv 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 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 FAIL cannot ls '@kv': no matches for key pattern 'db*' and value pattern '**nomatch**' diff --git a/testdata/list-no-header.ct b/testdata/list-no-header.ct index 6b80e52..92ca62b 100644 --- a/testdata/list-no-header.ct +++ b/testdata/list-no-header.ct @@ -1,4 +1,4 @@ # --no-header suppresses the header row $ pda set a@nh 1 $ pda ls nh --format tsv --no-header -a nh 1 no expiry +a nh 1 none diff --git a/testdata/list-no-keys.ct b/testdata/list-no-keys.ct index c1ffe62..f444f6c 100644 --- a/testdata/list-no-keys.ct +++ b/testdata/list-no-keys.ct @@ -2,4 +2,4 @@ $ pda set a@nk 1 $ pda ls nk --format tsv --no-keys Store Value TTL -nk 1 no expiry +nk 1 none diff --git a/testdata/list-no-values.ct b/testdata/list-no-values.ct index b76c081..388f330 100644 --- a/testdata/list-no-values.ct +++ b/testdata/list-no-values.ct @@ -2,4 +2,4 @@ $ pda set a@nv 1 $ pda ls nv --format tsv --no-values Key Store TTL -a nv no expiry +a nv none diff --git a/testdata/list-stores.ct b/testdata/list-stores.ct index e5c7412..744109a 100644 --- a/testdata/list-stores.ct +++ b/testdata/list-stores.ct @@ -3,7 +3,7 @@ $ pda set a@lsalpha 1 $ pda set b@lsbeta 2 $ pda ls lsalpha --format tsv Key Store Value TTL -a lsalpha 1 no expiry +a lsalpha 1 none $ pda ls lsbeta --format tsv Key Store Value TTL -b lsbeta 2 no expiry +b lsbeta 2 none diff --git a/testdata/list-value-filter.ct b/testdata/list-value-filter.ct index 0e29856..ecb31b8 100644 --- a/testdata/list-value-filter.ct +++ b/testdata/list-value-filter.ct @@ -4,12 +4,12 @@ $ pda set greeting@vt < tmpval $ pda set number@vt 42 $ pda ls vt --value "**world**" --format tsv 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 Key Store Value TTL -url vt https://example.com no expiry +url vt https://example.com none $ pda ls vt --value "*" --format tsv Key Store Value TTL -number vt 42 no expiry +number vt 42 none $ pda ls vt --value "**nomatch**" --> FAIL FAIL cannot ls '@vt': no matches for value pattern '**nomatch**' diff --git a/testdata/list-value-multi-filter.ct b/testdata/list-value-multi-filter.ct index df1fe0b..4725bc7 100644 --- a/testdata/list-value-multi-filter.ct +++ b/testdata/list-value-multi-filter.ct @@ -4,5 +4,5 @@ $ pda set greeting@vm < tmpval $ pda set number@vm 42 $ pda ls vm --value "**world**" --value "42" --format tsv Key Store Value TTL -greeting vm hello world (..1 more chars) no expiry -number vm 42 no expiry +greeting vm hello world (..1 more chars) none +number vm 42 none diff --git a/testdata/multistore.ct b/testdata/multistore.ct index cde1df3..79e7f63 100644 --- a/testdata/multistore.ct +++ b/testdata/multistore.ct @@ -7,4 +7,4 @@ $ pda get x@ms2 y $ pda ls ms2 --format tsv Key Store Value TTL -x ms2 y no expiry +x ms2 y none diff --git a/testdata/remove-dedupe.ct b/testdata/remove-dedupe.ct index 6f8fe32..c30a1e5 100644 --- a/testdata/remove-dedupe.ct +++ b/testdata/remove-dedupe.ct @@ -3,8 +3,8 @@ $ pda set foo@rdd 1 $ pda set bar@rdd 2 $ pda ls rdd --format tsv Key Store Value TTL -bar rdd 2 no expiry -foo rdd 1 no expiry +bar rdd 2 none +foo rdd 1 none $ pda rm foo@rdd --key "*@rdd" -y $ pda get bar@rdd --> FAIL FAIL cannot get 'bar@rdd': no such key