pda

build status

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

pda demo

### 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
Setting · Getting · Running · Listing · Editing · Moving & Copying · Removing · Metadata · TTL · Encryption · Read-Only · Pinned · Stores · Import & Export · Templates · Filtering · Binary Data Git · Identity · Config · Environment · Doctor · Help & Version

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

Most commands have aliases and flags. Run `pda help [command]` to see them. ### Key commands

· pda set, pda get, pda run, pda list, pda edit, pda move, pda remove

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.

See also: pda help list

#### Editing

· pda edit, pda set, pda meta

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

See also: pda help edit

#### Moving & Copying

· pda move, pda copy

[`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).

See also: pda help move, pda help copy

#### Removing

· pda remove, --key

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

See also: pda help remove

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

See also: pda help meta

#### TTL

· pda set, pda 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.

See also: pda help set, pda help meta

#### Encryption

· pda set, pda meta, pda 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.

See also: pda help set, pda help meta, pda help identity

#### Read-Only

· pda set, pda meta, pda edit

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

See also: pda help set, pda help meta, pda help edit

#### Pinned

· pda set, pda meta, pda list

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

See also: pda help set, pda help meta

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

See also: pda help list-stores, pda help move-store, pda help remove-store

#### Import & Export

· pda export, pda 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.

See also: pda help export, pda help 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, pda get

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

See also: pda help get, pda help set

### 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) # ... ```

See also: pda help list, pda help remove

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

See also: pda help set, pda help get

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

· Git, pda sync

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

See also: pda help init

#### Sync

· Git

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

See also: pda help sync

#### Auto-Commit & Auto-Push

· Git, 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 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 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 identity, Encryption

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

See also: pda help identity

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

See also: pda help config

#### Example config.toml

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

See also: pda help doctor

### Help & Version

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

MIT — see [LICENSE](LICENSE).