personal digital assistant! A key/value store tool for the command line with rich template support, written in Go.
Find a file
2025-12-18 01:53:09 +00:00
.github/workflows bump Go version in CI 2025-11-20 18:35:42 +00:00
cmd feat(cmd): improves error messaging for globs 2025-12-18 01:53:09 +00:00
docs feat(docs): remove cmds.png 2025-11-20 19:15:45 +00:00
testdata feat(cmd): improves error messaging for globs 2025-12-18 01:53:09 +00:00
vhs feat(VHS): for demos 2025-11-20 19:51:46 +00:00
.gitignore build(gitignore): adds .gocache and .build 2025-11-06 22:39:31 +00:00
go.mod feat(del): adds glob support for deletion 2025-12-17 18:37:10 +00:00
go.sum feat(del): adds glob support for deletion 2025-12-17 18:37:10 +00:00
LICENSE feat(init): initial commit 2025-11-06 15:00:18 +00:00
main.go feat(init): initial commit 2025-11-06 15:00:18 +00:00
main_test.go fix(cmdtest): create binary in /tmp 2025-11-20 15:41:31 +00:00
README.md docs(README): dump/restore glob 2025-12-18 00:50:12 +00:00

pda

Contents

Overview

Available Commands:
  get         # Get a value.
  set         # Set a value.
  cp          # Copy a value.
  mv          # Move a value.
  del         # Delete a value.
  del-db      # Delete a whole database.
  list-dbs    # List all databases.
  dump        # Export a database as NDJSON.
  restore     # Imports NDJSON into a database.
  completion  # Generate autocompletions for a specified shell.
  help        # Additional help for any command.
  version     # Current version.

Installation

# Get the latest release from the AUR
yay -S pda

# Or use pda-git for the latest commit
yay -S pda-git

# Go install
go install github.com/llywelwyn/pda@latest

# Or
git clone https://github.com/llywelwyn/pda
cd pda
go install

Get Started

pda set to save a key.

# From arguments
pda set name "Alice"

# From stdin
echo "Alice" | pda set name
cat dogs.txt | pda set dogs
pda set kitty < cat.png

pda get to retrieve it.

pda get name
# Alice

# Or run it directly.
pda get name --run

pda mv to move it.

pda mv name name2
# renamed name to name2

pda cp to make a copy.

pda cp name name2

# 'mv --copy' and 'cp' are aliases. Either one works.
pda mv name name2 --copy

pda del to delete one or more keys.

pda del kitty
# remove  "kitty": are you sure? [y/n]
# y

# Or skip the prompt.
pda del kitty --force

# Remove multiple keys, within the same or different stores.
pda del kitty dog@animals
# remove "kitty", "dog@animals": are you sure? [y/n]
# y

# Mix exact keys with globs.
pda set cog "cogs"
pda set dog "doggy"
pda set kitty "cat"
pda del kitty --glob ?og
# remove "kitty", "cog", "dog": are you sure? [y/n]
# y
# Default glob separators: "/-_.@: " (space included). Override with --glob-sep.

pda ls to see what you've got stored.

pda ls
# name    Alice
# dogs    four legged mammals

# Or as CSV.
pda ls --format csv
# name,Alice
# dogs,four legged mammals

# Or TSV, or Markdown, or HTML.

pda dump to export everything as NDJSON.

pda dump > my_backup

# Dump only matching keys.
pda dump --glob a*

pda restore to import it all back.

# Restore with an argument.
pda restore -f my_backup
# Restored 2 entries into @default.

# Or from stdin.
pda restore < my_backup
# Restored 2 entries into @default.

# Restore only matching keys.
pda restore --glob a* -f my_backup

You can have as many stores as you want.

# Save to a spceific store.
pda set alice@birthdays 11/11/1998

# See which stores have contents.
pda list-dbs
# @default
# @birthdays

# Check out a specific store.
pda ls @birthdays
# alice    11/11/1998
# bob      05/12/1980

# Dump it.
pda dump birthdays > friends_birthdays

# Restore it.
pda restore birthdays < friends_birthdays

# Delete it.
pda del-db birthdays --force

Templates

Values support effectively all of Go's text/template syntax. Templates are evaluated on pda get.

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, enums, required values, lists, among others.

Below is more detail on the extra functions added by this tool.

{{ .BASIC }} substitution

pda set greeting "Hello, {{ .NAME }}"
pda get greeting NAME="Alice"
# Hello, Alice

default sets a default value.

pda set greeting "Hello, {{ default "World" .NAME }}"
pda get greeting
# Hello, World
pda get greeting NAME="Bob"
# Hello, Bob

require errors if missing.

pda set file "{{ require .FILE }}"
pda get file
# Error: required value missing or empty

env reads from environment variables.

pda set my_name "{{ env "USER" }}"
pda get my_name
# llywelwyn

enum restricts acceptable values.

pda set level "Log level: {{ enum .LEVEL "info" "warn" "error" }}"
pda get level LEVEL=info
# Log level: info
pda get level LEVEL=debug
# Error: invalid value "debug" (allowed: [info warn error])

int to parse as an integer.

pda set number "{{ int .N }}"
pda get number N=3
# 3

# Use it in a loop.
pda set meows "{{ range int .COUNT }}meow! {{ end }}"
pda get meows COUNT=4
# meow! meow! meow! meow!

list to parse CSV as a list.

pda set names "{{ range list .NAMES }}Hi {{.}}. {{ end }}"
pda get names NAMES=Bob,Alice
# Hi Bob. Hi Alice.

pass no-template to output literally without templating.

pda set hello "{{ if .MORNING }}Good morning.{{ end }}"
pda get hello MORNING=1
# Good morning.
pda get hello --no-template
# {{ if .MORNING }}Good morning.{{ end }}

Globs

Globs can be used in a few commands where their use makes sense. gobwas/glob is used for matching.

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 database.

* wildcards a word or series of characters.

pda ls --no-values
# cat
# dog
# cog
# mouse hotdog
# mouse house
# foo.bar.baz

pda ls --glob "*"
# cat
# dog
# cog

pda ls --glob "* *"
# mouse hotdog
# mouse house

pda ls --glob "foo.*.baz"
# foo.bar.baz

** super-wildcards ignore word boundaries.

pda ls --glob "foo**"
# foo.bar.baz

pda ls --glob "**g"
# dog
# cog
# mouse hotdog

? wildcards a single letter.

pda ls --glob ?og
# dog
# cog
# frog --> fail
# dogs --> fail

[abc] must match one of the characters in the brackets.

pda ls --glob [dc]og
# dog
# cog
# bog --> fail

# Can be negated with '!'
pda ls --glob [!dc]og
# dog --> fail
# cog --> fail
# bog

[a-c] must fall within the range given in the brackets

pda ls --glob [a-g]ag
# bag
# gag
# wag --> fail

# Can be negated with '!'
pda ls --glob [!a-g]ag
# bag --> fail
# gag --> fail
# wag

pda ls --glob 19[90-99]
# 1991
# 1992
# 2001 --> fail
# 1988 --> fail

Globs can be arbitrarily complex, and can be combined with strict matches.

pda ls --no-keys
# cat
# mouse trap
# dog house
# cat flap
# cogwheel

pda rm cat --glob "{mouse,[cd]og}**"
# remove: 'cat', 'mouse trap', 'dog house', 'cogwheel': are you sure? [y/n]

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

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

Secrets

Mark sensitive values with secret to stop accidents.

# Store a secret
pda set password "hunter2" --secret

secret is used for revealing secrets too.

pda get password
# Error: "password" is marked secret; re-run with --secret to display it
pda get password --secret
# hunter2

list censors secrets.

pda ls
# password    ************

pda ls --secret
# password    hunter2

dump excludes secrets unless allowed.

pda dump
# nil

pda dump --secret
# {"key":"password","value":"hunter2","encoding":"text"}

TTL

ttl sets an expiration time. Expired keys get marked for garbage collection and will be deleted on the next-run of the store. They wont be accessible.

# Expire after 1 hour
pda set session "123" --ttl 1h

# After 52 minutes and 10 seconds
pda set session2 "xyz" --ttl 54m10s

list --ttl shows expiration date in list output.

pda ls --ttl
# session    123    2025-11-21T15:30:00Z (in 59m30s)
# session2   xyz    2025-11-21T15:21:40Z (in 51m40s)

dump and restore persists the expiry date. Expirations will continue ticking down regardless of if they're actively in a store or not - the expiry is just a timestamp, not a timer.

Binary

Save binary data.

pda set logo < logo.png```

And get it like normal.

pda get logo > output.png

list and get will omit binary data whenever it's a human reading it. If it's being piped somewhere or ran outside of a TTY, it'll output the whole data.

include-binary to show the full binary data regardless.

pda get logo
# (omitted binary data)

pda get logo --include-binary
# 89504E470D0A1A0A0000000D4948445200000001000000010802000000

dump encodes binary data as base64.

pda dump
# {"key":"logo","value":"89504E470D0A1A0A0000000D4948445200000001000000010802000000","encoding":"base64"}

Environment

Data is stored in your user data directory under pda/stores/.

Usually:

  • linux: ~/.local/share/pda/stores/
  • macOS: ~/Library/Application Support/pda/stores/
  • windows: %LOCALAPPDATA%/pda/stores/

PDA_DATA_DIR overrides the default storage location.

PDATA_DATA_DIR=/tmp/stores pda set key value

pda get --run uses SHELL for command execution.

# SHELL is usually your current shell.
pda get script --run

# An empty SHELL falls back to using 'sh'.
export SHELL=""
pda get script --run