Skip to main content

exsh — eXist-db Shell

Tests Coverage e2e Ruff ty docs

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_modified timestamp 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/sub also excludes build/sub/x.xml). Note that * in such a pattern also crosses / (plain fnmatch, unlike gitignore): build/*.xml matches build/sub/x.xml too.
  • A pattern without / matches any single path segment at any depth — build excludes build/, a/build/, and their contents; *.tmp excludes 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

exist_shell-0.2.0.tar.gz (181.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

exist_shell-0.2.0-py3-none-any.whl (74.8 kB view details)

Uploaded Python 3

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

Hashes for exist_shell-0.2.0.tar.gz
Algorithm Hash digest
SHA256 780635ae1c6538a3afd0327c4a1a2eb171c04bf3deef14192b2b2e0ef4b469ee
MD5 ed625192fe4c54451ffa914e90fccfd3
BLAKE2b-256 5ffff68de7d5c4973f13eae890260099e56635e4556829ade0a9ee11a1143698

See more details on using hashes here.

Provenance

The following attestation bundles were made for exist_shell-0.2.0.tar.gz:

Publisher: release.yml on ambs/exist-shell

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for exist_shell-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f110025ca11e130860994004c09129dce37d1c14305fcd868d0917895edb27d0
MD5 7def73077810af5717f92d846dd8faf3
BLAKE2b-256 7bf8a33b83eaa9a1f9350ce67c8df7db98cbce56f8855e65c6b46c874054ef1d

See more details on using hashes here.

Provenance

The following attestation bundles were made for exist_shell-0.2.0-py3-none-any.whl:

Publisher: release.yml on ambs/exist-shell

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page