`pda!` is a command-line key-value store tool with:
- [templates](#templates) supporting arbitrary shell execution, conditionals, loops, more,
- [encryption](#encryption) at rest using [age](https://github.com/FiloSottile/age),
- Git-backed [version control](#git) with automatic syncing,
- [search and filtering](#filtering) by key, value, or store,
- plaintext exports in 7 different formats,
- support for all [binary data](#binary-data),
- expiring keys with a [time-to-live](#ttl),
- [read-only](#read-only) keys and [pinned](#pinned) entries,
- built-in [diagnostics](#doctor) and [configuration](#config),
and more, written in pure Go, and inspired by [skate](https://github.com/charmbracelet/skate) and [nb](https://github.com/xwmx/nb).
`pda` stores key-value pairs natively as [newline-delimited JSON](https://en.wikipedia.org/wiki/JSON_streaming#Newline-delimited_JSON) files. [`pda list`](#listing) outputs tabular data by default, but also supports [CSV](https://en.wikipedia.org/wiki/Comma-separated_values), [TSV](), [Markdown]() and [HTML]() tables, [JSON](), and raw NDJSON. Everything is in plaintext to make version control easy, and to avoid tying anybody to using this tool forever.
Git versioning can be initiated with [`pda init`](#git), and varying levels of automation can be toggled via the [config](#config): `git.autocommit`, `git.autofetch`, and `git.autopush`. Running Git operations on every change can be slow, but a commit is fast. A happy middle-ground is enabling `git.autocommit` and doing the rest manually via [`pda sync`](#git) when changing devices.
[Templates](#templates) are a primary feature, enabling values to make use of substitutions, environment variables, arbitrary shell execution, cross-references to other keys, and more.
### Installation
↑
Prerequisites ·
Build ·
Shell Completion
#### Prerequisites
`pda` has no mandatory requirements outside of a shell to run it in. However, it is enhanced by other tools being installed.
- [go](https://go.dev) is needed for compiling the `pda` binary.
- [git](https://git-scm.com) enhances `pda` with [version control](#git).
#### Build
The easiest (and most universal) way to install `pda` is to use `go install` to build from source. The same command can be used to update.
```bash
go install github.com/llywelwyn/pda@latest
# Or from a spceific commit.
git clone https://github.com/llywelwyn/pda
cd pda
go install
```
[Arch Linux](https://archlinux.org) users can install and update `pda` from the [aur](https://aur.archlinux.org) with a package manager of choice. There are two packages available: `pda`, the latest stable release, and `pda-git`, which will install the latest commit to the main branch on this repository.
```bash
# Latest stable release
yay -S pda
# Latest commit
yay -S pda-git
# Updating
yay -Syu pda
```
#### Setting up Shell Completion
`pda` is built with [cobra](https://cobra.dev) and so comes with shell completions for bash, zsh, fish, and powershell.
```bash
# Bash
pda completion bash > /etc/bash_completion.d/pda
# Zsh
pda completion zsh > "${fpath[1]}/_pda"
# Fish
pda completion fish > ~/.config/fish/completions/pda.fish
# Powershell
pda completion powershell | Out-String | Invoke-Expression
```
Powershell users will need to manually add the above command to their profile; the given command will only instantiate `pda` for the current shell instance.
## Overview
Most commands have aliases and flags. Run `pda help [command]` to see them.
### Key commands
↑ ·
Setting,
Getting,
Running,
Listing,
Editing,
Moving and Copying,
Removing
Use of `pda` revolves around creating keys with [`pda set`](#setting) and later retrieving them with [`pda get`](#getting). Keys can belong to a single store which can be set manually or left to default to the default store. Keys can be modified with [`pda edit`](#editing) and [`pda meta`](#metadata) for content or metadata editing respectively, and can be listed with [`pda list`](#listing). Keys are written as `KEY[@STORE]`. The default store can be configured with `store.default_store_name`.
Keys are capable of storing any arbitrary bytes and are not limited to just text.
Advanced usage of `pda` revolves around [templates](#templates) and [`pda run`](#running).
#### Setting
↑ ·
pda set
[`pda set`](#setting) (alias: [`s`](#setting)) creates a key-value pair, and requires a key and a value as inputs. The first argument given will always be used to determine the key (and store). Values can come from arguments, stdin, or a file.
```bash
# create a key-value pair
pda set name "Alice"
# create a key-value pair in the "Favourites" store
pda set movie@favourites "The Road"
# create a key-value pair with piped input
echo "Bob" | pda set name
# create a key-value pair with redirection
pda set example < silmarillion.txt
# create a pinned key-value pair from a file
pda set --pin example --file example.md
# create an encrypted key-value pair, expiring in one day
pda set secret "Secret data." --encrypt --ttl 24h
```
The `interactive` and `safe` flags exist to prevent accidentally overwriting an existing key when creating a new one. These flags exist on all writable commands.
```bash
# prevent ever overwriting an existing key
pda set name "Bob" --safe
# guarantee a prompt when overwriting an existing key
pda set name "Joe" --interactive
```
Making a key `readonly` will also prevent unintended changes. It prevents making any changes unless `force` is passed or the key is made writable once again with [`pda edit`](#editing) or [`pda meta`](#metadata).
```bash
# create a readonly key-value pair
pda set repo "https://github.com/llywelwyn/pda" --readonly
# force-overwrite a readonly key-value pair
pda set dog "A four-legged mammal that isn't a cat." --force
```
#### Getting
↑ ·
Templates ·
pda get
[`pda get`](#getting) (alias: [`g`](#getting)) can be used to retrieve a key's value, and takes one argument: the desired key. The value is output to stdout. [Templates](#templates) are evaluated at retrieval time unless opted-out via the `no-template` flag.
```bash
# get the value of a key
❯ pda get name
Alice
```
As mentioned in [setting](#setting), values support any arbitrary bytes. Values which are not valid UTF8 are retrieved slightly differently. Printing raw bytes directly in the terminal can (and will) cause [undefined behaviour](https://en.wikipedia.org/wiki/Undefined_behavior), so if a TTY is detected then a raw `pda get` will return instead some metadata about the contents of the bytes. In a non-TTY setting (when the data is piped or redirected), the raw bytes will be returned as expected.
If a representation of the bytes in a TTY is desired, the `base64` flag provides a safe way to view them.
```bash
# get the information of a non-UTF8 key
❯ pda get cat_gif
(size: 101.2k, image/gif)
# get the raw bytes of a non-UTF8 key via pipe
pda get cat_gif | xdg-open
# get the raw bytes of a non-UTF8 key via redirect
pda get cat_gif > cat.gif
# get the base64 representation of a non-UTF8 key
❯ pda get cat_gif --base64
R0lGODlhXANYAvf/MQAAAAEBAQICAgMDAwQEBAUFBQYGBggI...
```
The existence of a key can be checked with `exists`. It returns a `0 exit code` on an existent key, or a `1 exit code` on a non-existent one. This is primarily useful for scripting.
```bash
# check if an existent key exists
❯ pda get name --exists
exit code 0
# check if a non-existent key exists
❯ pda get nlammfd --exists
exit code 1
```
Running [`pda get`](#getting) will resolve templates in the stored key at run-time. This can be prevented with the `no-template` flag.
```bash
# set key "user" to a template of the USER environment variable
❯ pda set user "{{ env "USER" }}"
# get a templated key
❯ pda get user
lew
# get a templated key without resolving the template
❯ pda get user --no-template
{{ env "USER" }}
```
#### Running
↑ ·
pda get --run ·
pda run
[`pda run`](#running) retrieves a key and executes it as a shell command. It uses the shell set in `$SHELL. If, somehow, this environment variable is unset, it falls back and attempts to use `/bin/sh`.
Running takes one argument: the key. [`pda run`](#running) and [`pda get --run`](#getting) are functionally equivalent.
```bash
# create a key containg a script
❯ pda set my_script "echo Hello, world."
# get and run a key using $SHELL
❯ pda get my_script --run
Hello, world.
❯ pda run my_script
Hello, world.
```
[Templates](#templates) are fully resolved before any shell execution happens.
```bash
# create a key containing a script, and a template
❯ pda set greet 'echo "Hello, {{ default "Jane Doe" .NAME }}"'
# run the key directly in $SHELL
❯ pda run greet
Hello, Jane Doe
# run the key, setting NAME to "Alice"
❯ pda run greet NAME="Alice"
Hello, Alice
```
#### Listing
↑ ·
pda list ·
Filtering
[`pda list`](#listing) (alias: [`ls`](#listing)) displays your key-value pairs. The default columns are `meta,size,ttl,store,key,value`. Meta is a 4-char flag string: `(e)ncrypted (w)ritable (t)tl (p)inned`, or a dash for an unset flag.
-b, --base64 view binary data as base64
-c, --count print only the count of matching entries
[`pda list`](#listing) (alias: [`ls`](#listing)) displays stored key-value pairs with [pinned](#pinning) keys first, followed by alphabetical order. Default behaviour is to list [metadata](#metadata), size, [time-to-live](#ttl), store, the key, and value as a table. The order and visibility of every column can be toggled either via `list.default_columns` in the [config](#config) or via one-off flags.
It accepts one or zero arguments. If no argument is passed, with a default configuration [`pda list`](#listing) will display all keys from all stores, but this behaviour can be toggled to instead display only keys from the default store. If a store name is passed as an argument, only the keys contained within that store will be listed.
If `list.always_show_all_stores` is toggled off, the behaviour can be enabled on an individual basis by passing `all`.
```bash
# list all store contents
❯ pda list
Meta Size TTL Store Key Value
-w-p 5 - todos todo don't forget this
---- 23 - store url https://example.com
-w-- 5 - me name Alice
# list only the contents of the "todos" store
❯ pda list todos
Meta Size TTL Store Key Value
-w-p 5 - todos todo don't forget this
# list all store contents, but without Meta, Size, or TTL
❯ pda list --no-ttl --no-header --no-size
Store Key Value
todos todo don't forget this
store url https://example.com
me name Alice
# count the number of entries for a given query
❯ pda list --count
3
```
When [listing](#listing), [glob patterns](#filtering) can be used to further filter by `key`, `store`, or `value`.
```bash
# list all store contents beginning with "https"
pda ls --value "https**"
# list all store contents beginning with "https" with "db" in the key
pda ls --value "https**" --key "**db**"
```
The standard tabular output of [`pda list`](#listing) can be swapped out for tab-separated values, comma-separated values, a markdown table, a HTML table, newline-delimited JSON, or JSON. Newline-delimited JSON is the native storage format of a `pda` store.
```bash
# list all store contents as comma-separated values
❯ pda ls --format csv
Meta,Size,TTL,Store,Key,Value
-w--,5,-,store,name,Alice
# list all store contents as JSON
❯ pda ls --format json
[{"key":"name","value":"Alice","encoding":"text","store":"store"}]
```
By default, long values are truncated to fit the terminal, but this behaviour can be toggled via the [config](#config) or by passing `full`. The setting is `list.always_show_full_values`.
```bash
❯ pda ls
Key Value
note this is a very long (..30 more chars)
❯ pda ls --full
Key Value
note this is a very long value that keeps on going and going
```
As with [`getting`](#getting), non-UTF8 data in lists will be substituted for a summary rather than displaying raw bytes. This can be changed out for a base64 representation by passing `base64`.
#### Editing
↑ ·
pda edit
[`pda edit`](#editing) (alias: [`e`](#editing)) opens a key's value in your `$EDITOR`. If the key doesn't exist, an empty file is opened, and saving non-empty content creates it.
When [editing](#editing) a key, [metadata](#metadata)-altering flags can be passed in the same operation. These alterations will only take place if the edit is finalised by saving.
```bash
# edit an existing key or create a new one
pda edit name
# edit a key and pin it
pda edit name --pin
# edit a key and give it an expiration, and encrypt it
pda edit secret_stuff --ttl 1h --encrypt
# edit a key and make it readonly
pda edit do_not_change --readonly
```
Most `$EDITOR` will add a trailing newline on saving a file. These are stripped by default, but this behaviour can be toggled via `edit.always_preserve_newline` in the [config](#config) or as a one-off by passing `preserve-newline`.
```bash
# edit a key and preserve the automatic $EDITOR newline
pda edit example --preserve-newline
```
Binary values will be presented as base64 for editing and will be decoded back to raw bytes on save. [Read-only](#read-only) keys require being made writable before they can be edited, or `force` can be explicitly passed.
#### Moving & Copying
↑ ·
pda move,
pda copy
To move (or rename) a key, [`pda move`](#moving--copying) (alias: [`mv`](#moving--copying)) can be used. To copy a key, [`pda move --copy`](#moving--copying) (alias: [`cp`](#moving--copying)) can be used. With both of these operations, all [metadata](#metadata) is preserved.
```bash
# rename a key
❯ pda move name name2
ok renamed name to name2
# move a key across stores
❯ pda mv name name@some_other_store
ok renamed name to name@some_other_store
# copy a key
❯ pda cp name name2
ok copied name to name2
```
Accidental overwrites have a few ways of being prevented. `safe` exists for moving and copying as it does for all changeful commands, skipping an operation if a destination already exists that would be overwritten. A [read-only](#read-only) key will also prevent being moved or overwritten unless `force` is explicitly passed.
Additionally, `interactive` being passed or `key.always_prompt_overwrite` being enabled in the [config](#config) will cause a yes-no prompt to be presented if a key is going to be overwritten. Inversely, prompts can always be skipped by passing `yes`.
```bash
# move a key safely
❯ pda mv name name2 --safe
# info skipped 'name2': already exists
# move a key interactively
❯ pda mv name name2 --safe
??? overwrite 'name2'? (y/n)
>>> y
# move a key and skip all warning prompts
❯ pda mv name name2 --yes
ok renamed name to name2
```
#### Removing
↑ ·
pda help remove
[`pda remove`](#removing) (alias: [`rm`](#removing)) deletes one or more keys.
```bash
pda rm kitty
```
Remove multiple keys at once:
```bash
pda rm kitty dog@animals
```
Mix exact keys with [glob patterns](#filtering) using [`--key`](#removing):
```bash
pda set cog "cogs"
pda set dog "doggy"
pda set kitty "cat"
pda rm kitty --key "?og"
```
Filter by store with [`--store`](#removing) / `-s` and by value with [`--value`](#removing) / `-v`:
```bash
pda rm --store "temp*" --key "session*"
```
[`--interactive`](#removing) / `-i` prompts before each deletion (or set `key.always_prompt_delete` in [config](#config)):
```bash
pda rm kitty -i
# ??? remove 'kitty'? (y/n)
# ==> y
```
Glob-matched deletions prompt by default (configurable with `key.always_prompt_glob_delete`).
[`--yes`](#removing) / `-y` auto-accepts all confirmation prompts:
```bash
pda rm kitty -y
```
[Read-only](#read-only) keys can't be deleted without [`--force`](#removing):
```bash
❯ pda rm protected-key
FAIL cannot remove 'protected-key': key is read-only
pda rm protected-key --force
```
### Metadata
↑ ·
pda meta,
TTL,
Encryption,
Read-Only,
Pinned
[`pda meta`](#metadata) views or modifies metadata for a key without changing its value. With no flags, it displays the key's current metadata:
```bash
❯ pda meta session
key: session@store
secret: false
writable: true
pinned: false
expires: 59m30s
```
Pass flags to modify: [`--ttl`](#ttl), [`--encrypt`](#encryption) / [`--decrypt`](#encryption), [`--readonly`](#read-only) / [`--writable`](#read-only), [`--pin`](#pinned) / [`--unpin`](#pinned).
Multiple metadata changes can be combined in one call:
```bash
pda meta session --ttl 2h --encrypt --pin
```
Modifying a [read-only](#read-only) key's metadata requires [`--force`](#metadata) (except for toggling the read-only flag itself, and pin/unpin):
```bash
❯ pda meta api-url --ttl 1h
FAIL cannot meta 'api-url': key is read-only
pda meta api-url --ttl 1h --force
```
#### TTL
↑ ·
pda help set,
pda help meta
Keys can be given an expiration time. Expired keys are marked for garbage collection and deleted on the next access to the store.
Set a TTL at creation time with [`pda set --ttl`](#setting):
```bash
# expire after 1 hour
pda set session "123" --ttl 1h
# expire after 54 minutes and 10 seconds
pda set session2 "xyz" --ttl 54m10s
```
[`pda list`](#listing) shows expiration in the TTL column:
```bash
❯ pda ls
TTL Key Value
59m30s session 123
51m40s session2 xyz
```
Change or clear the TTL on an existing key with [`pda meta --ttl`](#metadata):
```bash
❯ pda meta session --ttl 2h
ok set ttl to 2h session
❯ pda meta session --ttl never
ok cleared ttl session
```
The [`edit`](#editing) command also accepts `--ttl`:
```bash
pda edit session --ttl 30m
```
[`export`](#import--export) and [`import`](#import--export) preserve the expiry date. Expirations are stored as a timestamp, not a timer — they continue ticking down regardless of whether the key is in an active store or sitting in a backup file.
#### Encryption
↑ ·
pda help set,
pda help meta,
pda help identity
[`pda set --encrypt`](#setting) encrypts values at rest using [age](https://github.com/FiloSottile/age). Values are stored on disk as age ciphertext and decrypted automatically by commands like [`get`](#getting) and [`list`](#listing) when the correct identity file is present. An X25519 identity is generated on first use.
```bash
pda set --encrypt api-key "sk-live-abc123"
# ok created identity at ~/.config/pda/identity.txt
pda set --encrypt token "ghp_xxxx"
```
[`pda get`](#getting) decrypts automatically:
```bash
❯ pda get api-key
sk-live-abc123
```
Toggle encryption on an existing key with [`pda meta`](#metadata):
```bash
❯ pda meta api-key --encrypt
ok encrypted api-key
❯ pda meta api-key --decrypt
ok decrypted api-key
```
The on-disk value is ciphertext, so encrypted entries are safe to commit and push with [Git](#git):
```bash
❯ pda export
{"key":"api-key","value":"YWdlLWVuY3J5cHRpb24u...","encoding":"secret"}
```
[`mv`](#moving--copying), [`cp`](#moving--copying), and [`import`](#import--export) all preserve encryption, read-only, and pinned flags. Overwriting an encrypted key without `--encrypt` will warn you:
```bash
pda cp api-key api-key-backup
# still encrypted
❯ pda set api-key "oops"
WARN overwriting encrypted key 'api-key' as plaintext
hint pass --encrypt to keep it encrypted
```
If the identity file is missing, encrypted values are inaccessible but not lost. Keys remain visible, and the ciphertext is preserved through reads and writes:
```bash
❯ pda ls
Meta Key Value
ew-- api-key locked (identity file missing)
❯ pda get api-key
FAIL cannot get 'api-key': secret is locked (identity file missing)
```
All encryption operations can be set as default with `key.always_encrypt` in [config](#config), so every [`pda set`](#setting) automatically encrypts.
#### Read-Only
↑ ·
pda help set,
pda help meta
Keys marked read-only are protected from accidental modification. You can modify a read-only key again by making it [`--writable`](#metadata) or by explicitly bypassing with [`--force`](#metadata).
Set a key as read-only at creation time:
```bash
pda set api-url "https://prod.example.com" --readonly
```
Toggle with [`pda meta`](#metadata):
```bash
❯ pda meta api-url --readonly
ok made readonly api-url
❯ pda meta api-url --writable
ok made writable api-url
```
Or alongside an edit:
```bash
pda edit notes --readonly
```
Read-only keys are protected from [`set`](#setting), [`rm`](#removing), [`mv`](#moving--copying), and [`edit`](#editing). Use `--force` to bypass:
```bash
❯ pda set api-url "new value"
FAIL cannot set 'api-url': key is read-only
pda set api-url "new value" --force
pda rm api-url --force
pda mv api-url new-name --force
```
Modifying a read-only key's metadata also requires `--force` (except for toggling the read-only flag itself, and pin/unpin):
```bash
❯ pda meta api-url --ttl 1h
FAIL cannot meta 'api-url': key is read-only
pda meta api-url --ttl 1h --force
```
[`cp`](#moving--copying) can copy a read-only key freely (since the source isn't modified), and the copy preserves the read-only flag. Overwriting a read-only destination is blocked without `--force`.
#### Pinned
↑ ·
pda help set,
pda help meta
Pinned keys sort to the top of [`pda list`](#listing) output, preserving alphabetical order within the pinned and unpinned groups.
Pin a key at creation time:
```bash
pda set important "remember this" --pin
```
Toggle with [`pda meta`](#metadata):
```bash
❯ pda meta todo --pin
ok pinned todo
❯ pda meta todo --unpin
ok unpinned todo
```
```bash
❯ pda ls
Meta Key Value
-w-p important remember this
-w-- name Alice
-w-- other foo
```
### Stores
↑ ·
pda list-stores,
pda move-store,
pda remove-store
You can have as many stores as you want. Stores are created implicitly when you set a key with a `@STORE` suffix. Each store is a separate NDJSON file on disk.
[`pda list-stores`](#stores) (alias: [`lss`](#stores)) shows all stores with key counts and file sizes:
```bash
❯ pda list-stores
Keys Size Store
2 1.8k @birthdays
12 4.2k @store
```
[`--short`](#stores) prints only the names:
```bash
❯ pda list-stores --short
@birthdays
@store
```
Save to a specific store with the `@STORE` syntax:
```bash
pda set alice@birthdays "11/11/1998"
```
List a specific store:
```bash
❯ pda ls @birthdays
Store Key Value
birthdays alice 11/11/1998
birthdays bob 05/12/1980
```
[`pda move-store`](#stores) (alias: [`mvs`](#stores)) renames a store:
```bash
pda move-store birthdays bdays
```
Copy a store with `--copy`:
```bash
pda move-store birthdays bdays --copy
```
[`--safe`](#stores) skips if the destination already exists:
```bash
pda move-store birthdays bdays --safe
```
[`pda remove-store`](#stores) (alias: [`rms`](#stores)) deletes a store:
```bash
pda remove-store birthdays
```
[`--yes`](#stores) / `-y` skips confirmation prompts:
```bash
pda remove-store birthdays -y
```
#### Import & Export
↑ ·
pda help export,
pda help import
[`pda export`](#import--export) exports everything as NDJSON (it's an alias for `list --format ndjson`):
```bash
pda export > my_backup
```
Filter exports with [`--key`](#filtering), [`--value`](#filtering), and [`--store`](#filtering):
```bash
# export only matching keys
pda export --key "a*"
# export only entries whose values contain a URL
pda export --value "**https**"
```
[`pda import`](#import--export) restores entries from an NDJSON dump. By default, each entry is routed to the store it came from (via the `"store"` field in the NDJSON). If no `"store"` field is present, entries go to `store.default_store_name`.
```bash
# entries are routed to their original stores
pda import -f my_backup
# ok restored 5 entries
```
Pass a store name as a positional argument to force all entries into one store:
```bash
pda import mystore -f my_backup
# ok restored 5 entries into @mystore
```
Read from stdin:
```bash
pda import < my_backup
```
Filter imports with [`--key`](#filtering) and [`--store`](#filtering):
```bash
# import only matching keys
pda import --key "a*" -f my_backup
# import only entries from matching stores
pda import --store "prod*" -f my_backup
```
[`--drop`](#import--export) does a full replace — drops all existing entries before importing:
```bash
pda import --drop -f my_backup
```
[`--interactive`](#import--export) / `-i` prompts before overwriting existing keys.
[`export`](#import--export) encodes [binary data](#binary-data) as base64. [Encryption](#encryption), [read-only](#read-only), [pinned](#pinned) flags, and [TTL](#ttl) are all preserved through export and import.
### Templates
↑ ·
pda get,
pda run
Values support Go's [`text/template`](https://pkg.go.dev/text/template) syntax. Templates are evaluated on [`pda get`](#getting) and [`pda run`](#running).
`text/template` is a Turing-complete templating library that supports pipelines, nested templates, conditionals, loops, and more. Actions are given with `{{ action }}` syntax. To fit `text/template` into a CLI key-value tool, `pda!` adds a small set of built-in functions on top of the standard library.
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").
#### Basic Substitution
↑ ·
Templates
Template variables are substituted from `KEY=VALUE` arguments passed to [`pda get`](#getting):
```bash
pda set greeting "Hello, {{ .NAME }}"
❯ pda get greeting NAME="Alice"
Hello, Alice
```
#### `default`
↑ ·
Templates
`default` sets a fallback value when a variable is missing or empty:
```bash
pda set greeting "Hello, {{ default "World" .NAME }}"
❯ pda get greeting
Hello, World
❯ pda get greeting NAME="Bob"
Hello, Bob
```
#### `require`
↑ ·
Templates
`require` errors if the variable is missing or empty:
```bash
pda set file "{{ require .FILE }}"
❯ pda get file
FAIL cannot get 'file': ...required value is missing or empty
```
#### `env`
↑ ·
Templates
`env` reads from environment variables:
```bash
pda set my_name "{{ env "USER" }}"
❯ pda get my_name
llywelwyn
```
#### `time`
↑ ·
Templates
`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`
↑ ·
Templates
`enum` restricts a variable to a set of acceptable values:
```bash
pda set level "Log level: {{ enum .LEVEL "info" "warn" "error" }}"
❯ pda get level LEVEL=info
Log level: info
❯ pda get level LEVEL=debug
FAIL cannot get 'level': ...invalid value 'debug', allowed: [info warn error]
```
#### `int`
↑ ·
Templates
`int` parses a variable as an integer, useful for loops and arithmetic:
```bash
pda set number "{{ int .N }}"
❯ pda get number N=3
3
```
Use it in a range loop:
```bash
pda set meows "{{ range int .COUNT }}meow! {{ end }}"
❯ pda get meows COUNT=4
meow! meow! meow! meow!
```
#### `list`
↑ ·
Templates
`list` parses a comma-separated string into a list for iteration:
```bash
pda set names "{{ range list .NAMES }}Hi {{.}}. {{ end }}"
❯ pda get names NAMES=Bob,Alice
Hi Bob. Hi Alice.
```
#### `shell`
↑ ·
Templates
`shell` executes a command and returns its stdout:
```bash
pda set rev '{{ shell "git rev-parse --short HEAD" }}'
❯ pda get rev
a1b2c3d
```
```bash
pda set today '{{ shell "date +%Y-%m-%d" }}'
❯ pda get today
2025-06-15
```
#### `pda` (Recursive)
↑ ·
Templates
`pda` gets another key's value, enabling recursive composition:
```bash
pda set base_url "https://api.example.com"
pda set endpoint '{{ pda "base_url" }}/users/{{ require .ID }}'
❯ pda get endpoint ID=42
https://api.example.com/users/42
```
Cross-store references work too:
```bash
pda set host@urls "https://example.com"
pda set api '{{ pda "host@urls" }}/api'
❯ pda get api
https://example.com/api
```
#### `no-template`
↑ ·
pda help get
Pass [`--no-template`](#getting) to [`pda get`](#getting) to output the raw value without evaluating templates:
```bash
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 }}
```
### Filtering
↑ ·
pda list,
pda remove,
pda export,
pda import
[`--key`](#filtering) / `-k`, [`--value`](#filtering) / `-v`, and [`--store`](#filtering) / `-s` filter entries with glob support. All three flags are repeatable, with results matching one-or-more of the patterns per flag. When multiple flags are combined, results must satisfy all of them (AND across flags, OR within the same flag).
These filters work with [`list`](#listing), [`export`](#import--export), [`import`](#import--export), and [`remove`](#removing). [`--value`](#filtering) is not available on [`import`](#import--export) or [`remove`](#removing).
[`gobwas/glob`](https://github.com/gobwas/glob) is used for matching. The default separators are `/-_.@:` and space.
#### Glob Patterns
↑ ·
Filtering
`*` wildcards a word or series of characters, stopping at separator boundaries:
```bash
❯ pda ls
cat
dog
cog
mouse hotdog
mouse house
foo.bar.baz
pda ls --key "*"
# cat, dog, cog (single-segment keys only)
pda ls --key "* *"
# mouse hotdog, mouse house
pda ls --key "foo.*.baz"
# foo.bar.baz
```
`**` super-wildcards ignore word boundaries:
```bash
pda ls --key "foo**"
# foo.bar.baz
pda ls --key "**g"
# dog, cog, mouse hotdog
```
`?` matches a single character:
```bash
pda ls --key "?og"
# dog, cog
```
`[abc]` matches one of the characters in the brackets:
```bash
pda ls --key "[dc]og"
# dog, cog
# negate with '!'
pda ls --key "[!dc]og"
# bog (if it exists)
```
`[a-c]` matches a range:
```bash
pda ls --key "[a-g]ag"
# bag, gag
pda ls --key "[!a-g]ag"
# wag
```
#### Filtering by Key
↑ ·
Filtering,
pda list
[`--key`](#filtering) / `-k` filters entries by key name:
```bash
pda ls --key "db*"
pda ls --key "session*" --key "token*"
```
Multiple `--key` patterns are OR'd — an entry matches if it matches any of them.
#### Filtering by Value
↑ ·
Filtering,
pda list
[`--value`](#filtering) / `-v` filters by value content using the same glob syntax:
```bash
❯ pda ls --value "**localhost**"
Key Value
db-url postgres://localhost:5432
```
Multiple `--value` patterns are OR'd:
```bash
❯ pda ls --value "**world**" --value "42"
Key Value
greeting hello world
number 42
```
Locked (encrypted without an available identity) and non-UTF-8 (binary) entries are silently excluded from `--value` matching.
#### Filtering by Store
↑ ·
Filtering,
pda list
[`--store`](#filtering) / `-s` filters by store name:
```bash
pda ls --store "prod*"
pda export --store "dev*"
```
#### Combining Filters
↑ ·
Filtering
Combine key, value, and store filters. Results must match all flags (AND), with OR within each flag:
```bash
pda ls --key "db*" --value "**localhost**"
```
Globs can be arbitrarily complex, and [`--key`](#filtering) can be combined with exact positional args on [`rm`](#removing):
```bash
pda rm cat --key "{mouse,[cd]og}**"
# ??? remove 'cat'? (y/n)
# ==> y
# ??? remove 'mouse trap'? (y/n)
# ...
```
### Binary Data
↑ ·
pda set,
pda get,
pda list
`pda!` supports all binary data. Save it with [`pda set`](#setting):
```bash
pda set logo < logo.png
pda set logo -f logo.png
```
And retrieve it with [`pda get`](#getting):
```bash
pda get logo > output.png
```
On a TTY, [`get`](#getting) and [`list`](#listing) show a summary for binary data. If piped or run outside of a TTY, raw bytes are output:
```bash
❯ pda get logo
(binary: 4.2 KB, image/png)
```
[`--base64`](#getting) / `-b` views binary data as base64:
```bash
❯ pda get logo --base64
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVQI12...
```
[`pda export`](#import--export) encodes binary data as base64 in the NDJSON:
```bash
❯ pda export
{"key":"logo","value":"89504E470D0A1A0A0000000D4948445200000001000000010802000000","encoding":"base64"}
```
[`pda edit`](#editing) presents binary values as base64 for editing and decodes them back on save.
### Git
↑ ·
pda init,
pda sync,
Config
`pda!` supports automatic version control backed by Git, either in a local-only repository or by initialising from a remote.
#### Init
↑ ·
pda help init
[`pda init`](#git) initialises version control:
```bash
# initialise an empty repository
pda init
# or clone an existing one
pda init https://github.com/llywelwyn/my-repository
```
[`--clean`](#git) removes the existing `.git` directory first, useful for reinitialising or switching remotes:
```bash
pda init --clean
pda init https://github.com/llywelwyn/my-repository --clean
```
#### Sync
↑ ·
pda help sync
[`pda sync`](#sync) conducts a best-effort sync of your local data with your Git repository. Any time you swap machine or know you've made changes outside of `pda!`, syncing is recommended.
If you're ahead, syncing will commit and push. If you're behind, syncing will detect this and prompt you: either stash local changes and pull, or abort and fix manually.
```bash
# sync with Git
pda sync
# with a custom commit message
pda sync -m "added production credentials"
```
Running [`pda sync`](#sync) manually will always fetch, commit, and push — or stash and pull if behind — regardless of config.
#### Auto-Commit & Auto-Push
↑ ·
pda help config
`pda!` supports automation via its [config](#config). There are options for `git.auto_commit`, `git.auto_fetch`, and `git.auto_push`.
**`git.auto_commit`** commits changes immediately to the local Git repository any time data is changed.
**`git.auto_fetch`** fetches before committing any changes. This incurs a noticeable slowdown due to network round-trips.
**`git.auto_push`** automatically pushes committed changes to the remote repository, if one is configured.
If `auto_commit` is false, `auto_fetch` and `auto_push` have no effect. They are additional steps in the commit process.
A recommended setup is to enable `git.auto_commit` and run [`pda sync`](#sync) manually when switching machines.
### Identity
↑ ·
pda identity,
Encryption
[`pda identity`](#identity) (alias: [`id`](#identity)) manages the age encryption identity used for [encryption](#encryption).
#### Viewing Identity
↑ ·
pda help identity
With no flags, [`pda identity`](#identity) shows your public key, identity file path, and any additional recipients:
```bash
❯ pda identity
ok pubkey age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
ok identity ~/.config/pda/identity.txt
```
[`--path`](#identity) prints only the identity file path:
```bash
❯ pda identity --path
~/.config/pda/identity.txt
```
#### Creating an Identity
↑ ·
pda help identity
An identity is generated automatically the first time you use [`--encrypt`](#encryption). To create one manually:
```bash
pda identity --new
```
[`--new`](#identity) errors if an identity already exists. Delete the file manually to replace it.
#### Recipients
↑ ·
pda help identity
By default, secrets are encrypted only for your own identity. To encrypt for additional recipients (e.g. a teammate or another device), use [`--add-recipient`](#identity) with their age public key. All existing secrets are automatically re-encrypted for every recipient:
```bash
❯ pda identity --add-recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
ok re-encrypted api-key
ok added recipient age1ql3z...
ok re-encrypted 1 secret(s)
```
Remove a recipient with [`--remove-recipient`](#identity). Secrets are re-encrypted without their key:
```bash
pda identity --remove-recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
```
Additional recipients are shown in the default identity display:
```bash
❯ pda identity
ok pubkey age1abc...
ok identity ~/.local/share/pda/identity.txt
ok recipient age1ql3z...
```
### Config
↑ ·
pda config,
pda doctor
Config is stored at `~/.config/pda/config.toml` (Linux/macOS) or `%LOCALAPPDATA%/pda/config.toml` (Windows). All values have sensible defaults, so a config file is entirely optional.
#### Config Commands
↑ ·
pda help config
[`pda config`](#config) manages configuration without editing files by hand:
```bash
# list all config values and their current settings
pda config list
# get a single value
❯ pda config get git.auto_commit
false
# set a value (validated before saving)
pda config set git.auto_commit true
# open in $EDITOR (validated on save)
pda config edit
# print the config file path
pda config path
# generate a fresh default config file
pda config init
# overwrite an existing config with defaults
pda config init --new
# update config: migrate deprecated keys and fill missing defaults
pda config init --update
```
[`pda doctor`](#doctor) will warn about unrecognised keys (typos, removed options) and show any non-default values, so it doubles as a config audit.
#### Example config.toml
↑ ·
Config,
pda help config
All values below are the defaults. A missing config file or missing keys will use these values.
```toml
# display ascii header in long root and version commands
display_ascii_art = true
[key]
# prompt y/n before deleting keys
always_prompt_delete = false
# prompt y/n before deleting with a glob match
always_prompt_glob_delete = true
# prompt y/n before key overwrites
always_prompt_overwrite = false
# encrypt all values at rest by default
always_encrypt = false
[store]
# store name used when none is specified
default_store_name = "store"
# prompt y/n before deleting whole store
always_prompt_delete = true
# prompt y/n before store overwrites
always_prompt_overwrite = true
[list]
# list all, or list only the default store when none specified
always_show_all_stores = true
# default output, accepts: table|tsv|csv|markdown|html|ndjson|json
default_list_format = "table"
# show full values without truncation
always_show_full_values = false
# suppress the header row
always_hide_header = false
# columns and order, accepts: meta,size,ttl,store,key,value
default_columns = "meta,size,ttl,store,key,value"
[git]
# auto fetch whenever a change happens
auto_fetch = false
# auto commit any changes
auto_commit = false
# auto push after committing
auto_push = false
# commit message if none manually specified
# supports templates, see: #templates section
default_commit_message = "{{ summary }} {{ time }}"
```
### Environment
↑ ·
Config,
pda doctor
`PDA_CONFIG` overrides the config directory. `pda!` will look for `config.toml` in this directory:
```bash
PDA_CONFIG=/tmp/config/ pda set key value
```
`PDA_DATA` overrides the data storage directory:
```bash
PDA_DATA=/tmp/stores pda set key value
```
Default data locations:
- Linux: `~/.local/share/pda/`
- macOS: `~/Library/Application Support/pda/`
- Windows: `%LOCALAPPDATA%/pda/`
`EDITOR` is used by [`pda edit`](#editing) and [`pda config edit`](#config) to open values in a text editor. Must be set for these commands to work:
```bash
EDITOR=nvim pda edit mykey
```
`SHELL` is used by [`pda run`](#running) (or [`pda get --run`](#getting)) for command execution. Falls back to `/bin/sh` if unset:
```bash
pda run script
```
### Doctor
↑ ·
Config,
Environment
[`pda doctor`](#doctor) runs a set of health checks of your environment:
```bash
❯ pda doctor
ok pda! 2025.52 Christmas release (linux/amd64)
ok OS: Linux 6.18.7-arch1-1
ok Go: go1.23.0
ok Git: 2.45.0
ok Shell: /bin/zsh
ok Config: /home/user/.config/pda
ok Non-default config:
├── display_ascii_art: false
└── git.auto_commit: true
ok Data: /home/user/.local/share/pda
ok Identity: /home/user/.config/pda/identity.txt
ok Git initialised on main
ok Git remote configured
ok Git in sync with remote
ok 3 store(s), 15 key(s), 2 secret(s), 4.2k total size
ok No issues found
```
Severity levels are colour-coded: `ok` (green), `WARN` (yellow), and `FAIL` (red). Only `FAIL` produces a non-zero exit code. `WARN` is generally not a problem, but may mean some functionality isn't being made use of, like version control not having been initialised yet.
### Version
↑ ·
pda help version
```bash
# display the full version output
pda version
# or just the release
❯ pda version --short
pda! 2025.52 Christmas release
```
`pda!` uses calendar versioning: `YYYY.WW`. ASCII art can be permanently disabled with `display_ascii_art = false` in [config](#config).
### Help
↑
#### `pda set`
↑ ·
See also:
Setting
```text
Set a key to a given value or stdin. Optionally specify a store.
Pass --encrypt to encrypt the value at rest using age. An identity file
is generated automatically on first use.
PDA supports parsing Go templates. Actions are delimited with {{ }}.
For example:
'Hello, {{ .NAME }}' can be substituted with NAME="John Doe".
'Hello, {{ env "USER" }}' will fetch the USER env variable.
'Hello, {{ default "World" .NAME }}' will default to World if NAME is blank.
'Hello, {{ require .NAME }}' will error if NAME is blank.
'{{ enum .NAME "Alice" "Bob" }}' allows only NAME=Alice or NAME=Bob.
Usage:
pda set KEY[@STORE] [VALUE] [flags]
Aliases:
set, s
Flags:
-e, --encrypt encrypt the value at rest using age
-f, --file string read value from a file
--force bypass read-only protection
-h, --help help for set
-i, --interactive prompt before overwriting an existing key
--pin pin the key (sorts to top in list)
--readonly mark the key as read-only
--safe do not overwrite if the key already exists
-t, --ttl duration expire the key after the provided duration (e.g. 24h, 30m)
```
#### `pda get`
↑ ·
See also:
Getting,
Templates
```text
Get the value of a key. Optionally specify a store.
{{ .TEMPLATES }} can be filled by passing TEMPLATE=VALUE as an
additional argument after the initial KEY being fetched.
For example:
pda set greeting 'Hello, {{ .NAME }}!'
pda get greeting NAME=World
Usage:
pda get KEY[@STORE] [flags]
Aliases:
get, g
Flags:
-b, --base64 view binary data as base64
--exists exit 0 if the key exists, exit 1 if not (no output)
-h, --help help for get
--no-template directly output template syntax
-c, --run execute the result as a shell command
```
#### `pda run`
↑ ·
See also:
Running,
Templates
```text
Get the value of a key and execute it as a shell command. Optionally specify a store.
{{ .TEMPLATES }} can be filled by passing TEMPLATE=VALUE as an
additional argument after the initial KEY being fetched.
For example:
pda set greeting 'Hello, {{ .NAME }}!'
pda run greeting NAME=World
Usage:
pda run KEY[@STORE] [flags]
Flags:
-b, --base64 view binary data as base64
-h, --help help for run
--no-template directly output template syntax
```
#### `pda list`
↑ ·
See also:
Listing,
Filtering
```text
List the contents of all stores.
By default, list shows entries from every store. Pass a store name as a
positional argument to narrow to a single store, or use --store/-s with a
glob pattern to filter by store name.
Use --key/-k and --value/-v to filter by key or value glob, and --store/-s
to filter by store name. All filters are repeatable and OR'd within the
same flag.
Usage:
pda list [STORE] [flags]
Aliases:
list, ls
Flags:
-a, --all list across all stores
-b, --base64 view binary data as base64
-c, --count print only the count of matching entries
-o, --format format output format (table|tsv|csv|markdown|html|ndjson|json)
-f, --full show full values without truncation
-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-meta suppress the meta column
--no-size suppress the size column
--no-store suppress the store column
--no-ttl suppress the TTL column
--no-values suppress the value column
-s, --store strings filter stores with glob pattern (repeatable)
-v, --value strings filter values with glob pattern (repeatable)
```
#### `pda edit`
↑ ·
See also:
Editing
```text
Open a key's value in $EDITOR. If the key doesn't exist, opens an
empty file — saving non-empty content creates the key.
Binary values are presented as base64 for editing and decoded back on save.
Metadata flags (--ttl, --encrypt, --decrypt) can be passed alongside the edit
to modify metadata in the same operation.
Usage:
pda edit KEY[@STORE] [flags]
Aliases:
edit, e
Flags:
-d, --decrypt decrypt the value (store as plaintext)
-e, --encrypt encrypt the value at rest
--force bypass read-only protection
-h, --help help for edit
--pin pin the key (sorts to top in list)
--preserve-newline keep trailing newlines added by the editor
--readonly mark the key as read-only
--ttl string set expiry (e.g. 30m, 2h) or 'never' to clear
--unpin unpin the key
--writable clear the read-only flag
```
#### `pda move`
↑ ·
See also:
Moving & Copying
```text
Move a key
Usage:
pda move FROM[@STORE] TO[@STORE] [flags]
Aliases:
move, mv
Flags:
--copy copy instead of move (keeps source)
--force bypass read-only protection
-h, --help help for move
-i, --interactive prompt before overwriting destination
--safe do not overwrite if the destination already exists
-y, --yes skip all confirmation prompts
```
#### `pda copy`
↑ ·
See also:
Moving & Copying
```text
Make a copy of a key
Usage:
pda copy FROM[@STORE] TO[@STORE] [flags]
Aliases:
copy, cp
Flags:
--force bypass read-only protection
-h, --help help for copy
-i, --interactive prompt before overwriting destination
--safe do not overwrite if the destination already exists
-y, --yes skip all confirmation prompts
```
#### `pda remove`
↑ ·
See also:
Removing,
Filtering
```text
Delete one or more keys
Usage:
pda remove KEY[@STORE] [KEY[@STORE] ...] [flags]
Aliases:
remove, rm
Flags:
--force bypass read-only protection
-h, --help help for remove
-i, --interactive prompt yes/no for each deletion
-k, --key strings delete keys matching glob pattern (repeatable)
-s, --store strings target stores matching glob pattern (repeatable)
-v, --value strings delete entries matching value glob pattern (repeatable)
-y, --yes skip all confirmation prompts
```
#### `pda meta`
↑ ·
See also:
Metadata,
TTL,
Encryption,
Read-Only,
Pinned
```text
View or modify metadata (TTL, encryption, read-only, pinned) for a key
without changing its value.
With no flags, displays the key's current metadata. Pass flags to modify.
Usage:
pda meta KEY[@STORE] [flags]
Flags:
-d, --decrypt decrypt the value (store as plaintext)
-e, --encrypt encrypt the value at rest
--force bypass read-only protection for metadata changes
-h, --help help for meta
--pin pin the key (sorts to top in list)
--readonly mark the key as read-only
--ttl string set expiry (e.g. 30m, 2h) or 'never' to clear
--unpin unpin the key
--writable clear the read-only flag
```
#### `pda identity`
↑ ·
See also:
Identity,
Encryption
```text
Show or create the age encryption identity
Usage:
pda identity [flags]
Aliases:
identity, id
Flags:
--add-recipient string add an age public key as an additional encryption recipient
-h, --help help for identity
--new generate a new identity (errors if one already exists)
--path print only the identity file path
--remove-recipient string remove an age public key from the recipient list
```
#### `pda export`
↑ ·
See also:
Import & Export
```text
Export store as NDJSON (alias for list --format ndjson)
Usage:
pda export [STORE] [flags]
Flags:
-h, --help help for export
-k, --key strings filter keys with glob pattern (repeatable)
-s, --store strings filter stores with glob pattern (repeatable)
-v, --value strings filter values with glob pattern (repeatable)
```
#### `pda import`
↑ ·
See also:
Import & Export
```text
Restore key/value pairs from an NDJSON dump
Usage:
pda import [STORE] [flags]
Flags:
--drop drop existing entries before restoring (full replace)
-f, --file string path to an NDJSON dump (defaults to stdin)
-h, --help help for import
-i, --interactive prompt before overwriting existing keys
-k, --key strings restore keys matching glob pattern (repeatable)
-s, --store strings restore entries from stores matching glob pattern (repeatable)
```
#### `pda list-stores`
↑ ·
See also:
Stores
```text
List all stores
Usage:
pda list-stores [flags]
Aliases:
list-stores, lss
Flags:
-h, --help help for list-stores
--no-header suppress the header row
--short only print store names
```
#### `pda move-store`
↑ ·
See also:
Stores
```text
Rename a store
Usage:
pda move-store FROM TO [flags]
Aliases:
move-store, mvs
Flags:
--copy copy instead of move (keeps source)
-h, --help help for move-store
-i, --interactive prompt before overwriting destination
--safe do not overwrite if the destination store already exists
-y, --yes skip all confirmation prompts
```
#### `pda remove-store`
↑ ·
See also:
Stores
```text
Delete a store
Usage:
pda remove-store STORE [flags]
Aliases:
remove-store, rms
Flags:
-h, --help help for remove-store
-i, --interactive prompt yes/no for each deletion
-y, --yes skip all confirmation prompts
```
#### `pda init`
↑ ·
See also:
Init,
Git
```text
Initialise pda! version control
Usage:
pda init [remote-url] [flags]
Flags:
--clean remove .git from stores directory before initialising
-h, --help help for init
```
#### `pda sync`
↑ ·
See also:
Sync,
Git
```text
Manually sync your stores with Git
Usage:
pda sync [flags]
Flags:
-h, --help help for sync
-m, --message string custom commit message (defaults to timestamp)
```
#### `pda git`
↑ ·
See also:
Git
```text
Run any arbitrary command. Use with caution.
The Git repository lives directly in the data directory
("PDA_DATA"). Store files (*.ndjson) are tracked by Git as-is.
If you manually modify files without using the built-in
commands, you may desync your repository.
Generally prefer "pda sync".
Usage:
pda git [args...] [flags]
Flags:
-h, --help help for git
```
#### `pda config`
↑ ·
See also:
Config
```text
View and modify configuration
Usage:
pda config [command]
Available Commands:
edit Open config file in $EDITOR
get Print a configuration value
init Generate default config file
list List all configuration values
path Print config file path
set Set a configuration value
Flags:
-h, --help help for config
Use "pda config [command] --help" for more information about a command.
```
#### `pda doctor`
↑ ·
See also:
Doctor
```text
Check environment health
Usage:
pda doctor [flags]
Flags:
-h, --help help for doctor
```
#### `pda version`
↑ ·
See also:
Version
```text
Display pda! version
Usage:
pda version [flags]
Flags:
-h, --help help for version
--short print only the version string
```
### License
↑
MIT — see [LICENSE](LICENSE).