feat: huge overhaul of messaging into FAIL, WARN, hint, ok, prompt, and progress types

This commit is contained in:
Lewis Wynne 2026-02-11 02:11:58 +00:00
parent 6ccd801c89
commit b52a5bfdb7
30 changed files with 192 additions and 96 deletions

View file

@ -45,29 +45,28 @@ func delStore(cmd *cobra.Command, args []string) error {
store := &Store{}
dbName, err := store.parseDB(args[0], false)
if err != nil {
return fmt.Errorf("cannot delete-store '%s': %v", args[0], err)
return fmt.Errorf("cannot delete store '%s': %v", args[0], err)
}
var notFound errNotFound
path, err := store.FindStore(dbName)
if errors.As(err, &notFound) {
return fmt.Errorf("cannot delete-store '%s': %v", dbName, err)
return fmt.Errorf("cannot delete store '%s': %w", dbName, err)
}
if err != nil {
return fmt.Errorf("cannot delete-store '%s': %v", dbName, err)
return fmt.Errorf("cannot delete store '%s': %v", dbName, err)
}
interactive, err := cmd.Flags().GetBool("interactive")
if err != nil {
return fmt.Errorf("cannot delete-store '%s': %v", dbName, err)
return fmt.Errorf("cannot delete store '%s': %v", dbName, err)
}
if interactive || config.Store.AlwaysPromptDelete {
message := fmt.Sprintf("delete-store '%s': are you sure? (y/n)", args[0])
fmt.Println(message)
promptf("delete store '%s'? (y/n)", args[0])
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
return fmt.Errorf("cannot delete-store '%s': %v", dbName, err)
if err := scanln(&confirm); err != nil {
return fmt.Errorf("cannot delete store '%s': %v", dbName, err)
}
if strings.ToLower(confirm) != "y" {
return nil
@ -81,7 +80,7 @@ func delStore(cmd *cobra.Command, args []string) error {
func executeDeletion(path string) error {
if err := os.Remove(path); err != nil {
return fmt.Errorf("cannot delete-store '%s': %v", path, err)
return fmt.Errorf("cannot delete store '%s': %v", path, err)
}
return nil
}

View file

@ -66,7 +66,7 @@ func del(cmd *cobra.Command, args []string) error {
}
if len(targets) == 0 {
return fmt.Errorf("cannot remove: No such key")
return fmt.Errorf("cannot remove: no such key")
}
// Group targets by store for batch deletes.
@ -78,9 +78,8 @@ func del(cmd *cobra.Command, args []string) error {
for _, target := range targets {
if interactive || config.Key.AlwaysPromptDelete {
var confirm string
message := fmt.Sprintf("remove %q: are you sure? (y/n)", target.display)
fmt.Println(message)
if _, err := fmt.Scanln(&confirm); err != nil {
promptf("remove '%s'? (y/n)", target.display)
if err := scanln(&confirm); err != nil {
return fmt.Errorf("cannot remove '%s': %v", target.full, err)
}
if strings.ToLower(confirm) != "y" {
@ -111,7 +110,7 @@ func del(cmd *cobra.Command, args []string) error {
for _, t := range st.targets {
idx := findEntry(entries, t.key)
if idx < 0 {
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:]...)
}

View file

@ -90,7 +90,7 @@ func get(cmd *cobra.Command, args []string) error {
for i, e := range entries {
keys[i] = e.Key
}
return fmt.Errorf("cannot get '%s': %v", args[0], suggestKey(spec.Key, keys))
return fmt.Errorf("cannot get '%s': %w", args[0], suggestKey(spec.Key, keys))
}
v := entries[idx].Value
@ -128,7 +128,7 @@ func applyTemplate(tplBytes []byte, substitutions []string) ([]byte, error) {
for _, s := range substitutions {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 || parts[0] == "" {
fmt.Fprintf(os.Stderr, "invalid substitutions %q (expected KEY=VALUE)\n", s)
warnf("invalid substitution '%s', expected KEY=VALUE", s)
continue
}
key := parts[0]
@ -159,13 +159,13 @@ func applyTemplate(tplBytes []byte, substitutions []string) ([]byte, error) {
if slices.Contains(allowed, s) {
return s, nil
}
return "", fmt.Errorf("invalid value %q (allowed: %v)", s, allowed)
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("failed to convert to int: %w", err)
return 0, fmt.Errorf("cannot convert to int: %w", err)
}
return i, nil
},

View file

@ -61,36 +61,36 @@ func vcsInit(cmd *cobra.Command, args []string) error {
if clean {
gitDir := filepath.Join(repoDir, ".git")
if _, err := os.Stat(gitDir); err == nil {
fmt.Printf("remove .git from '%s'? (y/n)\n", repoDir)
promptf("remove .git from '%s'? (y/n)", repoDir)
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
return fmt.Errorf("cannot clean git dir: %w", err)
if err := scanln(&confirm); err != nil {
return fmt.Errorf("cannot init: %w", err)
}
if strings.ToLower(confirm) != "y" {
return fmt.Errorf("aborted cleaning git dir")
return fmt.Errorf("cannot init: aborted")
}
if err := os.RemoveAll(gitDir); err != nil {
return fmt.Errorf("cannot clean git dir: %w", err)
return fmt.Errorf("cannot init: %w", err)
}
}
if hasRemote {
dbs, err := store.AllStores()
if err == nil && len(dbs) > 0 {
fmt.Printf("remove all existing stores and .gitignore? (required for clone) (y/n)\n")
promptf("remove all existing stores and .gitignore, required for clone? (y/n)")
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
return fmt.Errorf("cannot clean stores: %w", err)
if err := scanln(&confirm); err != nil {
return fmt.Errorf("cannot init: %w", err)
}
if strings.ToLower(confirm) != "y" {
return fmt.Errorf("aborted cleaning stores")
return fmt.Errorf("cannot init: aborted")
}
if err := wipeAllStores(store); err != nil {
return fmt.Errorf("cannot clean stores: %w", err)
return fmt.Errorf("cannot init: %w", err)
}
gi := filepath.Join(repoDir, ".gitignore")
if err := os.Remove(gi); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("cannot remove .gitignore: %w", err)
return fmt.Errorf("cannot init: %w", err)
}
}
}
@ -98,7 +98,8 @@ func vcsInit(cmd *cobra.Command, args []string) error {
gitDir := filepath.Join(repoDir, ".git")
if _, err := os.Stat(gitDir); err == nil {
fmt.Println("vcs already initialised; use --clean to reinitialise")
warnf("vcs already initialised")
printHint("use --clean to reinitialise")
return nil
}
@ -106,11 +107,11 @@ func vcsInit(cmd *cobra.Command, args []string) error {
// git clone requires the target directory to be empty
entries, err := os.ReadDir(repoDir)
if err == nil && len(entries) > 0 {
return fmt.Errorf("stores directory is not empty; use --clean with a remote to wipe and clone")
return withHint(fmt.Errorf("cannot init: stores directory not empty"), "use --clean with a remote to wipe and clone")
}
remote := args[0]
fmt.Printf("running: git clone %s %s\n", remote, repoDir)
progressf("git clone %s %s", remote, repoDir)
if err := runGit("", "clone", remote, repoDir); err != nil {
return err
}
@ -118,7 +119,7 @@ func vcsInit(cmd *cobra.Command, args []string) error {
if err := os.MkdirAll(repoDir, 0o750); err != nil {
return err
}
fmt.Printf("running: git init\n")
progressf("git init")
if err := runGit(repoDir, "init"); err != nil {
return err
}

View file

@ -41,7 +41,7 @@ func listStores(cmd *cobra.Command, args []string) error {
store := &Store{}
dbs, err := store.AllStores()
if err != nil {
return fmt.Errorf("cannot list-stores: %v", err)
return fmt.Errorf("cannot list stores: %v", err)
}
for _, db := range dbs {
fmt.Println("@" + db)

View file

@ -47,7 +47,7 @@ func (e *formatEnum) Set(v string) error {
*e = formatEnum(v)
return nil
default:
return fmt.Errorf("must be one of \"table\", \"tsv\", \"csv\", \"html\", \"markdown\", or \"ndjson\"")
return fmt.Errorf("must be one of 'table', 'tsv', 'csv', 'html', 'markdown', or 'ndjson'")
}
}
@ -91,7 +91,7 @@ func list(cmd *cobra.Command, args []string) error {
if _, err := store.FindStore(dbName); err != nil {
var notFound errNotFound
if errors.As(err, &notFound) {
return fmt.Errorf("cannot ls '%s': No such store", args[0])
return fmt.Errorf("cannot ls '%s': %w", args[0], err)
}
return fmt.Errorf("cannot ls '%s': %v", args[0], err)
}
@ -99,7 +99,7 @@ func list(cmd *cobra.Command, args []string) error {
}
if listNoKeys && listNoValues && !listTTL {
return fmt.Errorf("cannot ls '%s': no columns selected; disable --no-keys/--no-values or pass --ttl", targetDB)
return withHint(fmt.Errorf("cannot ls '%s': no columns selected", targetDB), "disable --no-keys/--no-values or pass --ttl")
}
var columns []columnKind
@ -145,7 +145,7 @@ func list(cmd *cobra.Command, args []string) error {
}
if len(matchers) > 0 && len(filtered) == 0 {
return fmt.Errorf("cannot ls '%s': No matches for pattern %s", targetDB, formatGlobPatterns(globPatterns))
return fmt.Errorf("cannot ls '%s': no matches for pattern %s", targetDB, formatGlobPatterns(globPatterns))
}
output := cmd.OutOrStdout()

95
cmd/msg.go Normal file
View file

@ -0,0 +1,95 @@
package cmd
import (
"errors"
"fmt"
"os"
"strings"
"golang.org/x/term"
)
// hinted wraps an error with an actionable hint shown on a separate line.
type hinted struct {
err error
hint string
}
func (h hinted) Error() string { return h.err.Error() }
func (h hinted) Unwrap() error { return h.err }
func withHint(err error, hint string) error {
return hinted{err: err, hint: hint}
}
func stderrIsTerminal() bool {
return term.IsTerminal(int(os.Stderr.Fd()))
}
func stdoutIsTerminal() bool {
return term.IsTerminal(int(os.Stdout.Fd()))
}
// keyword returns a right-aligned, colored keyword (color only on TTY).
//
// FAIL red (stderr)
// hint dim (stderr)
// WARN yellow (stderr)
// ok green (stderr)
// ? cyan (stdout)
// > dim (stdout)
func keyword(code, word string, tty bool) string {
padded := fmt.Sprintf("%4s", word)
if tty {
return fmt.Sprintf("\033[%sm%s\033[0m", code, padded)
}
return padded
}
func printError(err error) {
fmt.Fprintf(os.Stderr, "%s %s\n", keyword("31", "FAIL", stderrIsTerminal()), err)
}
func printHint(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, "%s %s\n", keyword("2", "hint", stderrIsTerminal()), msg)
}
func warnf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, "%s %s\n", keyword("33", "WARN", stderrIsTerminal()), msg)
}
func okf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, "%s %s\n", keyword("32", "ok", stderrIsTerminal()), msg)
}
func promptf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stdout, "%s %s\n", keyword("36", "???", stdoutIsTerminal()), msg)
}
func progressf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stdout, "%s %s\n", keyword("2", ">", stdoutIsTerminal()), msg)
}
func scanln(dest *string) error {
fmt.Fprintf(os.Stdout, "%s ", keyword("2", "==>", stdoutIsTerminal()))
_, err := fmt.Scanln(dest)
return err
}
// printErrorWithHints prints the error and any hints found in the error chain.
func printErrorWithHints(err error) {
printError(err)
var h hinted
if errors.As(err, &h) {
printHint("%s", h.hint)
}
var nf errNotFound
if errors.As(err, &nf) && len(nf.suggestions) > 0 {
printHint("did you mean '%s'?", strings.Join(nf.suggestions, "', '"))
}
}

View file

@ -84,7 +84,7 @@ func mvImpl(cmd *cobra.Command, args []string, keepSource bool) error {
}
srcIdx := findEntry(srcEntries, fromSpec.Key)
if srcIdx < 0 {
return fmt.Errorf("cannot move '%s': No such key", fromSpec.Key)
return fmt.Errorf("cannot move '%s': no such key", fromSpec.Key)
}
srcEntry := srcEntries[srcIdx]
@ -108,8 +108,8 @@ func mvImpl(cmd *cobra.Command, args []string, keepSource bool) error {
if promptOverwrite && dstIdx >= 0 {
var confirm string
fmt.Printf("overwrite '%s'? (y/n)\n", toSpec.Display())
if _, err := fmt.Scanln(&confirm); err != nil {
promptf("overwrite '%s'? (y/n)", toSpec.Display())
if err := scanln(&confirm); err != nil {
return fmt.Errorf("cannot move '%s': %v", fromSpec.Key, err)
}
if strings.ToLower(confirm) != "y" {

View file

@ -116,7 +116,7 @@ func writeStoreFile(path string, entries []Entry) error {
je := encodeJsonEntry(e)
data, err := json.Marshal(je)
if err != nil {
return fmt.Errorf("key %q: %w", e.Key, err)
return fmt.Errorf("key '%s': %w", e.Key, err)
}
w.Write(data)
w.WriteByte('\n')
@ -142,10 +142,10 @@ func decodeJsonEntry(je jsonEntry) (Entry, error) {
var err error
value, err = base64.StdEncoding.DecodeString(je.Value)
if err != nil {
return Entry{}, fmt.Errorf("decode base64 for %q: %w", je.Key, err)
return Entry{}, fmt.Errorf("decode base64 for '%s': %w", je.Key, err)
}
default:
return Entry{}, fmt.Errorf("unsupported encoding %q for %q", je.Encoding, je.Key)
return Entry{}, fmt.Errorf("unsupported encoding '%s' for '%s'", je.Encoding, je.Key)
}
var expiresAt uint64
if je.ExpiresAt != nil {

View file

@ -104,10 +104,10 @@ 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 pattern %s", displayTarget, formatGlobPatterns(globPatterns))
}
fmt.Fprintf(cmd.ErrOrStderr(), "Restored %d entries into @%s\n", restored, dbName)
okf("restored %d entries into @%s", restored, dbName)
return autoSync()
}
@ -169,9 +169,9 @@ func restoreEntries(decoder *json.Decoder, storePath string, opts restoreOpts) (
idx := findEntry(existing, entry.Key)
if opts.promptOverwrite && idx >= 0 {
fmt.Printf("overwrite '%s'? (y/n)\n", entry.Key)
promptf("overwrite '%s'? (y/n)", entry.Key)
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
if err := scanln(&confirm); err != nil {
return 0, fmt.Errorf("entry %d: %v", entryNo, err)
}
if strings.ToLower(confirm) != "y" {

View file

@ -34,15 +34,17 @@ var rootCmd = &cobra.Command{
Use: "pda",
Short: "A key-value store tool",
Long: asciiArt,
SilenceErrors: true, // we print errors ourselves
}
func Execute() {
if configErr != nil {
fmt.Fprintln(os.Stderr, "failed to load config:", configErr)
printError(fmt.Errorf("cannot load config: %v", configErr))
os.Exit(1)
}
err := rootCmd.Execute()
if err != nil {
printErrorWithHints(err)
os.Exit(1)
}
}

View file

@ -93,9 +93,9 @@ func set(cmd *cobra.Command, args []string) error {
idx := findEntry(entries, spec.Key)
if promptOverwrite && idx >= 0 {
fmt.Printf("overwrite '%s'? (y/n)\n", spec.Display())
promptf("overwrite '%s'? (y/n)", spec.Display())
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
if err := scanln(&confirm); err != nil {
return fmt.Errorf("cannot set '%s': %v", args[0], err)
}
if strings.ToLower(confirm) != "y" {

View file

@ -37,14 +37,12 @@ import (
)
type errNotFound struct {
what string // "key" or "store"
suggestions []string
}
func (err errNotFound) Error() string {
if len(err.suggestions) == 0 {
return "No such key"
}
return fmt.Sprintf("No such key. Did you mean '%s'?", strings.Join(err.suggestions, ", "))
return fmt.Sprintf("no such %s", err.what)
}
type Store struct{}
@ -129,7 +127,7 @@ func (s *Store) FindStore(k string) (string, error) {
if err != nil {
return "", err
}
return "", errNotFound{suggestions}
return "", errNotFound{what: "store", suggestions: suggestions}
}
if statErr != nil {
return "", statErr
@ -205,7 +203,7 @@ func suggestKey(target string, keys []string) error {
suggestions = append(suggestions, k)
}
}
return errNotFound{suggestions}
return errNotFound{what: "key", suggestions: suggestions}
}
func ensureSubpath(base, target string) error {

View file

@ -67,12 +67,12 @@ func sync(manual bool) error {
return err
}
} else if manual {
fmt.Println("no changes to commit")
okf("no changes to commit")
}
if remoteInfo.Ref == "" {
if manual {
fmt.Println("no remote configured; skipping push")
warnf("no remote configured, skipping push")
}
return nil
}
@ -105,7 +105,7 @@ func sync(manual bool) error {
return pushRemote(repoDir, remoteInfo)
}
if manual {
fmt.Println("nothing to push")
okf("nothing to push")
}
}

View file

@ -17,7 +17,7 @@ func ensureVCSInitialized() (string, error) {
}
if _, err := os.Stat(filepath.Join(repoDir, ".git")); err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("vcs repository not initialised; run 'pda init' first")
return "", withHint(fmt.Errorf("vcs not initialised"), "run 'pda init' first")
}
return "", err
}
@ -43,7 +43,7 @@ func writeGitignore(repoDir string) error {
}
return runGit(repoDir, "commit", "-m", "generated gitignore")
}
fmt.Println("Existing .gitignore found.")
okf("existing .gitignore found")
return nil
}
@ -195,7 +195,7 @@ func wipeAllStores(store *Store) error {
return err
}
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove store '%s': %w", db, err)
return fmt.Errorf("cannot remove store '%s': %w", db, err)
}
}
return nil

View file

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

View file

@ -1,2 +1,2 @@
$ pda get key@foo/bar --> FAIL
Error: cannot get 'key@foo/bar': bad store format, use STORE or @STORE
FAIL cannot get 'key@foo/bar': bad store format, use STORE or @STORE

View file

@ -1,2 +1,2 @@
$ pda get foobar --> FAIL
Error: cannot get 'foobar': No such key
FAIL cannot get 'foobar': no such key

View file

@ -5,10 +5,10 @@ $ pda get foobar --include-binary --run --secret --> FAIL
$ pda get foobar --run --> FAIL
$ pda get foobar --run --secret --> FAIL
$ pda get foobar --secret --> FAIL
Error: cannot get 'foobar': No such key
Error: cannot get 'foobar': No such key
Error: cannot get 'foobar': No such key
Error: unknown flag: --secret
Error: cannot get 'foobar': No such key
Error: unknown flag: --secret
Error: unknown flag: --secret
FAIL cannot get 'foobar': no such key
FAIL cannot get 'foobar': no such key
FAIL cannot get 'foobar': no such key
FAIL unknown flag: --secret
FAIL cannot get 'foobar': no such key
FAIL unknown flag: --secret
FAIL unknown flag: --secret

View file

@ -1,3 +1,2 @@
$ pda invalidcmd --> FAIL
Error: unknown command "invalidcmd" for "pda"
Run 'pda --help' for usage.
FAIL unknown command "invalidcmd" for "pda"

View file

@ -1,2 +1,2 @@
$ pda ls foo/bar --> FAIL
Error: cannot ls 'foo/bar': cannot parse store: bad store format, use STORE or @STORE
FAIL cannot ls 'foo/bar': cannot parse store: bad store format, use STORE or @STORE

View file

@ -7,4 +7,4 @@ a2 2
$ pda ls lg --glob b* --format tsv
b1 3
$ pda ls lg --glob c* --> FAIL
Error: cannot ls '@lg': No matches for pattern 'c*'
FAIL cannot ls '@lg': no matches for pattern 'c*'

View file

@ -1,2 +1,2 @@
$ pda rms foo/bar --> FAIL
Error: cannot delete-store 'foo/bar': cannot parse store: bad store format, use STORE or @STORE
FAIL cannot delete store 'foo/bar': cannot parse store: bad store format, use STORE or @STORE

View file

@ -10,6 +10,6 @@ $ pda ls
foo 1
$ pda rm foo --glob "*"
$ pda get bar --> FAIL
Error: cannot get 'bar': No such key
FAIL cannot get 'bar': no such key
$ pda get foo --> FAIL
Error: cannot get 'foo': No such key
FAIL cannot get 'foo': no such key

View file

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

View file

@ -3,8 +3,9 @@ $ pda set a2 2
$ pda set b1 3
$ pda rm --glob a*
$ pda get a1 --> FAIL
Error: cannot get 'a1': No such key. Did you mean 'b1'?
FAIL cannot get 'a1': no such key
hint did you mean 'b1'?
$ pda get a2 --> FAIL
Error: cannot get 'a2': No such key
FAIL cannot get 'a2': no such key
$ pda get b1
3

View file

@ -2,6 +2,7 @@ $ pda set a 1
$ pda set b 2
$ pda rm a b
$ pda get a --> FAIL
Error: cannot get 'a': No such key
FAIL cannot get 'a': no such key
$ pda get b --> FAIL
Error: cannot get 'b': No such key. Did you mean 'b1'?
FAIL cannot get 'b': no such key
hint did you mean 'b1'?

View file

@ -2,8 +2,8 @@ $ pda set existing keep-me
$ pda set other also-keep
$ fecho dumpfile {"key":"new","value":"hello","encoding":"text"}
$ pda restore --drop --file dumpfile
Restored 1 entries into @default
ok restored 1 entries into @default
$ pda get new
hello
$ pda get existing --> FAIL
Error: cannot get 'existing': No such key
FAIL cannot get 'existing': no such key

View file

@ -4,12 +4,13 @@ $ 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
Restored 2 entries into @default
ok restored 2 entries into @default
$ pda get a1
1
$ pda get a2
2
$ pda get b1 --> FAIL
Error: cannot get 'b1': No such key. Did you mean 'a1'?
FAIL cannot get 'b1': no such key
hint did you mean 'a1'?
$ pda restore --glob c* --file dumpfile --> FAIL
Error: cannot restore '@default': No matches for pattern 'c*'
FAIL cannot restore '@default': no matches for pattern 'c*'

View file

@ -1,2 +1,2 @@
$ pda set a b --ttl 3343r --> FAIL
Error: invalid argument "3343r" for "-t, --ttl" flag: time: unknown unit "r" in duration "3343r"
FAIL invalid argument "3343r" for "-t, --ttl" flag: time: unknown unit "r" in duration "3343r"