exsh — eXist-db Shell
A command-line tool to interact with an eXist-db server via its REST API. Designed for shell scripting and pipe-friendly workflows.
Requirements
- Python 3.11+
- uv
Installation
Install system-wide with uv tool:
uv tool install exist-shell
Or with pipx:
pipx install exist-shell
This places exsh on your PATH. Verify with:
exsh --version
To uninstall:
uv tool uninstall exist-shell
Run without installing
uvx --from exist-shell exsh --version
uvx fetches the package into a temporary, cached environment and runs it — handy for one-off use or trying out a new release.
Install from git
To track an unreleased commit instead of a PyPI release:
uv tool install git+https://github.com/ambs/exist-shell
Configuration
Add a server
exsh server add localhost --port 8080 --user admin
A nickname is derived from the hostname by default (e.g. localhost). Override with --nick.
Add an existing collection
exsh collection add mydata@localhost
This registers the /db/mydata collection on the localhost server under the nick mydata.
Create and register a new collection
exsh collection new mydata@localhost
Creates /db/mydata on the server and registers it in one step. If the collection already exists, it prints a message and exits without modifying the config. Use --nick to register it under a different name:
exsh collection new mydata@localhost --nick md
List configured servers and collections:
exsh server ls
exsh collection ls
Configuration is stored at ~/.config/exsh/config.toml.
Commands
| Command | Description |
|---|---|
exsh ls <nick>[:<path>] |
List subcollections and documents at a path |
exsh cat <nick>:<path> |
Print a document to stdout |
exsh put <file> <nick>:<path> |
Upload a local file to a collection |
exsh cp <src> <dst> |
Copy a document (local ↔ remote or remote ↔ remote) |
exsh edit <nick>:<path> |
Open a document in $EDITOR, re-upload if changed |
exsh rm <nick>:<path>... |
Delete one or more documents; --recursive/-r (with --yes/-y) to delete a collection |
exsh mkdir <nick>:<path> |
Create a collection |
exsh sync <local> <nick>[:<path>] |
Push a local folder to a remote collection |
exsh sync <nick>[:<path>] <local> |
Pull a remote collection to a local folder |
exsh exec <nick>[:<path>] |
Execute an XQuery script on a server |
exsh exec --resource <nick>:<path.xql> |
Execute a stored resource in place, forwarding -p name=value params |
exsh find <nick>[:<path>] --query <xpath> |
Find documents matching an XPath expression, with optional --remove |
exsh server add <host> |
Register a server |
exsh server ls |
List registered servers |
exsh server rm <nick> |
Remove a server (and its collections) |
exsh server rename <old> <new> |
Rename a server nick (updates collection references) |
exsh collection add <name>[@<server>] |
Register an existing collection |
exsh collection new <name>[@<server>] |
Create a collection on the server and register it |
exsh collection ls |
List registered collections |
exsh collection rm <nick> |
Remove a collection from the config |
exsh user ls [@server] |
List user accounts and their groups |
exsh user add <user[@server]> |
Create a user account (prompts for password) |
exsh user rm <user[@server]> |
Remove a user account |
exsh user info <user[@server]> |
Show user account details |
exsh group ls [@server] |
List groups and their members |
exsh group add <group[@server]> |
Create a group |
exsh group rm <group[@server]> |
Remove a group |
exsh chown <spec> <nick>:<path> |
Change owner and/or group of a document or collection |
exsh chmod <mode> <nick>:<path> |
Change POSIX permissions of a document or collection |
Examples
# List the root of a collection
exsh ls mydata
# List a subdirectory
exsh ls mydata:reports/2025
# Print a document
exsh cat mydata:reports/2025/summary.xml
# Upload a file
exsh put report.xml mydata:reports/2025/report.xml
# Copy from remote to local
exsh cp mydata:reports/2025/report.xml ./report.xml
# Edit in place
exsh edit mydata:reports/2025/report.xml
# Delete a document
exsh rm mydata:reports/2025/old.xml
# Delete multiple documents
exsh rm mydata:reports/2025/a.xml mydata:reports/2025/b.xml
# Delete a whole collection (prompts unless --yes is also given)
exsh rm --recursive mydata:reports/2025
# Create a subcollection
exsh mkdir mydata:reports/2026
# Execute an XQuery script from a file
exsh exec mydata:/ -f query.xq
# Execute an XQuery script from stdin
echo 'count(collection("/db/mydata"))' | exsh exec mydata:/
# Execute without local preprocessing
exsh exec mydata:/ --no-fix -f query.xq
# List locally available XQuery validators
exsh exec --list-validators
# Execute a stored .xql/.xqm resource in place, rather than downloading it first
exsh exec --resource mydata:/report.xql
# ...forwarding query-string parameters as external variables
exsh exec --resource mydata:/report.xql -p from=2026-01-01 -p to=2026-12-31
# List documents containing a matching element anywhere in their tree
exsh find mydata:reports --query 'foo[@type="draft"]'
# Delete every match, skipping the confirmation prompt
exsh find mydata:reports --query 'foo[@type="draft"]' --remove --yes
# Push a local folder to the server (only transfers changed files)
exsh sync ./reports mydata:reports
# Pull a remote collection to a local folder
exsh sync mydata:reports ./reports
# Preview what would be transferred without doing it
exsh sync --dry-run ./reports mydata:reports
# Push and remove files on the server that no longer exist locally
exsh sync --delete ./reports mydata:reports
# Sync everything except temp files and the build folder (remembered for later runs)
exsh sync --exclude '*.tmp' --exclude build ./reports mydata:reports
# Change owner and group of a document
exsh chown alice:editors mydata:reports/annual.xml
# Recursively reassign a collection tree
exsh chown -R alice mydata:reports
# Set permissions with octal mode
exsh chmod 0644 mydata:reports/annual.xml
# Add execute permission for the owner
exsh chmod u+x mydata:scripts/run.xq
# Recursively set permissions for a collection
exsh chmod -R 0644 mydata:data
Sync
exsh sync transfers only files that have changed, using a local manifest stored at ~/.cache/exsh/sync/. Direction is inferred from the argument order: local-first means push, remote-first means pull.
Change detection:
- Push: SHA-256 hash of the local file is compared against the manifest. Same-size edits are caught.
- Pull:
last_modifiedtimestamp from the eXist listing is compared against the manifest.
Conflicts (both sides changed since last sync) are reported and skipped — use --force to override.
Excludes: --exclude/-e <pattern> (repeatable) skips matching paths on both sides of the sync — they are neither transferred nor ever deleted by --delete. Patterns are fnmatch-style (*, ?, [seq]), matched against the path relative to the sync root:
- A pattern containing
/matches that relative path and everything below it (build/subalso excludesbuild/sub/x.xml). Note that*in such a pattern also crosses/(plainfnmatch, unlike gitignore):build/*.xmlmatchesbuild/sub/x.xmltoo. - A pattern without
/matches any single path segment at any depth —buildexcludesbuild/,a/build/, and their contents;*.tmpexcludes temp files everywhere.
Patterns are persisted in the sync manifest, per (server, remote path, local folder), so later runs keep excluding without repeating the flag. New --exclude patterns are merged into the stored list; --clear-exclude wipes it (combine both to replace the list in one run). Files that were synced before becoming excluded are deleted on both sides after a confirmation prompt — --yes skips the prompt, declining (or running non-interactively without --yes) keeps the files, and --keep-excluded keeps them without asking. In every case they stop being tracked.
Options:
| Flag | Effect |
|---|---|
--force / -f |
Transfer all files, bypassing change detection |
--dry-run / -n |
Show what would happen without transferring |
--fail-fast |
Stop on the first conflict, invalid XML, or transfer error (runs sequentially; the manifest is saved so the run can resume) |
--jobs N / -j |
Number of parallel transfer workers (default: 4); --jobs 1 for fully sequential behaviour |
--delete |
Remove files and empty folders on the destination that no longer exist on the source |
--exclude PATTERN / -e |
Skip matching paths on both sides (repeatable); merged into the list stored for this sync pair |
--clear-exclude |
Wipe the stored exclude list before applying any --exclude given in the same run |
--keep-excluded |
Keep previously synced copies of newly excluded paths; only stop tracking them |
--yes / -y |
Skip the confirmation prompt when deleting previously synced, newly excluded files |
--verbose / -v |
Also print unchanged (skipped) files |
--checkpoint-every N |
Flush the manifest every N files (default: 100); allows interrupted syncs to resume near the point of failure |
Shell completion
Generate and install tab-completion for your shell:
# bash
exsh --install-completion bash
# zsh
exsh --install-completion zsh
# fish
exsh --install-completion fish
Upgrading? Re-run exsh --install-completion bash to pick up fixes to the generated script — it's written once to ~/.bash_completions/ (or equivalent) and isn't updated automatically.
Development
git clone https://github.com/ambs/exist-shell
cd exist-shell
uv sync
exsh --help
Run checks:
make checks # lint, type-check, and tests
make test # tests only
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file exist_shell-0.2.0.tar.gz.
File metadata
- Download URL: exist_shell-0.2.0.tar.gz
- Upload date:
- Size: 181.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
780635ae1c6538a3afd0327c4a1a2eb171c04bf3deef14192b2b2e0ef4b469ee
|
|
| MD5 |
ed625192fe4c54451ffa914e90fccfd3
|
|
| BLAKE2b-256 |
5ffff68de7d5c4973f13eae890260099e56635e4556829ade0a9ee11a1143698
|
Provenance
The following attestation bundles were made for exist_shell-0.2.0.tar.gz:
Publisher:
release.yml on ambs/exist-shell
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
exist_shell-0.2.0.tar.gz -
Subject digest:
780635ae1c6538a3afd0327c4a1a2eb171c04bf3deef14192b2b2e0ef4b469ee - Sigstore transparency entry: 2270161019
- Sigstore integration time:
-
Permalink:
ambs/exist-shell@0e28eb51a0b123520da3d93dd045fde94fa90878 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/ambs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0e28eb51a0b123520da3d93dd045fde94fa90878 -
Trigger Event:
push
-
Statement type:
File details
Details for the file exist_shell-0.2.0-py3-none-any.whl.
File metadata
- Download URL: exist_shell-0.2.0-py3-none-any.whl
- Upload date:
- Size: 74.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f110025ca11e130860994004c09129dce37d1c14305fcd868d0917895edb27d0
|
|
| MD5 |
7def73077810af5717f92d846dd8faf3
|
|
| BLAKE2b-256 |
7bf8a33b83eaa9a1f9350ce67c8df7db98cbce56f8855e65c6b46c874054ef1d
|
Provenance
The following attestation bundles were made for exist_shell-0.2.0-py3-none-any.whl:
Publisher:
release.yml on ambs/exist-shell
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
exist_shell-0.2.0-py3-none-any.whl -
Subject digest:
f110025ca11e130860994004c09129dce37d1c14305fcd868d0917895edb27d0 - Sigstore transparency entry: 2270161555
- Sigstore integration time:
-
Permalink:
ambs/exist-shell@0e28eb51a0b123520da3d93dd045fde94fa90878 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/ambs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0e28eb51a0b123520da3d93dd045fde94fa90878 -
Trigger Event:
push
-
Statement type: