diff --git a/README.md b/README.md
index f95caa9..097a7b6 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ and more, written in pure Go, and inspired by [skate](https://github.com/charmbr
-`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.
+`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](https://en.wikipedia.org/wiki/Tab-separated_values), [Markdown](https://en.wikipedia.org/wiki/Markdown) and [HTML](https://en.wikipedia.org/wiki/HTML) tables, [JSON](https://en.wikipedia.org/wiki/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.
@@ -497,60 +497,57 @@ Additionally, `interactive` being passed or `key.always_prompt_overwrite` being
↑ ·
- pda help remove
+ pda remove
-[`pda remove`](#removing) (alias: [`rm`](#removing)) deletes one or more keys.
+[`pda remove`](#removing) (alias: [`rm`](#removing)) deletes one or more keys. Any number of keys can be deleted in a single call, with keys from differing stores able to be mixed freely.
```bash
-pda rm kitty
+# delete a single key
+pda remove kitty
+
+# delete multiple keys at once
+pda remove kitty doggy
+
+# delete across stores
+pda remove kitty secret@private
```
-Remove multiple keys at once:
+Exact positional keys can be combined with [glob patterns](#filtering) via `key`, `store`, and `value` to widen the scope of a deletion. Glob-matched deletions prompt for confirmation by default due to their more error-prone nature. This is configurable with `key.always_prompt_glob_delete` in the [config](#config).
```bash
-pda rm kitty dog@animals
+# delete "kitty" and everything matching the key "?og"
+❯ pda rm kitty --key "?og"
+ ??? remove 'cog'? (y/n)
+ ==> y
+ ??? remove 'dog'? (y/n)
+
+# delete keys matching a store and key pattern
+❯ pda rm --store "temp*" --key "session*"
```
-Mix exact keys with [glob patterns](#filtering) using [`--key`](#removing):
+Passing `interactive` prompts before each deletion, including exact keys. This behaviour can be made permanent with `key.always_prompt_delete` in the [config](#config). Inversely, `yes` auto-accepts all confirmation prompts.
```bash
-pda set cog "cogs"
-pda set dog "doggy"
-pda set kitty "cat"
-pda rm kitty --key "?og"
+# prompt before each deletion
+❯ pda rm kitty -i
+ ??? remove 'kitty'? (y/n)
+ ==> y
+
+# auto-accept all prompts
+❯ pda rm kitty -y
```
-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):
+[Read-only](#read-only) keys cannot be deleted without explicitly passing `force`.
```bash
+# remove a read-only key
❯ pda rm protected-key
FAIL cannot remove 'protected-key': key is read-only
-pda rm protected-key --force
+# force-remove a read-only key
+❯ pda rm protected-key --force
```
### Metadata
@@ -558,7 +555,7 @@ pda rm protected-key --force
↑ ·
- pda meta,
+ pda meta,
TTL,
Encryption,
Read-Only,
@@ -566,47 +563,41 @@ pda rm protected-key --force
-[`pda meta`](#metadata) views or modifies metadata for a key without changing its value. With no flags, it displays the key's current metadata:
+[`pda meta`](#metadata) can be used to view or modify metadata for a given key without touching its value. It always takes one argument: the desired key.
+
+If no flags are passed, [`pda meta`](#metadata) will display the key's current metadata. Any flags passed can be used to modify metadata in-place: [`ttl`](#ttl), [`encrypt`](#encryption) or [`decrypt`](#encryption), [`readonly`](#read-only) or [`writable`](#read-only), and [`pin`](#pinned) or [`unpin`](#pinned). Multiple changes can be combined in a single command.
+
+In [`pda list`](#listing) output, metadata is demonstrated via a `Meta` column. The presence of each type of metadata is marked by a character, or a dash if unset: [(e)ncrypted](#encryption), [(w)ritable](#read-only), [(t)ime-to-live](#ttl), and [(p)inned](#pinned).
```bash
+# view a key's underlying metadata
❯ pda meta session
key: session@store
secret: false
writable: true
pinned: false
expires: 59m30s
+
+# make a key read-only
+❯ pda meta session --readonly
+
+# remove a key's expiration time
+❯ pda meta session --ttl never
```
-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
-```
+Modifying a [read-only](#read-only) key's metadata requires `force` or by first making it `writable`. A [read-only](#read-only) key can still be [pinned or unpinned](#pinned) as pin state only determines where a key is on `list output`[#listing], and does not change the actual key state.
#### TTL
↑ ·
- pda help set,
- pda help meta
+ 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):
+Keys can be given an expiration time. Expired keys are marked for garbage collection and deleted on the next access to the [store](#stores). [TTL](#ttl) can be set at creation time via [`pda set --ttl`](#setting), or toggled later with [`pda meta --ttl`](#metadata) and [`pda edit --ttl`](#editing).
```bash
# expire after 1 hour
@@ -614,91 +605,70 @@ pda set session "123" --ttl 1h
# expire after 54 minutes and 10 seconds
pda set session2 "xyz" --ttl 54m10s
+
+# remove an expiration time
+pda meta session --ttl never
```
-[`pda list`](#listing) shows expiration in the TTL column:
+TTL can be displayed with [`pda list`](#listing) in the [TTL](#ttl) column, or with [`pda meta`](#metadata).
```bash
+# view ttl in a store's list output
❯ pda ls
TTL Key Value
59m30s session 123
51m40s session2 xyz
+
+# view the metadata of a specific key
+❯ pda meta session
```
-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.
+Expiration time is preserved on [`import`](#import--export) and [`export`](#import--export) and [moving or copying](#moving--copying). TTL is stored as a timestamp rather than a timer; keys with a TTL are checked on access to the store they reside in, and any with an expiry that has already passed are deleted. If a key expires while having been exported, it will be deleted on import the next time `pda` touches the file.
#### Encryption
↑ ·
- pda help set,
- pda help meta,
- pda help identity
+ 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.
+[`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 at run-time by commands like [`pda get`](#getting) and [`pda list`](#listing) when the correct identity file is present. An X25519 [identity](#identity) is generated on first use.
+
+By default, the only recipient for encrypted keys is your own [identity](#identity) file. Additional recipients can be added or removed via [`pda identity`](#identity).
```bash
-pda set --encrypt api-key "sk-live-abc123"
-# ok created identity at ~/.config/pda/identity.txt
+# create a key called "api-key" and encrypt it
+❯ pda set --encrypt api-key "sk-live-abc123"
+ ok created identity at ~/.local/share/pda/identity.txt
-pda set --encrypt token "ghp_xxxx"
+# encrypt a key after editing in $EDITOR
+❯ pda edit --encrypt api-key
+
+# decrypt a key via meta
+❯ pda meta --decrypt api-key
```
-[`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):
+Because the on-disk value of an encrypted key is ciphertext, 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:
+[`mv`](#moving--copying), [`cp`](#moving--copying), and [`import`](#import--export) all preserve encryption, read-only, and pinned flags. Overwriting an encrypted key by setting a new value without `--encrypt` will warn you.
```bash
-pda cp api-key api-key-backup
-# still encrypted
-
+# setting a new key and forgetting the "--encrypt" flag
❯ 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:
+If your [identity](#identity) file does not match an intended recipient of an encrypted key, the value will be inaccessible. It will display `locked` on fetch until a matching [identity](#identity) is found.
```bash
❯ pda ls
@@ -709,93 +679,76 @@ ew-- api-key locked (identity file missing)
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.
+Encrypted keys can be made the default by enabling `key.always_encrypt` in the [config](#config).
#### Read-Only
↑ ·
- pda help set,
- pda help meta
+ pda set,
+ pda 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:
+Keys marked [read-only](#read-only) are protected from accidental modification. A [read-only](#read-only) flag can be set at creation time, toggled later with [`pda meta`](#metadata), or applied alongside an [`edit`](#editing). Making a key [`writable`](#metadata) again or explicitly passing [`force`](#metadata) allows changes through. A key being made writable is a permanent change, whereas the `force` flag is a one-off.
```bash
+# create a read-only key
pda set api-url "https://prod.example.com" --readonly
-```
-Toggle with [`pda meta`](#metadata):
-
-```bash
+# set a key to read-only with meta
❯ pda meta api-url --readonly
ok made readonly api-url
+# set a key as writable with meta
❯ pda meta api-url --writable
ok made writable api-url
+
+# edit a key, and set as readonly on save
+❯ pda edit notes --readonly
```
-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:
+Read-only keys are protected from [`setting`](#setting), [`removing`](#removing), [`moving`](#moving--copying), and [`editing`](#editing). They are *not protected* from the deletion of an entire [store](#stores).
```bash
+# set a new value to a read-only key
❯ 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
+# force changes to a read-only key with the force flag
+❯ pda set api-url "new value" --force
+❯ pda remove api-url --force
+❯ pda move 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`.
+[`pda copy`](#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
+ pda set,
+ pda 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:
+Pinned keys sort to the top of [`pda list`](#listing) output, preserving alphabetical order within the pinned and unpinned groups. A pin can be set at creation time, toggled with [`pda meta`](#metadata), or applied alongside an [`edit`](#editing).
```bash
+# pin a key at creation time
pda set important "remember this" --pin
-```
-Toggle with [`pda meta`](#metadata):
-
-```bash
+# pin a key with meta
❯ pda meta todo --pin
ok pinned todo
+# unpin a key with meta
❯ pda meta todo --unpin
ok unpinned todo
-```
-```bash
+# view pinned keys in list output, at the top
❯ pda ls
Meta Key Value
-w-p important remember this
@@ -808,95 +761,66 @@ Meta Key Value
↑ ·
- pda list-stores,
- pda move-store,
- pda remove-store
+ 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.
+Stores are saved on disk as NDJSON files. `pda` supports any number of stores, and creating them is automatic. If a key is created with a `@STORE` suffix, and the named store does not already exist, it will be created automatically to support the new key.
-[`pda list-stores`](#stores) (alias: [`lss`](#stores)) shows all stores with key counts and file sizes:
+[`pda list-stores`](#stores) (alias: [`lss`](#stores)) shows all stores with their respective key counts and file sizes. Passing `short` prints only the store names.
```bash
+# list all stores
❯ pda list-stores
Keys Size Store
2 1.8k @birthdays
12 4.2k @store
-```
-[`--short`](#stores) prints only the names:
-
-```bash
+# list all store names
❯ 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:
+[`pda move-store`](#stores) (alias: [`mvs`](#stores)) renames a store. Passing `copy` keeps the source intact.
```bash
+# rename a store
pda move-store birthdays bdays
-```
-Copy a store with `--copy`:
-
-```bash
+# copy a store
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:
+[`pda remove-store`](#stores) (alias: [`rms`](#stores)) deletes a store. A confirmation prompt is shown by default (configurable with `store.always_prompt_delete` in the [config](#config)). Deleting an entire store does not require unsetting [read-only](#read-only) on contained keys: if a [read-only](#read-only) key is within a store, it **will be deleted** if the store is removed.
```bash
+# delete a store
pda remove-store birthdays
```
-[`--yes`](#stores) / `-y` skips confirmation prompts:
+As with changeful key operations, store commands support `interactive` and `safe` flags where they make sense. Moving or removing a store interactively will generate a confirmation prompt if anything would be lost by the action being taken. The `safe` flag will prevent moving a store from ever overwriting another store.
-```bash
-pda remove-store birthdays -y
-```
+Inversely, `yes` can be passed to bypass any confirmation prompts.
#### Import & Export
↑ ·
- pda help export,
- pda help import
+ pda export,
+ pda import
-[`pda export`](#import--export) exports everything as NDJSON (it's an alias for `list --format ndjson`):
+[`pda export`](#import--export) dumps entries as NDJSON (it is functionally an alias for `list --format ndjson`). The [filtering](#filtering) flags `key`, `value`, and `store` all work with exports. It shares functionality with [`pda list`](#listing) in regard to which stores get exported: if `list.always_show_all_stores` is set and no store name is specified as an argument, all stores will be exported.
```bash
+# export everything
pda export > my_backup
-```
-Filter exports with [`--key`](#filtering), [`--value`](#filtering), and [`--store`](#filtering):
-
-```bash
# export only matching keys
pda export --key "a*"
@@ -904,46 +828,28 @@ pda export --key "a*"
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`.
+[`pda import`](#import--export) restores entries from an NDJSON dump. The default behaviour for an import is to merge with any existing stores of the same name. To completely replace existing stores instead of merging, `drop` can be passed.
+
+[Importing](#import--export) takes one or zero arguments. On export each key saves the name of the store it came from in its metadata; on import, by default, each key will be returned to that same store. If a store name is passed as an argument to [`pda import`](#import--export), this behaviour will be overriden and all keys will be imported into the specified store.
+
+As with [exporting](#import--export), `key`, `value`, and `store` flags can be passed to filter which keys will be imported from the input file.
```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
+# force all entries into a single store
pda import mystore -f my_backup
# ok restored 5 entries into @mystore
-```
-Read from stdin:
-
-```bash
+# read from stdin
pda import < my_backup
```
-Filter imports with [`--key`](#filtering) and [`--store`](#filtering):
+`interactive` can be passed to [`pda import`](#import--export) to prompt on potential overwrite, and is generally recommended if an import is ever being routed to a specific store, as it is likely to cause collisions.
-```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.
+[`pda export`](#import--export) encodes [binary data](#binary-data) as base64. All [metadata](#metadata) is preserved through export and import.
### Templates
@@ -957,9 +863,11 @@ pda import --drop -f my_backup
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.
+`text/template` is a Turing-complete templating library that supports pipelines, nested templates, conditionals, loops, and more. Actions are given with `{{ action }}` syntax. To better accomodate `text/template`, `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").
+These same functions are also available in `git.default_commit_message` templates, in addition to `summary`, which returns the action that triggered the commit (e.g. "set foo", "removed bar").
+
+`no-template` can be passed to output a raw value without resolving the template.
#### Basic Substitution
@@ -1084,20 +992,15 @@ FAIL cannot get 'level': ...invalid value 'debug', allowed: [info warn error]
-`int` parses a variable as an integer, useful for loops and arithmetic:
+`int` parses a variable as an integer. Useful mostly for loops or arithmetic.
```bash
-pda set number "{{ int .N }}"
-
+❯ 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 }}"
+# using "int" in a loop
+❯ pda set meows "{{ range int .COUNT }}meow! {{ end }}"
❯ pda get meows COUNT=4
meow! meow! meow! meow!
```
@@ -1129,23 +1032,19 @@ Hi Bob. Hi Alice.
-`shell` executes a command and returns its stdout:
+`shell` executes a command and returns its stdout. Commands executed by the `shell` function are executed by `$SHELL`. If it is somehow unset, it defaults to using `/usr/sh`.
```bash
-pda set rev '{{ shell "git rev-parse --short HEAD" }}'
-
+❯ pda set rev '{{ shell "git rev-parse --short HEAD" }}'
❯ pda get rev
a1b2c3d
-```
-
-```bash
-pda set today '{{ shell "date +%Y-%m-%d" }}'
+❯ pda set today '{{ shell "date +%Y-%m-%d" }}'
❯ pda get today
2025-06-15
```
-#### `pda` (Recursive)
+#### `pda`
@@ -1154,47 +1053,22 @@ pda set today '{{ shell "date +%Y-%m-%d" }}'
-`pda` gets another key's value, enabling recursive composition:
+`pda` returns the output of [`pda get`](#getting) on a key.
```bash
-pda set base_url "https://api.example.com"
-pda set endpoint '{{ pda "base_url" }}/users/{{ require .ID }}'
-
+# use the value of "base_url" in another key
+❯ 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'
+# use the value of a key from another store
+❯ 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
@@ -1207,11 +1081,11 @@ Good morning.
-[`--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).
+[`key`](#filtering), [`value`](#filtering), and [`store`](#filtering) flags can be used to filter entries via globs. 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).
+These filters work with [`pda list`](#listing), [`pda export`](#import--export), [`pda import`](#import--export), and [`pda remove`](#removing).
-[`gobwas/glob`](https://github.com/gobwas/glob) is used for matching. The default separators are `/-_.@:` and space.
+[`gobwas/glob`](https://github.com/gobwas/glob) is used for matching. The default separators are `/-_.@:` and space. For a detailed guide to globbing, I highly recommend taking a look at the documentation there directly.
#### Glob Patterns
@@ -1222,63 +1096,69 @@ These filters work with [`list`](#listing), [`export`](#import--export), [`impor
-`*` wildcards a word or series of characters, stopping at separator boundaries:
+`*` wildcards a word or series of characters, stopping at separator boundaries.
```bash
+# list all store contents
❯ pda ls
cat
-dog
-cog
-mouse hotdog
mouse house
foo.bar.baz
-pda ls --key "*"
-# cat, dog, cog (single-segment keys only)
+# match any single-word key
+❯ pda ls --key "*"
+cat
-pda ls --key "* *"
-# mouse hotdog, mouse house
+# match any two-word key
+❯ pda ls --key "* *"
+mouse house
-pda ls --key "foo.*.baz"
-# foo.bar.baz
+# match any key starting with "foo." and ending with ".baz", with one word between
+❯ pda ls --key "foo.*.baz"
+foo.bar.baz
```
-`**` super-wildcards ignore word boundaries:
+`**` super-wildcards ignore word boundaries.
```bash
-pda ls --key "foo**"
-# foo.bar.baz
-
-pda ls --key "**g"
-# dog, cog, mouse hotdog
+# match anything beginning with "foo"
+❯ pda ls --key "foo**"
+foo.bar.baz
```
`?` matches a single character:
```bash
-pda ls --key "?og"
-# dog, cog
+# match anything beginning with any letter, and ending with "og"
+❯ pda ls --key "?og"
+dog
+cog
```
-`[abc]` matches one of the characters in the brackets:
+`[abc]` matches one of the characters in the brackets.
```bash
-pda ls --key "[dc]og"
-# dog, cog
+# match anything beginning with "d" or "c", and ending with "og"
+❯ pda ls --key "[dc]og"
+dog
+cog
# negate with '!'
-pda ls --key "[!dc]og"
-# bog (if it exists)
+❯ pda ls --key "[!dc]og"
+bog
```
`[a-c]` matches a range:
```bash
-pda ls --key "[a-g]ag"
-# bag, gag
+# match anything beginning with "a" to "g", and ending with "ag"
+❯ pda ls --key "[a-g]ag"
+bag
+gag
-pda ls --key "[!a-g]ag"
-# wag
+# negate with '!'
+❯ pda ls --key "[!a-g]ag"
+wag
```
#### Filtering by Key
@@ -1291,14 +1171,13 @@ pda ls --key "[!a-g]ag"
-[`--key`](#filtering) / `-k` filters entries by key name:
+[`key`](#filtering) filters entries by key name. Multiple `key` patterns are OR'd. An entry matches if it matches any of them.
```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
@@ -1310,24 +1189,14 @@ Multiple `--key` patterns are OR'd — an entry matches if it matches any of the
-[`--value`](#filtering) / `-v` filters by value content using the same glob syntax:
+[`value`](#filtering) filters by value. Multiple `value` patterns are OR'd.
```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.
+Locked (encrypted without an available identity) and non-UTF-8 (binary) entries are silently excluded from `value` matching.
#### Filtering by Store
@@ -1339,7 +1208,7 @@ Locked (encrypted without an available identity) and non-UTF-8 (binary) entries
-[`--store`](#filtering) / `-s` filters by store name:
+[`store`](#filtering) filters by store name. Multiple `store` patterns are OR'd.
```bash
pda ls --store "prod*"
@@ -1355,13 +1224,13 @@ pda export --store "dev*"
-Combine key, value, and store filters. Results must match all flags (AND), with OR within each flag:
+`key`, `value`, and `store` filters can be combined. Results must match at least one of each category of filter used. For example, checking for `key` and two different `value` globs on the same filter: the results must match `key` and at least one of the two `value` globs; the results do not need to match both values.
```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):
+Globs can be combined to create some deeply complex queries. For example, [`key`](#filtering) can be combined with exact positional args on [`rm`](#removing) to remove exactly the "cat" key, and any keys beginning with "cat", "dog", or "mouse" followed by zero-or-more additional words.
```bash
pda rm cat --key "{mouse,[cd]og}**"
@@ -1382,75 +1251,59 @@ pda rm cat --key "{mouse,[cd]og}**"
-`pda!` supports all binary data. Save it with [`pda set`](#setting):
+`pda!` supports all binary data. Values can be read from a file with `--file` or piped in via stdin. Retrieval works the same way: pipe or redirect the output to get the raw bytes.
```bash
+# store binary data from a file
pda set logo < logo.png
pda set logo -f logo.png
-```
-And retrieve it with [`pda get`](#getting):
-
-```bash
+# retrieve binary data
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:
+On a TTY, [`get`](#getting) and [`list`](#listing) show a summary instead of printing raw bytes (which can cause undefined terminal behaviour). In a non-TTY setting (piped or redirected), the raw bytes are returned as expected. Passing `base64` provides a safe way to view binary data in a terminal.
```bash
+# TTY shows a summary
❯ pda get logo
(binary: 4.2 KB, image/png)
-```
-[`--base64`](#getting) / `-b` views binary data as base64:
-
-```bash
+# base64 view
❯ pda get logo --base64
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVQI12...
```
-[`pda export`](#import--export) encodes binary data as base64 in the NDJSON:
+[`pda export`](#import--export) encodes binary data as base64 in the NDJSON, and [`pda edit`](#editing) presents binary values as base64 for editing and decodes them back on save.
```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,
+ pda init,
+ pda sync,
+ pda git,
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:
+[`pda init`](#git) initialises version control in the data directory. Without arguments, this creates a local-only repository. Passing a remote URL clones from an existing repository instead, useful for syncing `pda` across machines. For anything that [`pda sync`](#sync) doesn't cover, [`pda git`](#pda-git) passes arguments directly to `git` from within the data directory (this generally should be avoided unless things are very broken somehow).
```bash
-# initialise an empty repository
+# initialise an empty local repository
pda init
-# or clone an existing one
+# or clone from an existing remote
pda init https://github.com/llywelwyn/my-repository
```
-[`--clean`](#git) removes the existing `.git` directory first, useful for reinitialising or switching remotes:
+Passing `clean` removes any existing `.git` directory first. This is primarily useful for reinitialising a broken repository or switching to a different remote.
```bash
pda init --clean
@@ -1462,108 +1315,76 @@ pda init https://github.com/llywelwyn/my-repository --clean
↑ ·
- pda help sync
+ pda 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.
+[`pda sync`](#sync) conducts a best-effort sync of local data with the 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.
+If the local repository is ahead, syncing will commit and push. If it is behind, syncing will detect this and prompt: either stash local changes and pull, or abort and resolve manually. Running [`pda sync`](#sync) will always fetch, commit, and push regardless of the automation settings in [config](#config).
```bash
# sync with Git
pda sync
# with a custom commit message
-pda sync -m "added production credentials"
+pda sync -m "new bookmarks"
```
-Running [`pda sync`](#sync) manually will always fetch, commit, and push — or stash and pull if behind — regardless of config.
-
-#### Auto-Commit & Auto-Push
+#### Auto-Commit, Push, and Fetch
↑ ·
- pda help config
+ pda config
-`pda!` supports automation via its [config](#config). There are options for `git.auto_commit`, `git.auto_fetch`, and `git.auto_push`.
+The amount of Git automation can be configured via `git.auto_commit`, `git.auto_fetch`, and `git.auto_push` in the [config](#config). All three are disabled by default.
-**`git.auto_commit`** commits changes immediately to the local Git repository any time data is changed.
+`git.auto_commit` commits changes to the local repository any time data is changed. `git.auto_fetch` fetches from the remote before committing, though this incurs a noticeable slowdown due to network round-trips. `git.auto_push` pushes committed changes to the remote after each commit.
-**`git.auto_fetch`** fetches before committing any changes. This incurs a noticeable slowdown due to network round-trips.
+`auto_fetch` and `auto_push` are additional steps that happen during the commit process, so they have no effect if `auto_commit` is disabled. Running all Git operations on every change can be slow, but a commit is fast. A happy middle-ground is enabling `git.auto_commit` and doing the rest manually via [`pda sync`](#sync) when changing devices.
-**`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,
+ pda identity,
Encryption
-[`pda identity`](#identity) (alias: [`id`](#identity)) manages the age encryption identity used for [encryption](#encryption).
+[`pda identity`](#identity) (alias: [`id`](#identity)) manages the [age](https://github.com/FiloSottile/age) identity used for [encryption](#encryption). An identity is generated automatically the first time [`--encrypt`](#encryption) is used, but one can also be created manually with `--new`. [`--new`](#identity) will error if an identity already exists; delete the file manually to replace it.
-#### Viewing Identity
+```bash
+# create a new identity manually
+pda identity --new
+```
-
-
- ↑ ·
- pda help identity
-
-
-
-With no flags, [`pda identity`](#identity) shows your public key, identity file path, and any additional recipients:
+With no flags, [`pda identity`](#identity) displays your public key, identity file path, and any additional recipients. Passing `path` prints only the identity file path, useful for scripting.
```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
+ pda 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:
+By default, secrets are encrypted only for your own identity. To encrypt for additional recipients (e.g. another device or a friend), 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
@@ -1572,15 +1393,11 @@ By default, secrets are encrypted only for your own identity. To encrypt for add
ok re-encrypted 1 secret(s)
```
-Remove a recipient with [`--remove-recipient`](#identity). Secrets are re-encrypted without their key:
+Removing a recipient with `--remove-recipient` re-encrypts all secrets without their key. Additional recipients are shown in the default [`pda identity`](#identity) output.
```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
@@ -1592,23 +1409,12 @@ Additional recipients are shown in the default identity display:
↑ ·
- pda config,
- pda doctor
+ 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:
+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. [`pda config`](#config) manages configuration without needing to edit files by hand, and [`pda doctor`](#doctor) will warn about unrecognised keys (typos, removed options) and show any non-default values, so it doubles as a config audit.
```bash
# list all config values and their current settings
@@ -1637,15 +1443,13 @@ pda config init --new
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
+#### Default Config
↑ ·
Config,
- pda help config
+ pda config
@@ -1703,39 +1507,28 @@ default_commit_message = "{{ summary }} {{ time }}"
↑ ·
Config,
- pda doctor
+ pda doctor
-`PDA_CONFIG` overrides the config directory. `pda!` will look for `config.toml` in this directory:
+`pda!` respects a small set of environment variables for overriding paths and tools. These are primarily useful for isolating stores across environments or for scripting.
+
+`PDA_CONFIG` overrides the config directory, causing `pda!` to look for `config.toml` here instead of the default XDG location. `PDA_DATA` overrides the data storage directory where stores and the Git repository live. Default data locations follow XDG conventions: `~/.local/share/pda/` on Linux, `~/Library/Application Support/pda/` on macOS, and `%LOCALAPPDATA%/pda/` on Windows.
```bash
+# use an alternative config directory
PDA_CONFIG=/tmp/config/ pda set key value
-```
-`PDA_DATA` overrides the data storage directory:
-
-```bash
+# use an alternative data directory
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:
+`EDITOR` is used by [`pda edit`](#editing) and [`pda config edit`](#config) to open values in a text editor, and must be set for these commands to work. `SHELL` is used by [`pda run`](#running) (or [`pda get --run`](#getting)) for command execution, falling back to `/bin/sh` if unset.
```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
@@ -1746,11 +1539,11 @@ pda run script
-[`pda doctor`](#doctor) runs a set of health checks of your environment:
+[`pda doctor`](#doctor) runs a set of health checks against your [environment](#environment), covering installed tools, [config](#config) validity, [store](#stores) integrity, and [Git](#git) status. As mentioned in [config](#config), it also doubles as a config audit by warning about unrecognised keys and surfacing any non-default values.
```bash
❯ pda doctor
- ok pda! 2025.52 Christmas release (linux/amd64)
+ ok pda! 2026.14 (linux/amd64)
ok OS: Linux 6.18.7-arch1-1
ok Go: go1.23.0
ok Git: 2.45.0
@@ -1768,28 +1561,28 @@ pda run script
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.
+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 indicate that some functionality isn't being made use of (like [version control](#git) not having been initialised yet).
### Version
↑ ·
- pda help version
+ pda version
+[`pda version`](#version) displays the current version. `pda!` uses calendar versioning in the format `YYYY.WW`. Passing `short` prints just the release string without ASCII art, useful for scripting. The ASCII art can also be permanently disabled with `display_ascii_art = false` in the [config](#config).
+
```bash
# display the full version output
pda version
# or just the release
❯ pda version --short
-pda! 2025.52 Christmas release
+pda! 2026.14
```
-`pda!` uses calendar versioning: `YYYY.WW`. ASCII art can be permanently disabled with `display_ascii_art = false` in [config](#config).
-
### Help
diff --git a/cmd/completions.go b/cmd/completions.go
new file mode 100644
index 0000000..0f8b856
--- /dev/null
+++ b/cmd/completions.go
@@ -0,0 +1,83 @@
+package cmd
+
+import (
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+// completeKeys returns key[@store] completions for the current toComplete prefix.
+// It handles three cases:
+// - No "@" typed yet: return all keys from all stores (as "key@store")
+// - "@" typed with partial store: return store-scoped completions
+// - "key@store" with known store: return keys from that store
+func completeKeys(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ store := &Store{}
+ stores, err := store.AllStores()
+ if err != nil || len(stores) == 0 {
+ return nil, cobra.ShellCompDirectiveNoFileComp
+ }
+
+ var completions []string
+ parts := strings.SplitN(toComplete, "@", 2)
+
+ if len(parts) == 2 {
+ // User typed "something@" — complete keys within matching stores.
+ prefix := parts[0]
+ dbFilter := strings.ToLower(parts[1])
+ for _, db := range stores {
+ if !strings.HasPrefix(db, dbFilter) {
+ continue
+ }
+ keys, err := store.Keys(db)
+ if err != nil {
+ continue
+ }
+ for _, k := range keys {
+ if prefix == "" || strings.HasPrefix(k, strings.ToLower(prefix)) {
+ completions = append(completions, k+"@"+db)
+ }
+ }
+ }
+ } else {
+ // No "@" yet — offer key@store for every key in every store.
+ lowerPrefix := strings.ToLower(toComplete)
+ for _, db := range stores {
+ keys, err := store.Keys(db)
+ if err != nil {
+ continue
+ }
+ for _, k := range keys {
+ full := k + "@" + db
+ if strings.HasPrefix(full, lowerPrefix) || strings.HasPrefix(k, lowerPrefix) {
+ completions = append(completions, full)
+ }
+ }
+ }
+ }
+
+ return completions, cobra.ShellCompDirectiveNoFileComp
+}
+
+// completeStores returns store name completions.
+func completeStores(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ store := &Store{}
+ stores, err := store.AllStores()
+ if err != nil {
+ return nil, cobra.ShellCompDirectiveNoFileComp
+ }
+
+ var completions []string
+ lowerPrefix := strings.ToLower(toComplete)
+ for _, db := range stores {
+ if strings.HasPrefix(db, lowerPrefix) {
+ completions = append(completions, db)
+ }
+ }
+ return completions, cobra.ShellCompDirectiveNoFileComp
+}
+
+// completeStoreFlag is a completion function for --store / -s string slice flags.
+func completeStoreFlag(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return completeStores(cmd, args, toComplete)
+}
diff --git a/cmd/del-db.go b/cmd/del-db.go
index 31fe227..e094370 100644
--- a/cmd/del-db.go
+++ b/cmd/del-db.go
@@ -33,12 +33,13 @@ import (
// delStoreCmd represents the set command
var delStoreCmd = &cobra.Command{
- Use: "remove-store STORE",
- Short: "Delete a store",
- Aliases: []string{"rms"},
- Args: cobra.ExactArgs(1),
- RunE: delStore,
- SilenceUsage: true,
+ Use: "remove-store STORE",
+ Short: "Delete a store",
+ Aliases: []string{"rms"},
+ Args: cobra.ExactArgs(1),
+ ValidArgsFunction: completeStores,
+ RunE: delStore,
+ SilenceUsage: true,
}
func delStore(cmd *cobra.Command, args []string) error {
diff --git a/cmd/del.go b/cmd/del.go
index 3dfda52..fba342c 100644
--- a/cmd/del.go
+++ b/cmd/del.go
@@ -34,9 +34,10 @@ import (
var delCmd = &cobra.Command{
Use: "remove KEY[@STORE] [KEY[@STORE] ...]",
Short: "Delete one or more keys",
- Aliases: []string{"rm"},
- Args: cobra.ArbitraryArgs,
- RunE: del,
+ Aliases: []string{"rm"},
+ Args: cobra.ArbitraryArgs,
+ ValidArgsFunction: completeKeys,
+ RunE: del,
SilenceUsage: true,
}
@@ -145,6 +146,7 @@ func init() {
delCmd.Flags().Bool("force", false, "bypass read-only protection")
delCmd.Flags().StringSliceP("key", "k", nil, "delete keys matching glob pattern (repeatable)")
delCmd.Flags().StringSliceP("store", "s", nil, "target stores matching glob pattern (repeatable)")
+ delCmd.RegisterFlagCompletionFunc("store", completeStoreFlag)
delCmd.Flags().StringSliceP("value", "v", nil, "delete entries matching value glob pattern (repeatable)")
rootCmd.AddCommand(delCmd)
}
diff --git a/cmd/edit.go b/cmd/edit.go
index 96c31ad..a5cbe03 100644
--- a/cmd/edit.go
+++ b/cmd/edit.go
@@ -22,9 +22,10 @@ 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.`,
- Aliases: []string{"e"},
- Args: cobra.ExactArgs(1),
- RunE: edit,
+ Aliases: []string{"e"},
+ Args: cobra.ExactArgs(1),
+ ValidArgsFunction: completeKeys,
+ RunE: edit,
SilenceUsage: true,
}
diff --git a/cmd/export.go b/cmd/export.go
index 94a22eb..80dade8 100644
--- a/cmd/export.go
+++ b/cmd/export.go
@@ -31,6 +31,7 @@ var exportCmd = &cobra.Command{
Short: "Export store as NDJSON (alias for list --format ndjson)",
Aliases: []string{},
Args: cobra.MaximumNArgs(1),
+ ValidArgsFunction: completeStores,
RunE: func(cmd *cobra.Command, args []string) error {
listFormat = "ndjson"
return list(cmd, args)
@@ -41,6 +42,7 @@ var exportCmd = &cobra.Command{
func init() {
exportCmd.Flags().StringSliceP("key", "k", nil, "filter keys with glob pattern (repeatable)")
exportCmd.Flags().StringSliceP("store", "s", nil, "filter stores with glob pattern (repeatable)")
+ exportCmd.RegisterFlagCompletionFunc("store", completeStoreFlag)
exportCmd.Flags().StringSliceP("value", "v", nil, "filter values with glob pattern (repeatable)")
rootCmd.AddCommand(exportCmd)
}
diff --git a/cmd/get.go b/cmd/get.go
index ff1f5a8..d4d9116 100644
--- a/cmd/get.go
+++ b/cmd/get.go
@@ -46,9 +46,10 @@ additional argument after the initial KEY being fetched.
For example:
pda set greeting 'Hello, {{ .NAME }}!'
pda get greeting NAME=World`,
- Aliases: []string{"g"},
- Args: cobra.MinimumNArgs(1),
- RunE: get,
+ Aliases: []string{"g"},
+ Args: cobra.MinimumNArgs(1),
+ ValidArgsFunction: completeKeys,
+ RunE: get,
SilenceUsage: true,
}
@@ -63,8 +64,9 @@ additional argument after the initial KEY being fetched.
For example:
pda set greeting 'Hello, {{ .NAME }}!'
pda run greeting NAME=World`,
- Args: cobra.MinimumNArgs(1),
- RunE: run,
+ Args: cobra.MinimumNArgs(1),
+ ValidArgsFunction: completeKeys,
+ RunE: run,
SilenceUsage: true,
}
diff --git a/cmd/list.go b/cmd/list.go
index 86259f1..50e319b 100644
--- a/cmd/list.go
+++ b/cmd/list.go
@@ -141,10 +141,11 @@ 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.`,
- Aliases: []string{"ls"},
- Args: cobra.MaximumNArgs(1),
- RunE: list,
- SilenceUsage: true,
+ Aliases: []string{"ls"},
+ Args: cobra.MaximumNArgs(1),
+ ValidArgsFunction: completeStores,
+ RunE: list,
+ SilenceUsage: true,
}
func list(cmd *cobra.Command, args []string) error {
@@ -785,6 +786,7 @@ func init() {
listCmd.Flags().VarP(&listFormat, "format", "o", "output format (table|tsv|csv|markdown|html|ndjson|json)")
listCmd.Flags().StringSliceP("key", "k", nil, "filter keys with glob pattern (repeatable)")
listCmd.Flags().StringSliceP("store", "s", nil, "filter stores with glob pattern (repeatable)")
+ listCmd.RegisterFlagCompletionFunc("store", completeStoreFlag)
listCmd.Flags().StringSliceP("value", "v", nil, "filter values with glob pattern (repeatable)")
rootCmd.AddCommand(listCmd)
}
diff --git a/cmd/meta.go b/cmd/meta.go
index 62b862a..91efc26 100644
--- a/cmd/meta.go
+++ b/cmd/meta.go
@@ -14,8 +14,9 @@ var metaCmd = &cobra.Command{
without changing its value.
With no flags, displays the key's current metadata. Pass flags to modify.`,
- Args: cobra.ExactArgs(1),
- RunE: meta,
+ Args: cobra.ExactArgs(1),
+ ValidArgsFunction: completeKeys,
+ RunE: meta,
SilenceUsage: true,
}
diff --git a/cmd/mv-db.go b/cmd/mv-db.go
index f3a360e..1e1db1e 100644
--- a/cmd/mv-db.go
+++ b/cmd/mv-db.go
@@ -33,12 +33,13 @@ import (
// mvStoreCmd represents the move-store command
var mvStoreCmd = &cobra.Command{
- Use: "move-store FROM TO",
- Short: "Rename a store",
- Aliases: []string{"mvs"},
- Args: cobra.ExactArgs(2),
- RunE: mvStore,
- SilenceUsage: true,
+ Use: "move-store FROM TO",
+ Short: "Rename a store",
+ Aliases: []string{"mvs"},
+ Args: cobra.ExactArgs(2),
+ ValidArgsFunction: completeStores,
+ RunE: mvStore,
+ SilenceUsage: true,
}
func mvStore(cmd *cobra.Command, args []string) error {
diff --git a/cmd/mv.go b/cmd/mv.go
index fa7b9d4..1594962 100644
--- a/cmd/mv.go
+++ b/cmd/mv.go
@@ -30,21 +30,23 @@ import (
)
var cpCmd = &cobra.Command{
- Use: "copy FROM[@STORE] TO[@STORE]",
- Aliases: []string{"cp"},
- Short: "Make a copy of a key",
- Args: cobra.ExactArgs(2),
- RunE: cp,
- SilenceUsage: true,
+ Use: "copy FROM[@STORE] TO[@STORE]",
+ Aliases: []string{"cp"},
+ Short: "Make a copy of a key",
+ Args: cobra.ExactArgs(2),
+ ValidArgsFunction: completeKeys,
+ RunE: cp,
+ SilenceUsage: true,
}
var mvCmd = &cobra.Command{
- Use: "move FROM[@STORE] TO[@STORE]",
- Aliases: []string{"mv"},
- Short: "Move a key",
- Args: cobra.ExactArgs(2),
- RunE: mv,
- SilenceUsage: true,
+ Use: "move FROM[@STORE] TO[@STORE]",
+ Aliases: []string{"mv"},
+ Short: "Move a key",
+ Args: cobra.ExactArgs(2),
+ ValidArgsFunction: completeKeys,
+ RunE: mv,
+ SilenceUsage: true,
}
func cp(cmd *cobra.Command, args []string) error {
diff --git a/cmd/restore.go b/cmd/restore.go
index 70948ba..03d1d30 100644
--- a/cmd/restore.go
+++ b/cmd/restore.go
@@ -37,12 +37,13 @@ import (
)
var restoreCmd = &cobra.Command{
- Use: "import [STORE]",
- Short: "Restore key/value pairs from an NDJSON dump",
- Aliases: []string{},
- Args: cobra.MaximumNArgs(1),
- RunE: restore,
- SilenceUsage: true,
+ Use: "import [STORE]",
+ Short: "Restore key/value pairs from an NDJSON dump",
+ Aliases: []string{},
+ Args: cobra.MaximumNArgs(1),
+ ValidArgsFunction: completeStores,
+ RunE: restore,
+ SilenceUsage: true,
}
func restore(cmd *cobra.Command, args []string) error {
@@ -323,6 +324,7 @@ func init() {
restoreCmd.Flags().StringP("file", "f", "", "path to an NDJSON dump (defaults to stdin)")
restoreCmd.Flags().StringSliceP("key", "k", nil, "restore keys matching glob pattern (repeatable)")
restoreCmd.Flags().StringSliceP("store", "s", nil, "restore entries from stores matching glob pattern (repeatable)")
+ restoreCmd.RegisterFlagCompletionFunc("store", completeStoreFlag)
restoreCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting existing keys")
restoreCmd.Flags().Bool("drop", false, "drop existing entries before restoring (full replace)")
rootCmd.AddCommand(restoreCmd)
diff --git a/cmd/set.go b/cmd/set.go
index d81f41b..9435c94 100644
--- a/cmd/set.go
+++ b/cmd/set.go
@@ -50,9 +50,10 @@ For example:
'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.`,
- Aliases: []string{"s"},
- Args: cobra.RangeArgs(1, 2),
- RunE: set,
+ Aliases: []string{"s"},
+ Args: cobra.RangeArgs(1, 2),
+ ValidArgsFunction: completeKeys,
+ RunE: set,
SilenceUsage: true,
}
diff --git a/cmd/version.go b/cmd/version.go
index 8c46579..5e27c90 100644
--- a/cmd/version.go
+++ b/cmd/version.go
@@ -28,7 +28,7 @@ import (
)
var (
- version = "pda! 2025.52 Christmas release"
+ version = "pda! 2026.14"
)
// versionCmd represents the version command