2033 lines
50 KiB
Markdown
2033 lines
50 KiB
Markdown
<p align="center"></p><!-- spacer -->
|
||
|
||
<div align="center">
|
||
<img src="https://raw.githubusercontent.com/llywelwyn/pda/master/docs/pda.png"
|
||
alt="pda"
|
||
width="200" />
|
||
</div>
|
||
|
||
<p align="center"></p><!-- spacer -->
|
||
|
||
<div align="center">
|
||
<a href="https://github.com/llywelwyn/pda/actions" rel="nofollow">
|
||
<img src="https://img.shields.io/github/actions/workflow/status/llywelwyn/pda/go.yml?branch=main"
|
||
alt="build status"
|
||
style="max-width:100%;">
|
||
</a>
|
||
</div>
|
||
|
||
<p align="center"></p><!-- spacer -->
|
||
|
||
`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).
|
||
|
||
<p align="center"></p><!-- spacer -->
|
||
|
||
`pda!` stores key-value pairs natively as [newline-delimited JSON](https://en.wikipedia.org/wiki/JSON_streaming#Newline-delimited_JSON) files. Every store is plaintext, portable, and yours. There's no daemon, no cloud service, and no proprietary format. Keys are just lines in a JSON file; stores are just files in a directory. If you can `cat` a file, you can read your data without `pda!` installed.
|
||
|
||
Git versioning is built in. Enable auto-committing, pushing, and fetching in the [config](#config) to automatically version every change, or just run [`pda sync`](#sync) when you want to. Because the storage format is line-oriented plaintext, diffs are meaningful and merges are clean.
|
||
|
||
Go's [`text/template`](https://pkg.go.dev/text/template) engine is available on every value at retrieval time, turning simple key-value pairs into dynamic snippets with variables, environment lookups, shell execution, cross-references, and more.
|
||
|
||
<p align="center"></p><!-- spacer -->
|
||
|
||
<div align="center">
|
||
<a href="https://github.com/llywelwyn/pda/actions" rel="nofollow">
|
||
<img src="https://github.com/Llywelwyn/pda/blob/main/vhs/intro.gif"
|
||
alt="pda demo"
|
||
style="max-width:50%;">
|
||
</a>
|
||
</div>
|
||
|
||
<p align="center"></p><!-- spacer -->
|
||
|
||
### Installation
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a>
|
||
<a href="#prerequisites">Prerequisites</a> ·
|
||
<a href="#build">Build</a> ·
|
||
<a href="#setting-up-shell-completion">Shell Completion</a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### 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.
|
||
|
||
<p align="center"></p><!-- spacer -->
|
||
|
||
## Overview
|
||
|
||
<div align="center">
|
||
<a href="#setting">Setting</a> ·
|
||
<a href="#getting">Getting</a> ·
|
||
<a href="#running">Running</a> ·
|
||
<a href="#listing">Listing</a> ·
|
||
<a href="#editing">Editing</a> ·
|
||
<a href="#moving--copying">Moving & Copying</a> ·
|
||
<a href="#removing">Removing</a> ·
|
||
<a href="#metadata">Metadata</a> ·
|
||
<a href="#ttl">TTL</a> ·
|
||
<a href="#encryption">Encryption</a> ·
|
||
<a href="#read-only">Read-Only</a> ·
|
||
<a href="#pinned">Pinned</a> ·
|
||
<a href="#stores">Stores</a> ·
|
||
<a href="#import--export">Import & Export</a> ·
|
||
<a href="#templates">Templates</a> ·
|
||
<a href="#filtering">Filtering</a> ·
|
||
<a href="#binary-data">Binary Data</a>
|
||
<a href="#git">Git</a> ·
|
||
<a href="#identity">Identity</a> ·
|
||
<a href="#config">Config</a> ·
|
||
<a href="#environment">Environment</a> ·
|
||
<a href="#doctor">Doctor</a> ·
|
||
<a href="#help--version">Help & Version</a>
|
||
</div>
|
||
|
||
<p align="center"></p><!-- spacer -->
|
||
|
||
```bash
|
||
pda! MIT licensed. (c) 2025 Lewis Wynne
|
||
|
||
Usage:
|
||
pda [command]
|
||
|
||
Key commands:
|
||
copy Make a copy of a key
|
||
edit Edit a key's value in $EDITOR
|
||
get Get the value of a key
|
||
identity Show or create the age encryption identity
|
||
list List the contents of all stores
|
||
meta View or modify metadata for a key
|
||
move Move a key
|
||
remove Delete one or more keys
|
||
run Get the value of a key and execute it
|
||
set Set a key to a given value
|
||
|
||
Store commands:
|
||
export Export store as NDJSON (alias for list --format ndjson)
|
||
import Restore key/value pairs from an NDJSON dump
|
||
list-stores List all stores
|
||
move-store Rename a store
|
||
remove-store Delete a store
|
||
|
||
Git commands:
|
||
git Run any arbitrary command. Use with caution.
|
||
init Initialise pda! version control
|
||
sync Manually sync your stores with Git
|
||
|
||
Environment commands:
|
||
config View and modify configuration
|
||
doctor Check environment health
|
||
|
||
Additional Commands:
|
||
completion Generate the autocompletion script for the specified shell
|
||
help Help about any command
|
||
version Display pda! version
|
||
```
|
||
|
||
<p align="center"></p><!-- spacer -->
|
||
|
||
Most commands have aliases and flags. Run `pda help [command]` to see them.
|
||
|
||
### Key commands
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#setting"><code>pda set</code></a>,
|
||
<a href="#getting"><code>pda get</code></a>,
|
||
<a href="#running"><code>pda run</code></a>,
|
||
<a href="#listing"><code>pda list</code></a>,
|
||
<a href="#editing"><code>pda edit</code></a>,
|
||
<a href="#moving--copying"><code>pda move</code></a>,
|
||
<a href="#removing"><code>pda remove</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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`](#setting) (alias: [`s`](#setting)) creates a key-value pair. Values can come from arguments, stdin, or a file.
|
||
|
||
```bash
|
||
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 set`](#setting) requires a key and a value as inputs. The first argument given will always be used to determine the key.
|
||
|
||
```bash
|
||
# create a key-value pair
|
||
pda set name "Alice"
|
||
|
||
# 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 a key-value pair in the "Favourites" store
|
||
pda set movie@favourites "The Road"
|
||
|
||
# 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
|
||
|
||
[`pda get`](#getting) (alias: [`g`](#getting)) retrieves a key's value. [Templates](#templates) are evaluated at retrieval time.
|
||
|
||
```bash
|
||
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 get`] takes one argument: the desired key. The value is output to stdout.
|
||
|
||
```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
|
||
{{ env "USER" }}
|
||
```
|
||
|
||
An alternative to [templates](#templates) is the `run` flag. For detailed information, see [`pda run`](#running), an alias for `pda get --run`.
|
||
|
||
```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.
|
||
```
|
||
|
||
#### Running
|
||
|
||
[`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`. Templates are functional when running a key directly.
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
Running takes one argument: the key.
|
||
|
||
```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`](#listing) (alias: [`ls`](#listing)) shows what you've got stored. 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.
|
||
|
||
```bash
|
||
❯ pda ls
|
||
Meta Size TTL Store Key Value
|
||
-w-p 5 - store todo don't forget this
|
||
---- 23 - store url https://prod.example.com
|
||
-w-- 5 - store name Alice
|
||
```
|
||
|
||
By default, [`pda list`](#listing) shows entries from every store. Pass a store name to narrow to a single store:
|
||
|
||
```bash
|
||
pda ls @store
|
||
```
|
||
|
||
Use [`--store`](#filtering) / `-s` to filter stores by [glob pattern](#filtering):
|
||
|
||
```bash
|
||
pda ls --store "prod*"
|
||
```
|
||
|
||
Filter by key or value with [`--key`](#filtering) / `-k` and [`--value`](#filtering) / `-v`:
|
||
|
||
```bash
|
||
pda ls --key "db*" --value "**localhost**"
|
||
```
|
||
|
||
Columns can be toggled with `--no-X` flags. `--no-X` suppresses a column; `--no-X=false` adds it even if it's not in the default config:
|
||
|
||
```bash
|
||
# hide the meta and size columns
|
||
pda ls --no-meta --no-size
|
||
```
|
||
|
||
Long values are truncated to fit the terminal. [`--full`](#listing) / `-f` shows the complete value:
|
||
|
||
```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
|
||
```
|
||
|
||
[`--count`](#listing) / `-c` prints only the count of matching entries:
|
||
|
||
```bash
|
||
❯ pda ls --count
|
||
3
|
||
|
||
❯ pda ls --count --key "d*"
|
||
1
|
||
```
|
||
|
||
[`--format`](#listing) / `-o` selects the output format. Available formats: `table` (default), `csv`, `tsv`, `json`, `ndjson`, `markdown`, `html`:
|
||
|
||
```bash
|
||
❯ pda ls --format csv
|
||
Meta,Size,TTL,Store,Key,Value
|
||
-w--,5,-,store,name,Alice
|
||
|
||
❯ pda ls --format json
|
||
[{"key":"name","value":"Alice","encoding":"text","store":"store"}]
|
||
```
|
||
|
||
[`--all`](#listing) / `-a` lists across all stores (default when `list.always_show_all_stores` is true).
|
||
|
||
[`--base64`](#listing) / `-b` shows binary data as base64.
|
||
|
||
[`--no-header`](#listing) suppresses the header row.
|
||
|
||
[Pinned](#pinned) entries sort to the top, preserving alphabetical order within the pinned and unpinned groups.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#listing"><code>pda help list</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Editing
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#editing"><code>pda edit</code></a>,
|
||
<a href="#setting"><code>pda set</code></a>,
|
||
<a href="#metadata"><code>pda meta</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`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 — saving non-empty content creates it.
|
||
|
||
```bash
|
||
# edit an existing key
|
||
pda edit name
|
||
|
||
# edit a new key — saving non-empty content creates it
|
||
pda edit newkey
|
||
```
|
||
|
||
Metadata flags can be passed alongside the edit to modify metadata in the same operation:
|
||
|
||
```bash
|
||
pda edit name --ttl 1h --encrypt
|
||
```
|
||
|
||
Trailing newlines added by the editor are stripped by default. [`--preserve-newline`](#editing) keeps them:
|
||
|
||
```bash
|
||
pda edit name --preserve-newline
|
||
```
|
||
|
||
[`--encrypt`](#editing) / `-e` encrypts the value. [`--decrypt`](#editing) / `-d` decrypts it. [`--readonly`](#editing) and [`--writable`](#editing) toggle protection. [`--pin`](#editing) and [`--unpin`](#editing) toggle pinning. [`--ttl`](#editing) sets or clears expiry (e.g. `30m`, `2h`, or `never`).
|
||
|
||
Binary values are presented as base64 for editing and decoded back on save.
|
||
|
||
[Read-only](#read-only) keys require [`--force`](#editing) to edit.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#editing"><code>pda help edit</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Moving & Copying
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#moving--copying"><code>pda move</code></a>,
|
||
<a href="#moving--copying"><code>pda copy</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`pda move`](#moving--copying) (alias: [`mv`](#moving--copying)) moves a key to a new name or store. All metadata is preserved.
|
||
|
||
```bash
|
||
❯ pda mv name name2
|
||
ok renamed name to name2
|
||
```
|
||
|
||
[`pda copy`](#moving--copying) (alias: [`cp`](#moving--copying)) makes a copy. The source is kept and all metadata is preserved.
|
||
|
||
```bash
|
||
pda cp name name2
|
||
```
|
||
|
||
[`mv --copy`](#moving--copying) and [`cp`](#moving--copying) are equivalent:
|
||
|
||
```bash
|
||
pda mv name name2 --copy
|
||
```
|
||
|
||
Move or copy across stores:
|
||
|
||
```bash
|
||
pda mv name@store name@archive
|
||
pda cp config@dev config@prod
|
||
```
|
||
|
||
[`--safe`](#moving--copying) skips if the destination already exists:
|
||
|
||
```bash
|
||
pda mv name name2 --safe
|
||
# info skipped 'name2': already exists
|
||
```
|
||
|
||
[`--yes`](#moving--copying) / `-y` skips all confirmation prompts:
|
||
|
||
```bash
|
||
pda mv name name2 -y
|
||
```
|
||
|
||
[Read-only](#read-only) keys can't be moved or overwritten without [`--force`](#moving--copying):
|
||
|
||
```bash
|
||
❯ pda mv readonly-key newname
|
||
FAIL cannot move 'readonly-key': key is read-only
|
||
|
||
pda mv readonly-key newname --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`](#moving--copying).
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#moving--copying"><code>pda help move</code></a>,
|
||
<a href="#moving--copying"><code>pda help copy</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Removing
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#removing"><code>pda remove</code></a>,
|
||
<a href="#filtering"><code>--key</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`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
|
||
```
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#removing"><code>pda help remove</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
### Metadata
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#metadata"><code>pda meta</code></a>,
|
||
<a href="#ttl">TTL</a>,
|
||
<a href="#encryption">Encryption</a>,
|
||
<a href="#read-only">Read-Only</a>,
|
||
<a href="#pinned">Pinned</a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`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
|
||
```
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#metadata"><code>pda help meta</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### TTL
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#setting"><code>pda set</code></a>,
|
||
<a href="#metadata"><code>pda meta</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#setting"><code>pda help set</code></a>,
|
||
<a href="#metadata"><code>pda help meta</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Encryption
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#setting"><code>pda set</code></a>,
|
||
<a href="#metadata"><code>pda meta</code></a>,
|
||
<a href="#identity"><code>pda identity</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`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.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#setting"><code>pda help set</code></a>,
|
||
<a href="#metadata"><code>pda help meta</code></a>,
|
||
<a href="#identity"><code>pda help identity</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Read-Only
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#setting"><code>pda set</code></a>,
|
||
<a href="#metadata"><code>pda meta</code></a>,
|
||
<a href="#editing"><code>pda edit</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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`.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#setting"><code>pda help set</code></a>,
|
||
<a href="#metadata"><code>pda help meta</code></a>,
|
||
<a href="#editing"><code>pda help edit</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Pinned
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#setting"><code>pda set</code></a>,
|
||
<a href="#metadata"><code>pda meta</code></a>,
|
||
<a href="#listing"><code>pda list</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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
|
||
```
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#setting"><code>pda help set</code></a>,
|
||
<a href="#metadata"><code>pda help meta</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
### Stores
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#stores"><code>pda list-stores</code></a>,
|
||
<a href="#stores"><code>pda move-store</code></a>,
|
||
<a href="#stores"><code>pda remove-store</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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
|
||
```
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#stores"><code>pda help list-stores</code></a>,
|
||
<a href="#stores"><code>pda help move-store</code></a>,
|
||
<a href="#stores"><code>pda help remove-store</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Import & Export
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#import--export"><code>pda export</code></a>,
|
||
<a href="#import--export"><code>pda import</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`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.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#import--export"><code>pda help export</code></a>,
|
||
<a href="#import--export"><code>pda help import</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
### Templates
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#getting"><code>pda get</code></a>,
|
||
<a href="#running"><code>pda run</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>,
|
||
<a href="#getting"><code>pda get</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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`
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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`
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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`
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`env` reads from environment variables:
|
||
|
||
```bash
|
||
pda set my_name "{{ env "USER" }}"
|
||
|
||
❯ pda get my_name
|
||
llywelwyn
|
||
```
|
||
|
||
#### `time`
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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`
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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`
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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`
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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`
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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)
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#templates">Templates</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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`
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#getting"><code>pda get</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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 }}
|
||
```
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#getting"><code>pda help get</code></a>,
|
||
<a href="#setting"><code>pda help set</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
### Filtering
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#listing"><code>pda list</code></a>,
|
||
<a href="#removing"><code>pda remove</code></a>,
|
||
<a href="#import--export"><code>pda export</code></a>,
|
||
<a href="#import--export"><code>pda import</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`--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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#filtering">Filtering</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`*` 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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#filtering">Filtering</a>,
|
||
<a href="#listing"><code>pda list</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`--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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#filtering">Filtering</a>,
|
||
<a href="#listing"><code>pda list</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`--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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#filtering">Filtering</a>,
|
||
<a href="#listing"><code>pda list</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`--store`](#filtering) / `-s` filters by store name:
|
||
|
||
```bash
|
||
pda ls --store "prod*"
|
||
pda export --store "dev*"
|
||
```
|
||
|
||
#### Combining Filters
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#filtering">Filtering</a>
|
||
</sup>
|
||
</p>
|
||
|
||
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)
|
||
# ...
|
||
```
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#listing"><code>pda help list</code></a>,
|
||
<a href="#removing"><code>pda help remove</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
### Binary Data
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#setting"><code>pda set</code></a>,
|
||
<a href="#getting"><code>pda get</code></a>,
|
||
<a href="#listing"><code>pda list</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#setting"><code>pda help set</code></a>,
|
||
<a href="#getting"><code>pda help get</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
### Git
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#git"><code>pda init</code></a>,
|
||
<a href="#sync"><code>pda sync</code></a>,
|
||
<a href="#config">Config</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`pda!` supports automatic version control backed by Git, either in a local-only repository or by initialising from a remote.
|
||
|
||
#### Init
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#git">Git</a>,
|
||
<a href="#sync"><code>pda sync</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`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
|
||
```
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#git"><code>pda help init</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Sync
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#git">Git</a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`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.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#sync"><code>pda help sync</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Auto-Commit & Auto-Push
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#git">Git</a>,
|
||
<a href="#config">Config</a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#identity"><code>pda identity</code></a>,
|
||
<a href="#encryption">Encryption</a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`pda identity`](#identity) (alias: [`id`](#identity)) manages the age encryption identity used for [encryption](#encryption).
|
||
|
||
#### Viewing Identity
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#identity"><code>pda identity</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#identity"><code>pda identity</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#identity"><code>pda identity</code></a>,
|
||
<a href="#encryption">Encryption</a>
|
||
</sup>
|
||
</p>
|
||
|
||
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...
|
||
```
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#identity"><code>pda help identity</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
### Config
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#config"><code>pda config</code></a>,
|
||
<a href="#doctor"><code>pda doctor</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#config"><code>pda config</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`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.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#config"><code>pda help config</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
#### Example config.toml
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#config">Config</a>
|
||
</sup>
|
||
</p>
|
||
|
||
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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#config">Config</a>,
|
||
<a href="#doctor"><code>pda doctor</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
`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
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a> ·
|
||
<a href="#config">Config</a>,
|
||
<a href="#environment">Environment</a>
|
||
</sup>
|
||
</p>
|
||
|
||
[`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.
|
||
|
||
<p>
|
||
<sup>
|
||
See also:
|
||
<a href="#doctor"><code>pda help doctor</code></a>
|
||
</sup>
|
||
</p>
|
||
|
||
### Help & Version
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a>
|
||
</sup>
|
||
</p>
|
||
|
||
```bash
|
||
# help for any command
|
||
pda help set
|
||
pda help list
|
||
pda help config
|
||
|
||
# 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).
|
||
|
||
### License
|
||
|
||
<p>
|
||
<sup>
|
||
<a href="#overview">↑</a>
|
||
</sup>
|
||
</p>
|
||
|
||
MIT — see [LICENSE](LICENSE).
|