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. [`pda list`](#listing) outputs tabular data by default, but also supports [CSV](https://en.wikipedia.org/wiki/Comma-separated_values), [TSV](), [Markdown]() and [HTML]() tables, [JSON](), and raw NDJSON. Everything is in plaintext to make version control easy, and to avoid tying anybody to using this tool forever. Git versioning can be initiated with [`pda init`](#git), and varying levels of automation can be toggled via the [config](#config): `git.autocommit`, `git.autofetch`, and `git.autopush`. Running Git operations on every change can be slow, but a commit is fast. A happy middle-ground is enabling `git.autocommit` and doing the rest manually via [`pda sync`](#git) when changing devices. [Templates](#templates) are a primary feature, enabling values to make use of substitutions, environment variables, arbitrary shell execution, cross-references to other keys, and more.

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 · Version · Help

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

· Setting, Getting, Running, Listing, Editing, Moving and Copying, Removing

Use of `pda` revolves around creating keys with [`pda set`](#setting) and later retrieving them with [`pda get`](#getting). Keys can belong to a single store which can be set manually or left to default to the default store. Keys can be modified with [`pda edit`](#editing) and [`pda meta`](#metadata) for content or metadata editing respectively, and can be listed with [`pda list`](#listing). Keys are written as `KEY[@STORE]`. The default store can be configured with `store.default_store_name`. Keys are capable of storing any arbitrary bytes and are not limited to just text. Advanced usage of `pda` revolves around [templates](#templates) and [`pda run`](#running). #### Setting

· pda set

[`pda set`](#setting) (alias: [`s`](#setting)) creates a key-value pair, and requires a key and a value as inputs. The first argument given will always be used to determine the key (and store). Values can come from arguments, stdin, or a file. ```bash # create a key-value pair pda set name "Alice" # create a key-value pair in the "Favourites" store pda set movie@favourites "The Road" # create a key-value pair with piped input echo "Bob" | pda set name # create a key-value pair with redirection pda set example < silmarillion.txt # create a pinned key-value pair from a file pda set --pin example --file example.md # create an encrypted key-value pair, expiring in one day pda set secret "Secret data." --encrypt --ttl 24h ``` The `interactive` and `safe` flags exist to prevent accidentally overwriting an existing key when creating a new one. These flags exist on all writable commands. ```bash # prevent ever overwriting an existing key pda set name "Bob" --safe # guarantee a prompt when overwriting an existing key pda set name "Joe" --interactive ``` Making a key `readonly` will also prevent unintended changes. It prevents making any changes unless `force` is passed or the key is made writable once again with [`pda edit`](#editing) or [`pda meta`](#metadata). ```bash # create a readonly key-value pair pda set repo "https://github.com/llywelwyn/pda" --readonly # force-overwrite a readonly key-value pair pda set dog "A four-legged mammal that isn't a cat." --force ``` #### Getting

· Templates · pda get

[`pda get`](#getting) (alias: [`g`](#getting)) can be used to retrieve a key's value, and takes one argument: the desired key. The value is output to stdout. [Templates](#templates) are evaluated at retrieval time unless opted-out via the `no-template` flag. ```bash # get the value of a key ❯ pda get name Alice ``` As mentioned in [setting](#setting), values support any arbitrary bytes. Values which are not valid UTF8 are retrieved slightly differently. Printing raw bytes directly in the terminal can (and will) cause [undefined behaviour](https://en.wikipedia.org/wiki/Undefined_behavior), so if a TTY is detected then a raw `pda get` will return instead some metadata about the contents of the bytes. In a non-TTY setting (when the data is piped or redirected), the raw bytes will be returned as expected. If a representation of the bytes in a TTY is desired, the `base64` flag provides a safe way to view them. ```bash # get the information of a non-UTF8 key ❯ pda get cat_gif (size: 101.2k, image/gif) # get the raw bytes of a non-UTF8 key via pipe pda get cat_gif | xdg-open # get the raw bytes of a non-UTF8 key via redirect pda get cat_gif > cat.gif # get the base64 representation of a non-UTF8 key ❯ pda get cat_gif --base64 R0lGODlhXANYAvf/MQAAAAEBAQICAgMDAwQEBAUFBQYGBggI... ``` The existence of a key can be checked with `exists`. It returns a `0 exit code` on an existent key, or a `1 exit code` on a non-existent one. This is primarily useful for scripting. ```bash # check if an existent key exists ❯ pda get name --exists exit code 0 # check if a non-existent key exists ❯ pda get nlammfd --exists exit code 1 ``` Running [`pda get`](#getting) will resolve templates in the stored key at run-time. This can be prevented with the `no-template` flag. ```bash # set key "user" to a template of the USER environment variable ❯ pda set user "{{ env "USER" }}" # get a templated key ❯ pda get user lew # get a templated key without resolving the template ❯ pda get user --no-template {{ env "USER" }} ``` #### Running

· pda get --run · pda run

[`pda run`](#running) retrieves a key and executes it as a shell command. It uses the shell set in `$SHELL. If, somehow, this environment variable is unset, it falls back and attempts to use `/bin/sh`. Running takes one argument: the key. [`pda run`](#running) and [`pda get --run`](#getting) are functionally equivalent. ```bash # create a key containg a script ❯ pda set my_script "echo Hello, world." # get and run a key using $SHELL ❯ pda get my_script --run Hello, world. ❯ pda run my_script Hello, world. ``` [Templates](#templates) are fully resolved before any shell execution happens. ```bash # create a key containing a script, and a template ❯ pda set greet 'echo "Hello, {{ default "Jane Doe" .NAME }}"' # run the key directly in $SHELL ❯ pda run greet Hello, Jane Doe # run the key, setting NAME to "Alice" ❯ pda run greet NAME="Alice" Hello, Alice ``` #### Listing

· pda list · Filtering

[`pda list`](#listing) (alias: [`ls`](#listing)) displays your key-value pairs. The default columns are `meta,size,ttl,store,key,value`. Meta is a 4-char flag string: `(e)ncrypted (w)ritable (t)tl (p)inned`, or a dash for an unset flag. -b, --base64 view binary data as base64 -c, --count print only the count of matching entries [`pda list`](#listing) (alias: [`ls`](#listing)) displays stored key-value pairs with [pinned](#pinning) keys first, followed by alphabetical order. Default behaviour is to list [metadata](#metadata), size, [time-to-live](#ttl), store, the key, and value as a table. The order and visibility of every column can be toggled either via `list.default_columns` in the [config](#config) or via one-off flags. It accepts one or zero arguments. If no argument is passed, with a default configuration [`pda list`](#listing) will display all keys from all stores, but this behaviour can be toggled to instead display only keys from the default store. If a store name is passed as an argument, only the keys contained within that store will be listed. If `list.always_show_all_stores` is toggled off, the behaviour can be enabled on an individual basis by passing `all`. ```bash # list all store contents ❯ pda list Meta Size TTL Store Key Value -w-p 5 - todos todo don't forget this ---- 23 - store url https://example.com -w-- 5 - me name Alice # list only the contents of the "todos" store ❯ pda list todos Meta Size TTL Store Key Value -w-p 5 - todos todo don't forget this # list all store contents, but without Meta, Size, or TTL ❯ pda list --no-ttl --no-header --no-size Store Key Value todos todo don't forget this store url https://example.com me name Alice # count the number of entries for a given query ❯ pda list --count 3 ``` When [listing](#listing), [glob patterns](#filtering) can be used to further filter by `key`, `store`, or `value`. ```bash # list all store contents beginning with "https" pda ls --value "https**" # list all store contents beginning with "https" with "db" in the key pda ls --value "https**" --key "**db**" ``` The standard tabular output of [`pda list`](#listing) can be swapped out for tab-separated values, comma-separated values, a markdown table, a HTML table, newline-delimited JSON, or JSON. Newline-delimited JSON is the native storage format of a `pda` store. ```bash # list all store contents as comma-separated values ❯ pda ls --format csv Meta,Size,TTL,Store,Key,Value -w--,5,-,store,name,Alice # list all store contents as JSON ❯ pda ls --format json [{"key":"name","value":"Alice","encoding":"text","store":"store"}] ``` By default, long values are truncated to fit the terminal, but this behaviour can be toggled via the [config](#config) or by passing `full`. The setting is `list.always_show_full_values`. ```bash ❯ pda ls Key Value note this is a very long (..30 more chars) ❯ pda ls --full Key Value note this is a very long value that keeps on going and going ``` As with [`getting`](#getting), non-UTF8 data in lists will be substituted for a summary rather than displaying raw bytes. This can be changed out for a base64 representation by passing `base64`. #### Editing

· pda edit

[`pda edit`](#editing) (alias: [`e`](#editing)) opens a key's value in your `$EDITOR`. If the key doesn't exist, an empty file is opened, and saving non-empty content creates it. When [editing](#editing) a key, [metadata](#metadata)-altering flags can be passed in the same operation. These alterations will only take place if the edit is finalised by saving. ```bash # edit an existing key or create a new one pda edit name # edit a key and pin it pda edit name --pin # edit a key and give it an expiration, and encrypt it pda edit secret_stuff --ttl 1h --encrypt # edit a key and make it readonly pda edit do_not_change --readonly ``` Most `$EDITOR` will add a trailing newline on saving a file. These are stripped by default, but this behaviour can be toggled via `edit.always_preserve_newline` in the [config](#config) or as a one-off by passing `preserve-newline`. ```bash # edit a key and preserve the automatic $EDITOR newline pda edit example --preserve-newline ``` Binary values will be presented as base64 for editing and will be decoded back to raw bytes on save. [Read-only](#read-only) keys require being made writable before they can be edited, or `force` can be explicitly passed. #### Moving & Copying

· pda move, pda copy

To move (or rename) a key, [`pda move`](#moving--copying) (alias: [`mv`](#moving--copying)) can be used. To copy a key, [`pda move --copy`](#moving--copying) (alias: [`cp`](#moving--copying)) can be used. With both of these operations, all [metadata](#metadata) is preserved. ```bash # rename a key ❯ pda move name name2 ok renamed name to name2 # move a key across stores ❯ pda mv name name@some_other_store ok renamed name to name@some_other_store # copy a key ❯ pda cp name name2 ok copied name to name2 ``` Accidental overwrites have a few ways of being prevented. `safe` exists for moving and copying as it does for all changeful commands, skipping an operation if a destination already exists that would be overwritten. A [read-only](#read-only) key will also prevent being moved or overwritten unless `force` is explicitly passed. Additionally, `interactive` being passed or `key.always_prompt_overwrite` being enabled in the [config](#config) will cause a yes-no prompt to be presented if a key is going to be overwritten. Inversely, prompts can always be skipped by passing `yes`. ```bash # move a key safely ❯ pda mv name name2 --safe # info skipped 'name2': already exists # move a key interactively ❯ pda mv name name2 --safe ??? overwrite 'name2'? (y/n) >>> y # move a key and skip all warning prompts ❯ pda mv name name2 --yes ok renamed name to name2 ``` #### Removing

· pda help remove

[`pda remove`](#removing) (alias: [`rm`](#removing)) deletes one or more keys. ```bash pda rm kitty ``` Remove multiple keys at once: ```bash pda rm kitty dog@animals ``` Mix exact keys with [glob patterns](#filtering) using [`--key`](#removing): ```bash pda set cog "cogs" pda set dog "doggy" pda set kitty "cat" pda rm kitty --key "?og" ``` Filter by store with [`--store`](#removing) / `-s` and by value with [`--value`](#removing) / `-v`: ```bash pda rm --store "temp*" --key "session*" ``` [`--interactive`](#removing) / `-i` prompts before each deletion (or set `key.always_prompt_delete` in [config](#config)): ```bash pda rm kitty -i # ??? remove 'kitty'? (y/n) # ==> y ``` Glob-matched deletions prompt by default (configurable with `key.always_prompt_glob_delete`). [`--yes`](#removing) / `-y` auto-accepts all confirmation prompts: ```bash pda rm kitty -y ``` [Read-only](#read-only) keys can't be deleted without [`--force`](#removing): ```bash ❯ pda rm protected-key FAIL cannot remove 'protected-key': key is read-only pda rm protected-key --force ``` ### Metadata

· pda meta, TTL, Encryption, Read-Only, Pinned

[`pda meta`](#metadata) views or modifies metadata for a key without changing its value. With no flags, it displays the key's current metadata: ```bash ❯ pda meta session key: session@store secret: false writable: true pinned: false expires: 59m30s ``` Pass flags to modify: [`--ttl`](#ttl), [`--encrypt`](#encryption) / [`--decrypt`](#encryption), [`--readonly`](#read-only) / [`--writable`](#read-only), [`--pin`](#pinned) / [`--unpin`](#pinned). Multiple metadata changes can be combined in one call: ```bash pda meta session --ttl 2h --encrypt --pin ``` Modifying a [read-only](#read-only) key's metadata requires [`--force`](#metadata) (except for toggling the read-only flag itself, and pin/unpin): ```bash ❯ pda meta api-url --ttl 1h FAIL cannot meta 'api-url': key is read-only pda meta api-url --ttl 1h --force ``` #### TTL

· pda help set, pda help meta

Keys can be given an expiration time. Expired keys are marked for garbage collection and deleted on the next access to the store. Set a TTL at creation time with [`pda set --ttl`](#setting): ```bash # expire after 1 hour pda set session "123" --ttl 1h # expire after 54 minutes and 10 seconds pda set session2 "xyz" --ttl 54m10s ``` [`pda list`](#listing) shows expiration in the TTL column: ```bash ❯ pda ls TTL Key Value 59m30s session 123 51m40s session2 xyz ``` Change or clear the TTL on an existing key with [`pda meta --ttl`](#metadata): ```bash ❯ pda meta session --ttl 2h ok set ttl to 2h session ❯ pda meta session --ttl never ok cleared ttl session ``` The [`edit`](#editing) command also accepts `--ttl`: ```bash pda edit session --ttl 30m ``` [`export`](#import--export) and [`import`](#import--export) preserve the expiry date. Expirations are stored as a timestamp, not a timer — they continue ticking down regardless of whether the key is in an active store or sitting in a backup file. #### Encryption

· pda help set, pda help meta, pda help identity

[`pda set --encrypt`](#setting) encrypts values at rest using [age](https://github.com/FiloSottile/age). Values are stored on disk as age ciphertext and decrypted automatically by commands like [`get`](#getting) and [`list`](#listing) when the correct identity file is present. An X25519 identity is generated on first use. ```bash pda set --encrypt api-key "sk-live-abc123" # ok created identity at ~/.config/pda/identity.txt pda set --encrypt token "ghp_xxxx" ``` [`pda get`](#getting) decrypts automatically: ```bash ❯ pda get api-key sk-live-abc123 ``` Toggle encryption on an existing key with [`pda meta`](#metadata): ```bash ❯ pda meta api-key --encrypt ok encrypted api-key ❯ pda meta api-key --decrypt ok decrypted api-key ``` The on-disk value is ciphertext, so encrypted entries are safe to commit and push with [Git](#git): ```bash ❯ pda export {"key":"api-key","value":"YWdlLWVuY3J5cHRpb24u...","encoding":"secret"} ``` [`mv`](#moving--copying), [`cp`](#moving--copying), and [`import`](#import--export) all preserve encryption, read-only, and pinned flags. Overwriting an encrypted key without `--encrypt` will warn you: ```bash pda cp api-key api-key-backup # still encrypted ❯ pda set api-key "oops" WARN overwriting encrypted key 'api-key' as plaintext hint pass --encrypt to keep it encrypted ``` If the identity file is missing, encrypted values are inaccessible but not lost. Keys remain visible, and the ciphertext is preserved through reads and writes: ```bash ❯ pda ls Meta Key Value ew-- api-key locked (identity file missing) ❯ pda get api-key FAIL cannot get 'api-key': secret is locked (identity file missing) ``` All encryption operations can be set as default with `key.always_encrypt` in [config](#config), so every [`pda set`](#setting) automatically encrypts. #### Read-Only

· pda help set, pda help meta

Keys marked read-only are protected from accidental modification. You can modify a read-only key again by making it [`--writable`](#metadata) or by explicitly bypassing with [`--force`](#metadata). Set a key as read-only at creation time: ```bash pda set api-url "https://prod.example.com" --readonly ``` Toggle with [`pda meta`](#metadata): ```bash ❯ pda meta api-url --readonly ok made readonly api-url ❯ pda meta api-url --writable ok made writable api-url ``` Or alongside an edit: ```bash pda edit notes --readonly ``` Read-only keys are protected from [`set`](#setting), [`rm`](#removing), [`mv`](#moving--copying), and [`edit`](#editing). Use `--force` to bypass: ```bash ❯ pda set api-url "new value" FAIL cannot set 'api-url': key is read-only pda set api-url "new value" --force pda rm api-url --force pda mv api-url new-name --force ``` Modifying a read-only key's metadata also requires `--force` (except for toggling the read-only flag itself, and pin/unpin): ```bash ❯ pda meta api-url --ttl 1h FAIL cannot meta 'api-url': key is read-only pda meta api-url --ttl 1h --force ``` [`cp`](#moving--copying) can copy a read-only key freely (since the source isn't modified), and the copy preserves the read-only flag. Overwriting a read-only destination is blocked without `--force`. #### Pinned

· pda help set, pda help meta

Pinned keys sort to the top of [`pda list`](#listing) output, preserving alphabetical order within the pinned and unpinned groups. Pin a key at creation time: ```bash pda set important "remember this" --pin ``` Toggle with [`pda meta`](#metadata): ```bash ❯ pda meta todo --pin ok pinned todo ❯ pda meta todo --unpin ok unpinned todo ``` ```bash ❯ pda ls Meta Key Value -w-p important remember this -w-- name Alice -w-- other foo ``` ### Stores

· pda list-stores, pda move-store, pda remove-store

You can have as many stores as you want. Stores are created implicitly when you set a key with a `@STORE` suffix. Each store is a separate NDJSON file on disk. [`pda list-stores`](#stores) (alias: [`lss`](#stores)) shows all stores with key counts and file sizes: ```bash ❯ pda list-stores Keys Size Store 2 1.8k @birthdays 12 4.2k @store ``` [`--short`](#stores) prints only the names: ```bash ❯ pda list-stores --short @birthdays @store ``` Save to a specific store with the `@STORE` syntax: ```bash pda set alice@birthdays "11/11/1998" ``` List a specific store: ```bash ❯ pda ls @birthdays Store Key Value birthdays alice 11/11/1998 birthdays bob 05/12/1980 ``` [`pda move-store`](#stores) (alias: [`mvs`](#stores)) renames a store: ```bash pda move-store birthdays bdays ``` Copy a store with `--copy`: ```bash pda move-store birthdays bdays --copy ``` [`--safe`](#stores) skips if the destination already exists: ```bash pda move-store birthdays bdays --safe ``` [`pda remove-store`](#stores) (alias: [`rms`](#stores)) deletes a store: ```bash pda remove-store birthdays ``` [`--yes`](#stores) / `-y` skips confirmation prompts: ```bash pda remove-store birthdays -y ``` #### Import & Export

· pda help export, pda help import

[`pda export`](#import--export) exports everything as NDJSON (it's an alias for `list --format ndjson`): ```bash pda export > my_backup ``` Filter exports with [`--key`](#filtering), [`--value`](#filtering), and [`--store`](#filtering): ```bash # export only matching keys pda export --key "a*" # export only entries whose values contain a URL pda export --value "**https**" ``` [`pda import`](#import--export) restores entries from an NDJSON dump. By default, each entry is routed to the store it came from (via the `"store"` field in the NDJSON). If no `"store"` field is present, entries go to `store.default_store_name`. ```bash # entries are routed to their original stores pda import -f my_backup # ok restored 5 entries ``` Pass a store name as a positional argument to force all entries into one store: ```bash pda import mystore -f my_backup # ok restored 5 entries into @mystore ``` Read from stdin: ```bash pda import < my_backup ``` Filter imports with [`--key`](#filtering) and [`--store`](#filtering): ```bash # import only matching keys pda import --key "a*" -f my_backup # import only entries from matching stores pda import --store "prod*" -f my_backup ``` [`--drop`](#import--export) does a full replace — drops all existing entries before importing: ```bash pda import --drop -f my_backup ``` [`--interactive`](#import--export) / `-i` prompts before overwriting existing keys. [`export`](#import--export) encodes [binary data](#binary-data) as base64. [Encryption](#encryption), [read-only](#read-only), [pinned](#pinned) flags, and [TTL](#ttl) are all preserved through export and import. ### Templates

· pda get, pda run

Values support Go's [`text/template`](https://pkg.go.dev/text/template) syntax. Templates are evaluated on [`pda get`](#getting) and [`pda run`](#running). `text/template` is a Turing-complete templating library that supports pipelines, nested templates, conditionals, loops, and more. Actions are given with `{{ action }}` syntax. To fit `text/template` into a CLI key-value tool, `pda!` adds a small set of built-in functions on top of the standard library. These same functions are also available in `git.default_commit_message` templates, along with `summary` which returns the action that triggered the commit (e.g. "set foo", "removed bar"). #### Basic Substitution

· Templates

Template variables are substituted from `KEY=VALUE` arguments passed to [`pda get`](#getting): ```bash pda set greeting "Hello, {{ .NAME }}" ❯ pda get greeting NAME="Alice" Hello, Alice ``` #### `default`

· Templates

`default` sets a fallback value when a variable is missing or empty: ```bash pda set greeting "Hello, {{ default "World" .NAME }}" ❯ pda get greeting Hello, World ❯ pda get greeting NAME="Bob" Hello, Bob ``` #### `require`

· Templates

`require` errors if the variable is missing or empty: ```bash pda set file "{{ require .FILE }}" ❯ pda get file FAIL cannot get 'file': ...required value is missing or empty ``` #### `env`

· Templates

`env` reads from environment variables: ```bash pda set my_name "{{ env "USER" }}" ❯ pda get my_name llywelwyn ``` #### `time`

· Templates

`time` returns the current UTC time in RFC3339 format: ```bash pda set note "Created at {{ time }}" ❯ pda get note Created at 2025-01-15T12:00:00Z ``` #### `enum`

· Templates

`enum` restricts a variable to a set of acceptable values: ```bash pda set level "Log level: {{ enum .LEVEL "info" "warn" "error" }}" ❯ pda get level LEVEL=info Log level: info ❯ pda get level LEVEL=debug FAIL cannot get 'level': ...invalid value 'debug', allowed: [info warn error] ``` #### `int`

· Templates

`int` parses a variable as an integer, useful for loops and arithmetic: ```bash pda set number "{{ int .N }}" ❯ pda get number N=3 3 ``` Use it in a range loop: ```bash pda set meows "{{ range int .COUNT }}meow! {{ end }}" ❯ pda get meows COUNT=4 meow! meow! meow! meow! ``` #### `list`

· Templates

`list` parses a comma-separated string into a list for iteration: ```bash pda set names "{{ range list .NAMES }}Hi {{.}}. {{ end }}" ❯ pda get names NAMES=Bob,Alice Hi Bob. Hi Alice. ``` #### `shell`

· Templates

`shell` executes a command and returns its stdout: ```bash pda set rev '{{ shell "git rev-parse --short HEAD" }}' ❯ pda get rev a1b2c3d ``` ```bash pda set today '{{ shell "date +%Y-%m-%d" }}' ❯ pda get today 2025-06-15 ``` #### `pda` (Recursive)

· Templates

`pda` gets another key's value, enabling recursive composition: ```bash pda set base_url "https://api.example.com" pda set endpoint '{{ pda "base_url" }}/users/{{ require .ID }}' ❯ pda get endpoint ID=42 https://api.example.com/users/42 ``` Cross-store references work too: ```bash pda set host@urls "https://example.com" pda set api '{{ pda "host@urls" }}/api' ❯ pda get api https://example.com/api ``` #### `no-template`

· pda help get

Pass [`--no-template`](#getting) to [`pda get`](#getting) to output the raw value without evaluating templates: ```bash pda set hello "{{ if .MORNING }}Good morning.{{ end }}" ❯ pda get hello MORNING=1 Good morning. ❯ pda get hello --no-template {{ if .MORNING }}Good morning.{{ end }} ``` ### Filtering

· pda list, pda remove, pda export, pda import

[`--key`](#filtering) / `-k`, [`--value`](#filtering) / `-v`, and [`--store`](#filtering) / `-s` filter entries with glob support. All three flags are repeatable, with results matching one-or-more of the patterns per flag. When multiple flags are combined, results must satisfy all of them (AND across flags, OR within the same flag). These filters work with [`list`](#listing), [`export`](#import--export), [`import`](#import--export), and [`remove`](#removing). [`--value`](#filtering) is not available on [`import`](#import--export) or [`remove`](#removing). [`gobwas/glob`](https://github.com/gobwas/glob) is used for matching. The default separators are `/-_.@:` and space. #### Glob Patterns

· Filtering

`*` wildcards a word or series of characters, stopping at separator boundaries: ```bash ❯ pda ls cat dog cog mouse hotdog mouse house foo.bar.baz pda ls --key "*" # cat, dog, cog (single-segment keys only) pda ls --key "* *" # mouse hotdog, mouse house pda ls --key "foo.*.baz" # foo.bar.baz ``` `**` super-wildcards ignore word boundaries: ```bash pda ls --key "foo**" # foo.bar.baz pda ls --key "**g" # dog, cog, mouse hotdog ``` `?` matches a single character: ```bash pda ls --key "?og" # dog, cog ``` `[abc]` matches one of the characters in the brackets: ```bash pda ls --key "[dc]og" # dog, cog # negate with '!' pda ls --key "[!dc]og" # bog (if it exists) ``` `[a-c]` matches a range: ```bash pda ls --key "[a-g]ag" # bag, gag pda ls --key "[!a-g]ag" # wag ``` #### Filtering by Key

· Filtering, pda list

[`--key`](#filtering) / `-k` filters entries by key name: ```bash pda ls --key "db*" pda ls --key "session*" --key "token*" ``` Multiple `--key` patterns are OR'd — an entry matches if it matches any of them. #### Filtering by Value

· Filtering, pda list

[`--value`](#filtering) / `-v` filters by value content using the same glob syntax: ```bash ❯ pda ls --value "**localhost**" Key Value db-url postgres://localhost:5432 ``` Multiple `--value` patterns are OR'd: ```bash ❯ pda ls --value "**world**" --value "42" Key Value greeting hello world number 42 ``` Locked (encrypted without an available identity) and non-UTF-8 (binary) entries are silently excluded from `--value` matching. #### Filtering by Store

· Filtering, pda list

[`--store`](#filtering) / `-s` filters by store name: ```bash pda ls --store "prod*" pda export --store "dev*" ``` #### Combining Filters

· Filtering

Combine key, value, and store filters. Results must match all flags (AND), with OR within each flag: ```bash pda ls --key "db*" --value "**localhost**" ``` Globs can be arbitrarily complex, and [`--key`](#filtering) can be combined with exact positional args on [`rm`](#removing): ```bash pda rm cat --key "{mouse,[cd]og}**" # ??? remove 'cat'? (y/n) # ==> y # ??? remove 'mouse trap'? (y/n) # ... ``` ### Binary Data

· pda set, pda get, pda list

`pda!` supports all binary data. Save it with [`pda set`](#setting): ```bash pda set logo < logo.png pda set logo -f logo.png ``` And retrieve it with [`pda get`](#getting): ```bash pda get logo > output.png ``` On a TTY, [`get`](#getting) and [`list`](#listing) show a summary for binary data. If piped or run outside of a TTY, raw bytes are output: ```bash ❯ pda get logo (binary: 4.2 KB, image/png) ``` [`--base64`](#getting) / `-b` views binary data as base64: ```bash ❯ pda get logo --base64 iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVQI12... ``` [`pda export`](#import--export) encodes binary data as base64 in the NDJSON: ```bash ❯ pda export {"key":"logo","value":"89504E470D0A1A0A0000000D4948445200000001000000010802000000","encoding":"base64"} ``` [`pda edit`](#editing) presents binary values as base64 for editing and decodes them back on save. ### Git

· pda init, pda sync, Config

`pda!` supports automatic version control backed by Git, either in a local-only repository or by initialising from a remote. #### Init

· pda help init

[`pda init`](#git) initialises version control: ```bash # initialise an empty repository pda init # or clone an existing one pda init https://github.com/llywelwyn/my-repository ``` [`--clean`](#git) removes the existing `.git` directory first, useful for reinitialising or switching remotes: ```bash pda init --clean pda init https://github.com/llywelwyn/my-repository --clean ``` #### Sync

· pda help sync

[`pda sync`](#sync) conducts a best-effort sync of your local data with your Git repository. Any time you swap machine or know you've made changes outside of `pda!`, syncing is recommended. If you're ahead, syncing will commit and push. If you're behind, syncing will detect this and prompt you: either stash local changes and pull, or abort and fix manually. ```bash # sync with Git pda sync # with a custom commit message pda sync -m "added production credentials" ``` Running [`pda sync`](#sync) manually will always fetch, commit, and push — or stash and pull if behind — regardless of config. #### Auto-Commit & Auto-Push

· pda help config

`pda!` supports automation via its [config](#config). There are options for `git.auto_commit`, `git.auto_fetch`, and `git.auto_push`. **`git.auto_commit`** commits changes immediately to the local Git repository any time data is changed. **`git.auto_fetch`** fetches before committing any changes. This incurs a noticeable slowdown due to network round-trips. **`git.auto_push`** automatically pushes committed changes to the remote repository, if one is configured. If `auto_commit` is false, `auto_fetch` and `auto_push` have no effect. They are additional steps in the commit process. A recommended setup is to enable `git.auto_commit` and run [`pda sync`](#sync) manually when switching machines. ### Identity

· pda identity, Encryption

[`pda identity`](#identity) (alias: [`id`](#identity)) manages the age encryption identity used for [encryption](#encryption). #### Viewing Identity

· pda help identity

With no flags, [`pda identity`](#identity) shows your public key, identity file path, and any additional recipients: ```bash ❯ pda identity ok pubkey age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p ok identity ~/.config/pda/identity.txt ``` [`--path`](#identity) prints only the identity file path: ```bash ❯ pda identity --path ~/.config/pda/identity.txt ``` #### Creating an Identity

· pda help identity

An identity is generated automatically the first time you use [`--encrypt`](#encryption). To create one manually: ```bash pda identity --new ``` [`--new`](#identity) errors if an identity already exists. Delete the file manually to replace it. #### Recipients

· pda help identity

By default, secrets are encrypted only for your own identity. To encrypt for additional recipients (e.g. a teammate or another device), use [`--add-recipient`](#identity) with their age public key. All existing secrets are automatically re-encrypted for every recipient: ```bash ❯ pda identity --add-recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p ok re-encrypted api-key ok added recipient age1ql3z... ok re-encrypted 1 secret(s) ``` Remove a recipient with [`--remove-recipient`](#identity). Secrets are re-encrypted without their key: ```bash pda identity --remove-recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p ``` Additional recipients are shown in the default identity display: ```bash ❯ pda identity ok pubkey age1abc... ok identity ~/.local/share/pda/identity.txt ok recipient age1ql3z... ``` ### Config

· pda config, pda doctor

Config is stored at `~/.config/pda/config.toml` (Linux/macOS) or `%LOCALAPPDATA%/pda/config.toml` (Windows). All values have sensible defaults, so a config file is entirely optional. #### Config Commands

· pda help config

[`pda config`](#config) manages configuration without editing files by hand: ```bash # list all config values and their current settings pda config list # get a single value ❯ pda config get git.auto_commit false # set a value (validated before saving) pda config set git.auto_commit true # open in $EDITOR (validated on save) pda config edit # print the config file path pda config path # generate a fresh default config file pda config init # overwrite an existing config with defaults pda config init --new # update config: migrate deprecated keys and fill missing defaults pda config init --update ``` [`pda doctor`](#doctor) will warn about unrecognised keys (typos, removed options) and show any non-default values, so it doubles as a config audit. #### Example config.toml

· Config, pda help config

All values below are the defaults. A missing config file or missing keys will use these values. ```toml # display ascii header in long root and version commands display_ascii_art = true [key] # prompt y/n before deleting keys always_prompt_delete = false # prompt y/n before deleting with a glob match always_prompt_glob_delete = true # prompt y/n before key overwrites always_prompt_overwrite = false # encrypt all values at rest by default always_encrypt = false [store] # store name used when none is specified default_store_name = "store" # prompt y/n before deleting whole store always_prompt_delete = true # prompt y/n before store overwrites always_prompt_overwrite = true [list] # list all, or list only the default store when none specified always_show_all_stores = true # default output, accepts: table|tsv|csv|markdown|html|ndjson|json default_list_format = "table" # show full values without truncation always_show_full_values = false # suppress the header row always_hide_header = false # columns and order, accepts: meta,size,ttl,store,key,value default_columns = "meta,size,ttl,store,key,value" [git] # auto fetch whenever a change happens auto_fetch = false # auto commit any changes auto_commit = false # auto push after committing auto_push = false # commit message if none manually specified # supports templates, see: #templates section default_commit_message = "{{ summary }} {{ time }}" ``` ### Environment

· Config, pda doctor

`PDA_CONFIG` overrides the config directory. `pda!` will look for `config.toml` in this directory: ```bash PDA_CONFIG=/tmp/config/ pda set key value ``` `PDA_DATA` overrides the data storage directory: ```bash PDA_DATA=/tmp/stores pda set key value ``` Default data locations: - Linux: `~/.local/share/pda/` - macOS: `~/Library/Application Support/pda/` - Windows: `%LOCALAPPDATA%/pda/` `EDITOR` is used by [`pda edit`](#editing) and [`pda config edit`](#config) to open values in a text editor. Must be set for these commands to work: ```bash EDITOR=nvim pda edit mykey ``` `SHELL` is used by [`pda run`](#running) (or [`pda get --run`](#getting)) for command execution. Falls back to `/bin/sh` if unset: ```bash pda run script ``` ### Doctor

· Config, Environment

[`pda doctor`](#doctor) runs a set of health checks of your environment: ```bash ❯ pda doctor ok pda! 2025.52 Christmas release (linux/amd64) ok OS: Linux 6.18.7-arch1-1 ok Go: go1.23.0 ok Git: 2.45.0 ok Shell: /bin/zsh ok Config: /home/user/.config/pda ok Non-default config: ├── display_ascii_art: false └── git.auto_commit: true ok Data: /home/user/.local/share/pda ok Identity: /home/user/.config/pda/identity.txt ok Git initialised on main ok Git remote configured ok Git in sync with remote ok 3 store(s), 15 key(s), 2 secret(s), 4.2k total size ok No issues found ``` Severity levels are colour-coded: `ok` (green), `WARN` (yellow), and `FAIL` (red). Only `FAIL` produces a non-zero exit code. `WARN` is generally not a problem, but may mean some functionality isn't being made use of, like version control not having been initialised yet. ### Version

· pda help version

```bash # display the full version output pda version # or just the release ❯ pda version --short pda! 2025.52 Christmas release ``` `pda!` uses calendar versioning: `YYYY.WW`. ASCII art can be permanently disabled with `display_ascii_art = false` in [config](#config). ### Help

set · get · run · list · edit · move · copy · remove · meta · identity · export · import · list-stores · move-store · remove-store · init · sync · git · config · doctor · version
#### `pda set`

· See also: Setting

```text Set a key to a given value or stdin. Optionally specify a store. Pass --encrypt to encrypt the value at rest using age. An identity file is generated automatically on first use. PDA supports parsing Go templates. Actions are delimited with {{ }}. For example: 'Hello, {{ .NAME }}' can be substituted with NAME="John Doe". 'Hello, {{ env "USER" }}' will fetch the USER env variable. 'Hello, {{ default "World" .NAME }}' will default to World if NAME is blank. 'Hello, {{ require .NAME }}' will error if NAME is blank. '{{ enum .NAME "Alice" "Bob" }}' allows only NAME=Alice or NAME=Bob. Usage: pda set KEY[@STORE] [VALUE] [flags] Aliases: set, s Flags: -e, --encrypt encrypt the value at rest using age -f, --file string read value from a file --force bypass read-only protection -h, --help help for set -i, --interactive prompt before overwriting an existing key --pin pin the key (sorts to top in list) --readonly mark the key as read-only --safe do not overwrite if the key already exists -t, --ttl duration expire the key after the provided duration (e.g. 24h, 30m) ``` #### `pda get`

· See also: Getting, Templates

```text Get the value of a key. Optionally specify a store. {{ .TEMPLATES }} can be filled by passing TEMPLATE=VALUE as an additional argument after the initial KEY being fetched. For example: pda set greeting 'Hello, {{ .NAME }}!' pda get greeting NAME=World Usage: pda get KEY[@STORE] [flags] Aliases: get, g Flags: -b, --base64 view binary data as base64 --exists exit 0 if the key exists, exit 1 if not (no output) -h, --help help for get --no-template directly output template syntax -c, --run execute the result as a shell command ``` #### `pda run`

· See also: Running, Templates

```text Get the value of a key and execute it as a shell command. Optionally specify a store. {{ .TEMPLATES }} can be filled by passing TEMPLATE=VALUE as an additional argument after the initial KEY being fetched. For example: pda set greeting 'Hello, {{ .NAME }}!' pda run greeting NAME=World Usage: pda run KEY[@STORE] [flags] Flags: -b, --base64 view binary data as base64 -h, --help help for run --no-template directly output template syntax ``` #### `pda list`

· See also: Listing, Filtering

```text List the contents of all stores. By default, list shows entries from every store. Pass a store name as a positional argument to narrow to a single store, or use --store/-s with a glob pattern to filter by store name. Use --key/-k and --value/-v to filter by key or value glob, and --store/-s to filter by store name. All filters are repeatable and OR'd within the same flag. Usage: pda list [STORE] [flags] Aliases: list, ls Flags: -a, --all list across all stores -b, --base64 view binary data as base64 -c, --count print only the count of matching entries -o, --format format output format (table|tsv|csv|markdown|html|ndjson|json) -f, --full show full values without truncation -h, --help help for list -k, --key strings filter keys with glob pattern (repeatable) --no-header suppress the header row --no-keys suppress the key column --no-meta suppress the meta column --no-size suppress the size column --no-store suppress the store column --no-ttl suppress the TTL column --no-values suppress the value column -s, --store strings filter stores with glob pattern (repeatable) -v, --value strings filter values with glob pattern (repeatable) ``` #### `pda edit`

· See also: Editing

```text Open a key's value in $EDITOR. If the key doesn't exist, opens an empty file — saving non-empty content creates the key. Binary values are presented as base64 for editing and decoded back on save. Metadata flags (--ttl, --encrypt, --decrypt) can be passed alongside the edit to modify metadata in the same operation. Usage: pda edit KEY[@STORE] [flags] Aliases: edit, e Flags: -d, --decrypt decrypt the value (store as plaintext) -e, --encrypt encrypt the value at rest --force bypass read-only protection -h, --help help for edit --pin pin the key (sorts to top in list) --preserve-newline keep trailing newlines added by the editor --readonly mark the key as read-only --ttl string set expiry (e.g. 30m, 2h) or 'never' to clear --unpin unpin the key --writable clear the read-only flag ``` #### `pda move`

· See also: Moving & Copying

```text Move a key Usage: pda move FROM[@STORE] TO[@STORE] [flags] Aliases: move, mv Flags: --copy copy instead of move (keeps source) --force bypass read-only protection -h, --help help for move -i, --interactive prompt before overwriting destination --safe do not overwrite if the destination already exists -y, --yes skip all confirmation prompts ``` #### `pda copy`

· See also: Moving & Copying

```text Make a copy of a key Usage: pda copy FROM[@STORE] TO[@STORE] [flags] Aliases: copy, cp Flags: --force bypass read-only protection -h, --help help for copy -i, --interactive prompt before overwriting destination --safe do not overwrite if the destination already exists -y, --yes skip all confirmation prompts ``` #### `pda remove`

· See also: Removing, Filtering

```text Delete one or more keys Usage: pda remove KEY[@STORE] [KEY[@STORE] ...] [flags] Aliases: remove, rm Flags: --force bypass read-only protection -h, --help help for remove -i, --interactive prompt yes/no for each deletion -k, --key strings delete keys matching glob pattern (repeatable) -s, --store strings target stores matching glob pattern (repeatable) -v, --value strings delete entries matching value glob pattern (repeatable) -y, --yes skip all confirmation prompts ``` #### `pda meta`

· See also: Metadata, TTL, Encryption, Read-Only, Pinned

```text View or modify metadata (TTL, encryption, read-only, pinned) for a key without changing its value. With no flags, displays the key's current metadata. Pass flags to modify. Usage: pda meta KEY[@STORE] [flags] Flags: -d, --decrypt decrypt the value (store as plaintext) -e, --encrypt encrypt the value at rest --force bypass read-only protection for metadata changes -h, --help help for meta --pin pin the key (sorts to top in list) --readonly mark the key as read-only --ttl string set expiry (e.g. 30m, 2h) or 'never' to clear --unpin unpin the key --writable clear the read-only flag ``` #### `pda identity`

· See also: Identity, Encryption

```text Show or create the age encryption identity Usage: pda identity [flags] Aliases: identity, id Flags: --add-recipient string add an age public key as an additional encryption recipient -h, --help help for identity --new generate a new identity (errors if one already exists) --path print only the identity file path --remove-recipient string remove an age public key from the recipient list ``` #### `pda export`

· See also: Import & Export

```text Export store as NDJSON (alias for list --format ndjson) Usage: pda export [STORE] [flags] Flags: -h, --help help for export -k, --key strings filter keys with glob pattern (repeatable) -s, --store strings filter stores with glob pattern (repeatable) -v, --value strings filter values with glob pattern (repeatable) ``` #### `pda import`

· See also: Import & Export

```text Restore key/value pairs from an NDJSON dump Usage: pda import [STORE] [flags] Flags: --drop drop existing entries before restoring (full replace) -f, --file string path to an NDJSON dump (defaults to stdin) -h, --help help for import -i, --interactive prompt before overwriting existing keys -k, --key strings restore keys matching glob pattern (repeatable) -s, --store strings restore entries from stores matching glob pattern (repeatable) ``` #### `pda list-stores`

· See also: Stores

```text List all stores Usage: pda list-stores [flags] Aliases: list-stores, lss Flags: -h, --help help for list-stores --no-header suppress the header row --short only print store names ``` #### `pda move-store`

· See also: Stores

```text Rename a store Usage: pda move-store FROM TO [flags] Aliases: move-store, mvs Flags: --copy copy instead of move (keeps source) -h, --help help for move-store -i, --interactive prompt before overwriting destination --safe do not overwrite if the destination store already exists -y, --yes skip all confirmation prompts ``` #### `pda remove-store`

· See also: Stores

```text Delete a store Usage: pda remove-store STORE [flags] Aliases: remove-store, rms Flags: -h, --help help for remove-store -i, --interactive prompt yes/no for each deletion -y, --yes skip all confirmation prompts ``` #### `pda init`

· See also: Init, Git

```text Initialise pda! version control Usage: pda init [remote-url] [flags] Flags: --clean remove .git from stores directory before initialising -h, --help help for init ``` #### `pda sync`

· See also: Sync, Git

```text Manually sync your stores with Git Usage: pda sync [flags] Flags: -h, --help help for sync -m, --message string custom commit message (defaults to timestamp) ``` #### `pda git`

· See also: Git

```text Run any arbitrary command. Use with caution. The Git repository lives directly in the data directory ("PDA_DATA"). Store files (*.ndjson) are tracked by Git as-is. If you manually modify files without using the built-in commands, you may desync your repository. Generally prefer "pda sync". Usage: pda git [args...] [flags] Flags: -h, --help help for git ``` #### `pda config`

· See also: Config

```text View and modify configuration Usage: pda config [command] Available Commands: edit Open config file in $EDITOR get Print a configuration value init Generate default config file list List all configuration values path Print config file path set Set a configuration value Flags: -h, --help help for config Use "pda config [command] --help" for more information about a command. ``` #### `pda doctor`

· See also: Doctor

```text Check environment health Usage: pda doctor [flags] Flags: -h, --help help for doctor ``` #### `pda version`

· See also: Version

```text Display pda! version Usage: pda version [flags] Flags: -h, --help help for version --short print only the version string ``` ### License

MIT — see [LICENSE](LICENSE).