exist-shell 0.1.2__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/docs.yml +2 -2
- {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/e2e.yml +4 -4
- {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/release.yml +2 -2
- {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/ruff.yml +3 -3
- {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/tests.yml +3 -3
- {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/ty.yml +4 -4
- {exist_shell-0.1.2 → exist_shell-0.2.0}/.gitignore +2 -0
- exist_shell-0.2.0/CHANGELOG.md +83 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/Makefile +2 -2
- {exist_shell-0.1.2 → exist_shell-0.2.0}/PKG-INFO +37 -2
- {exist_shell-0.1.2 → exist_shell-0.2.0}/README.md +36 -1
- {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/commands.md +77 -1
- {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/sync.md +25 -1
- {exist_shell-0.1.2 → exist_shell-0.2.0}/pyproject.toml +5 -5
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T05_put.sh +7 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T07_cat.sh +9 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T08_cp.sh +35 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T09_rm.sh +23 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T12_sync.sh +100 -1
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T13_mv.sh +37 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T14_exec.sh +43 -0
- exist_shell-0.2.0/scripts/e2e/sections/T19_find.sh +68 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e.sh +2 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/cache.py +95 -12
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/__init__.py +7 -3
- exist_shell-0.2.0/src/exist_shell/client/_base.py +102 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_collections.py +62 -13
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_documents.py +64 -23
- exist_shell-0.2.0/src/exist_shell/client/_queries.py +105 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/cat.py +1 -1
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/chmod.py +15 -20
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/chown.py +15 -20
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/cp.py +5 -1
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/exec.py +45 -8
- exist_shell-0.2.0/src/exist_shell/commands/find.py +64 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/mv.py +7 -0
- exist_shell-0.2.0/src/exist_shell/commands/rm.py +47 -0
- exist_shell-0.2.0/src/exist_shell/commands/sync.py +1405 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/completions.py +126 -12
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/config.py +34 -2
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/exceptions.py +28 -1
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/main.py +20 -1
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/utils.py +44 -7
- exist_shell-0.2.0/tests/__init__.py +1 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/conftest.py +8 -0
- exist_shell-0.2.0/tests/test_cache.py +335 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_client.py +584 -5
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_cat.py +19 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_chmod.py +38 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_chown.py +27 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_collection.py +36 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_cp.py +34 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_edit.py +14 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_exec.py +101 -1
- exist_shell-0.2.0/tests/test_commands_find.py +146 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_group.py +37 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_ls.py +17 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_mkdir.py +7 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_mv.py +51 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_put.py +17 -0
- exist_shell-0.2.0/tests/test_commands_rm.py +129 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_server.py +35 -0
- exist_shell-0.2.0/tests/test_commands_sync.py +1791 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_user.py +68 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_completions.py +154 -17
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_config.py +104 -6
- exist_shell-0.2.0/tests/test_exceptions.py +55 -0
- exist_shell-0.2.0/tests/test_main.py +45 -0
- exist_shell-0.2.0/tests/test_utils.py +258 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_xquery.py +32 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/uv.lock +48 -48
- exist_shell-0.1.2/CHANGELOG.md +0 -58
- exist_shell-0.1.2/src/exist_shell/client/_base.py +0 -65
- exist_shell-0.1.2/src/exist_shell/client/_queries.py +0 -37
- exist_shell-0.1.2/src/exist_shell/commands/rm.py +0 -23
- exist_shell-0.1.2/src/exist_shell/commands/sync.py +0 -767
- exist_shell-0.1.2/tests/__init__.py +0 -0
- exist_shell-0.1.2/tests/test_cache.py +0 -181
- exist_shell-0.1.2/tests/test_commands_rm.py +0 -66
- exist_shell-0.1.2/tests/test_commands_sync.py +0 -636
- exist_shell-0.1.2/tests/test_utils.py +0 -141
- {exist_shell-0.1.2 → exist_shell-0.2.0}/.gitguardian.yaml +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/dependabot.yml +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/CLAUDE.md +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/LICENSE +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/api.md +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/completion.md +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/configuration.md +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/development.md +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/index.md +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/installation.md +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/mkdocs.yml +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/docker.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/lib.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T02_server.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T03_collection.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T04_ls.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T06_ls_after.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T10_mkdir.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T11_edit.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T15_user.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T16_group.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T17_chown.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T18_chmod.sh +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/__init__.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_groups.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_permissions.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_users.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/__init__.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/collection.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/edit.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/group.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/ls.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/mkdir.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/put.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/server.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/user.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/models.py +0 -0
- {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/xquery.py +0 -0
|
@@ -16,10 +16,10 @@ jobs:
|
|
|
16
16
|
deploy:
|
|
17
17
|
runs-on: ubuntu-latest
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
20
20
|
|
|
21
21
|
- name: Set up Python
|
|
22
|
-
uses: actions/setup-python@
|
|
22
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
23
23
|
with:
|
|
24
24
|
python-version: "3.13"
|
|
25
25
|
|
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
outputs:
|
|
15
15
|
flags: ${{ steps.list.outputs.flags }}
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
18
18
|
- name: Build image matrix from --list-images
|
|
19
19
|
id: list
|
|
20
20
|
run: |
|
|
@@ -31,9 +31,9 @@ jobs:
|
|
|
31
31
|
flag: ${{ fromJson(needs.matrix.outputs.flags) }}
|
|
32
32
|
fail-fast: false
|
|
33
33
|
steps:
|
|
34
|
-
- uses: actions/checkout@
|
|
34
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
35
35
|
- run: sudo apt-get install -y libxml2-utils
|
|
36
|
-
- uses: astral-sh/setup-uv@
|
|
36
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
37
37
|
with:
|
|
38
38
|
enable-cache: true
|
|
39
39
|
- run: uv sync
|
|
@@ -88,7 +88,7 @@ jobs:
|
|
|
88
88
|
done
|
|
89
89
|
} > comment.md
|
|
90
90
|
cat comment.md
|
|
91
|
-
- uses: marocchino/sticky-pull-request-comment@
|
|
91
|
+
- uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2
|
|
92
92
|
with:
|
|
93
93
|
header: e2e
|
|
94
94
|
path: comment.md
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
if: github.actor == 'ambs'
|
|
16
16
|
environment: release
|
|
17
17
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
18
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
19
19
|
|
|
20
20
|
- name: Verify signed tag
|
|
21
21
|
run: |
|
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
env:
|
|
25
25
|
GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }}
|
|
26
26
|
|
|
27
|
-
- uses: astral-sh/setup-uv@
|
|
27
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
28
28
|
with:
|
|
29
29
|
enable-cache: true
|
|
30
30
|
|
|
@@ -12,8 +12,8 @@ jobs:
|
|
|
12
12
|
ruff:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
16
|
-
- uses: astral-sh/setup-uv@
|
|
15
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
16
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
17
17
|
with:
|
|
18
18
|
enable-cache: true
|
|
19
19
|
- run: uv sync
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
echo '```'
|
|
38
38
|
} > comment.md
|
|
39
39
|
fi
|
|
40
|
-
- uses: marocchino/sticky-pull-request-comment@
|
|
40
|
+
- uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2
|
|
41
41
|
if: github.event_name == 'pull_request'
|
|
42
42
|
with:
|
|
43
43
|
header: ruff
|
|
@@ -12,8 +12,8 @@ jobs:
|
|
|
12
12
|
test:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
16
|
-
- uses: astral-sh/setup-uv@
|
|
15
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
16
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
17
17
|
with:
|
|
18
18
|
enable-cache: true
|
|
19
19
|
- run: uv sync
|
|
@@ -34,7 +34,7 @@ jobs:
|
|
|
34
34
|
echo "**${test_line}** "
|
|
35
35
|
echo "**Coverage: ${coverage}**"
|
|
36
36
|
} > comment.md
|
|
37
|
-
- uses: marocchino/sticky-pull-request-comment@
|
|
37
|
+
- uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2
|
|
38
38
|
if: github.event_name == 'pull_request'
|
|
39
39
|
with:
|
|
40
40
|
header: tests
|
|
@@ -12,15 +12,15 @@ jobs:
|
|
|
12
12
|
typecheck:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
16
|
-
- uses: astral-sh/setup-uv@
|
|
15
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
16
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
17
17
|
with:
|
|
18
18
|
enable-cache: true
|
|
19
19
|
- run: uv sync
|
|
20
20
|
- name: Run ty
|
|
21
21
|
id: ty
|
|
22
22
|
continue-on-error: true
|
|
23
|
-
run: uv run ty check src/ 2>&1 | tee ty.log
|
|
23
|
+
run: uv run ty check src/ tests/ 2>&1 | tee ty.log
|
|
24
24
|
- name: Build PR comment
|
|
25
25
|
if: github.event_name == 'pull_request'
|
|
26
26
|
run: |
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
echo '```'
|
|
38
38
|
} > comment.md
|
|
39
39
|
fi
|
|
40
|
-
- uses: marocchino/sticky-pull-request-comment@
|
|
40
|
+
- uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2
|
|
41
41
|
if: github.event_name == 'pull_request'
|
|
42
42
|
with:
|
|
43
43
|
header: ty
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 - 2026-07-28
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- `config.toml` (which stores server passwords in plaintext) was written with default umask permissions — typically world-readable on multi-user machines. It is now written `0600` with the config directory at `0700`, and `exsh` warns on stderr when an existing config file is group/world-readable
|
|
8
|
+
- Non-401/404 HTTP errors (most notably `403 Forbidden`) leaked a raw `httpx` traceback; they now exit cleanly with a one-line error message. `cp` local→remote also gained the XML well-formedness check that `put` and `sync` already had
|
|
9
|
+
- The sync manifest was keyed only by the remote path, so syncing the same remote collection against two different local directories shared one manifest and silently corrupted each other's per-file state (false conflicts, wrong skip decisions). The manifest key now includes the resolved local directory; existing manifests are re-keyed via a one-time full recheck on the next sync
|
|
10
|
+
- A single failed transfer during sync left the rest of the queue finishing silently in the background with no progress output, ending in a generic unattributed error. Failures are now reported per file (`! <path> (error: ...)`) as they happen, the remaining queue keeps draining, the summary shows a `failed` count, and sync exits non-zero when anything failed
|
|
11
|
+
- `is_remote()` classified any argument containing `:` as a `nick:path` remote target, so Windows paths (`C:\data\doc.xml`) and any local path with a colon in a directory component were always misparsed as a remote nick. A prefix is now only treated as a nick when it matches a configured collection, isn't a single-letter drive prefix (`C:\`, `C:/`), and doesn't itself contain a path separator. Server/collection nicks must now be at least 2 characters, guaranteeing a configured nick can never collide with a Windows drive letter
|
|
12
|
+
- `mv` of a collection onto itself (`exsh mv nick:/a nick:/a`) or into itself via a trailing slash (`exsh mv nick:/a nick:/a/`) copied the contents onto/into the source and then recursively deleted it, destroying the data. `mv` now aborts with an error when the target is the same as, or nested inside, the source, for both collections and single documents
|
|
13
|
+
- `cat`/`cp`/`mv`/`edit`/`sync` downloading an executable resource (`.xql`, `.xqm`) ran the query on the server and returned its result instead of the document's raw source
|
|
14
|
+
- The above fix relied on eXist's `_source=yes` REST parameter, which requires the path to be explicitly allowlisted in the server's `descriptor.xml` — not the default for anything under `/db`, regardless of the caller's own read permission. This returned `403 Forbidden` for every executable-resource download (`.xq`, `.xql`, `.xqm`, `.xquery`, `.xqy`, `.xqws`) on an unconfigured (i.e. most) server. These resources are now fetched via `util:binary-doc()`, which only needs the read + query-eval permission already required to execute them
|
|
15
|
+
- `rm <nick>:<path>` on a path that is a collection silently deleted the entire subtree with no confirmation, since eXist's `DELETE` is recursive on collections. `rm` now refuses a collection target unless `--recursive`/`-r` is given, and prompts for confirmation unless `--yes`/`-y` is also passed
|
|
16
|
+
- `create_collection`, `is_collection`, and `move_document` embedded paths/names into generated XQuery without escaping `"`/`&`, unlike every other client method — a path or name containing either broke the query (or, in `move_document`, changed its meaning)
|
|
17
|
+
- `rm` and `find --remove` never invalidated the completion cache after deleting, so deleted documents kept appearing in tab completion until the cache TTL expired
|
|
18
|
+
|
|
19
|
+
### Enhancements
|
|
20
|
+
|
|
21
|
+
- **Independent client timeouts**: connect (10s), read (30s), and write (10s) now have separate budgets instead of one flat 30s timeout for every phase, and timeout errors say which phase failed — "Cannot connect to ..." for handshake failures vs "Server at ... did not respond in time" for read/write timeouts.
|
|
22
|
+
- **Faster, correct bash tab-completion for `nick:path` targets**: the generated bash completion script now handles the `:` word-break correctly (no more `dlp:dlp:...` insertions), completing a collection no longer needs an extra backspace before continuing into it, and completion listings are filtered and capped server-side and cached per prefix so progressive typing reuses a single fetch. Re-run `exsh --install-completion bash` after upgrading to pick up the new script.
|
|
23
|
+
- **Exit code 130 on Ctrl+C** for the whole CLI (previously sync-only), including during shell completion itself, which Typer/click don't handle on their own.
|
|
24
|
+
- **Parallel sync (`--jobs N`)**: uploads, downloads, and remote directory listings now run concurrently. The `--jobs N` flag (default: 4) controls the number of parallel workers. Use `--jobs 1` to restore fully sequential behaviour.
|
|
25
|
+
- **Clean Ctrl+C handling**: interrupting a sync now exits with code 130, saves the manifest (so the next run can resume), and prints a single `Interrupted.` message instead of a Python traceback.
|
|
26
|
+
- **`exsh find`**: locate documents by XPath expression anywhere in their content (`exsh find <nick>[:<path>] --query 'foo[@type="draft"]'`), with `--remove`/`--yes` to delete every match in one step.
|
|
27
|
+
- **`exsh exec --resource <nick>:<path.xql>`**: execute a stored `.xql`/`.xqm` resource in place (a plain `GET`, matching how eXist runs these resources natively) instead of downloading it and re-running it locally. Repeat `-p/--param NAME=VALUE` to forward query-string parameters as external variables.
|
|
28
|
+
- **`exsh sync --exclude`**: repeatable glob patterns to skip paths on both sides of a sync (`--exclude '*.tmp' --exclude build`). A pattern with `/` matches that relative path and its subtree; one without `/` matches any path segment at any depth. Patterns persist in the sync manifest per (server, remote path, local folder), so later runs keep excluding without the flag; new patterns merge into the stored list and `--clear-exclude` resets it. Excluded paths are never transferred and never deleted by `--delete`. Files synced before becoming excluded are deleted on both sides after confirmation (`--yes` skips the prompt, declining keeps the files, `--keep-excluded` keeps them without asking); either way they stop being tracked.
|
|
29
|
+
|
|
30
|
+
## 0.1.2 - 2026-06-23
|
|
31
|
+
|
|
32
|
+
### Documentation
|
|
33
|
+
|
|
34
|
+
- Document installing `exsh` from PyPI (`uv tool install`, `pipx install`, `uvx --from`); the existing `git+https://...` instructions are kept as an alternative for tracking unreleased commits
|
|
35
|
+
|
|
36
|
+
## 0.1.1 - 2026-06-23
|
|
37
|
+
|
|
38
|
+
### Fixes
|
|
39
|
+
|
|
40
|
+
- Complete PyPI project metadata: `readme`, `license` (MIT), `authors`, `classifiers`, `project.urls`, and `keywords` were all missing, leaving the 0.1.0 PyPI listing with no description, license, or links
|
|
41
|
+
- Add `LICENSE` file (MIT)
|
|
42
|
+
|
|
43
|
+
## 0.1.0 - 2026-06-23
|
|
44
|
+
|
|
45
|
+
### Commands
|
|
46
|
+
|
|
47
|
+
| Command | Description |
|
|
48
|
+
|---------|-------------|
|
|
49
|
+
| `server add / ls / rm` | Register, list, and remove eXist-db server configurations; `rm` cascade-removes associated collections |
|
|
50
|
+
| `server rename <old> <new>` | Rename a server nick; all registered collections are updated automatically |
|
|
51
|
+
| `collection add / new / ls / rm` | Manage named collection shortcuts; `new` creates the collection on the server and registers it in one step (idempotent: no-op if already exists) |
|
|
52
|
+
| `ls [path]` | List documents and sub-collections at a remote path; `--sort name\|time`, `--reverse`, `--names-only` |
|
|
53
|
+
| `cat <path>` | Print a remote document to stdout; `--raw` skips binary detection |
|
|
54
|
+
| `put <path>` | Upload a document from stdin or a local file; auto-detects MIME type |
|
|
55
|
+
| `cp <src> <dst>` | Copy documents local↔remote or remote↔remote |
|
|
56
|
+
| `rm <path...>` | Delete one or more remote documents |
|
|
57
|
+
| `mkdir <path>` | Create a remote collection (idempotent) |
|
|
58
|
+
| `edit <path>` | Download, open in `$VISUAL`/`$EDITOR`, re-upload if changed |
|
|
59
|
+
| `sync push/pull <dir> <path>` | Sync a local directory tree with a remote collection; supports `--delete`, `--dry-run`, `--force` |
|
|
60
|
+
| `mv <src> <dst>` | Move or rename a document or collection; trailing `/` on dest moves into that collection; cross-server moves not supported |
|
|
61
|
+
| `exec <nick>[:<path>]` | Read an XQuery script from a file or stdin, optionally preprocess it (version declaration, functx import), validate locally with BaseX or Saxon (auto-detected via PATH), then execute against eXist and print the result; supports `--no-fix`, `--no-validate`, `--validator`, `--list-validators` |
|
|
62
|
+
| `user ls` | List user accounts and their groups; accepts `@server` positional |
|
|
63
|
+
| `user add <user[@server]>` | Create a user account (prompts for password) |
|
|
64
|
+
| `user rm <user[@server]>` | Remove a user account |
|
|
65
|
+
| `user info <user[@server]>` | Show user account details |
|
|
66
|
+
| `user passwd <user[@server]>` | Change a user's password; `--stdin` for scripting |
|
|
67
|
+
| `group ls` | List groups and their members; accepts `@server` positional |
|
|
68
|
+
| `group add <group[@server]>` | Create a group |
|
|
69
|
+
| `group rm <group[@server]>` | Remove a group |
|
|
70
|
+
| `chown <spec> <path>` | Change owner and/or group of a document or collection; `--recursive / -R` for collections; spec forms: `owner`, `:group`, `owner:group` |
|
|
71
|
+
| `chmod <mode> <path>` | Change POSIX permissions of a document or collection; `--recursive / -R` for collections; accepts octal (`0755`) or symbolic (`u+x`, `go-w`, `a=rw`) modes |
|
|
72
|
+
|
|
73
|
+
### Infrastructure
|
|
74
|
+
|
|
75
|
+
- Shell completion for collection/document paths (with TTL cache)
|
|
76
|
+
- `<name>@<server>` syntax for ad-hoc server targeting in `collection add`, `collection new`, `user *`, and `group *`
|
|
77
|
+
- `--server` option on all commands completes registered server nicks
|
|
78
|
+
- Shell completion for `<name>@<server>` and `@<server>` argument forms
|
|
79
|
+
- Path traversal validation and URL encoding
|
|
80
|
+
- Google-style docstrings enforced via ruff
|
|
81
|
+
- CI: pytest + coverage (Codecov), ruff, ty, e2e test suite against live eXist-db in Docker
|
|
82
|
+
- CI: sticky PR comments for tests (counts + coverage), ruff, ty, and e2e workflows
|
|
83
|
+
- CI: e2e workflow with dynamic image matrix from `--list-images`; `fail-fast: false`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: exist-shell
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Command-line tool to interact with eXist-db via REST
|
|
5
5
|
Project-URL: Homepage, https://github.com/ambs/exist-shell
|
|
6
6
|
Project-URL: Repository, https://github.com/ambs/exist-shell
|
|
@@ -135,11 +135,13 @@ Configuration is stored at `~/.config/exsh/config.toml`.
|
|
|
135
135
|
| `exsh put <file> <nick>:<path>` | Upload a local file to a collection |
|
|
136
136
|
| `exsh cp <src> <dst>` | Copy a document (local ↔ remote or remote ↔ remote) |
|
|
137
137
|
| `exsh edit <nick>:<path>` | Open a document in `$EDITOR`, re-upload if changed |
|
|
138
|
-
| `exsh rm <nick>:<path>...` | Delete one or more documents |
|
|
138
|
+
| `exsh rm <nick>:<path>...` | Delete one or more documents; `--recursive`/`-r` (with `--yes`/`-y`) to delete a collection |
|
|
139
139
|
| `exsh mkdir <nick>:<path>` | Create a collection |
|
|
140
140
|
| `exsh sync <local> <nick>[:<path>]` | Push a local folder to a remote collection |
|
|
141
141
|
| `exsh sync <nick>[:<path>] <local>` | Pull a remote collection to a local folder |
|
|
142
142
|
| `exsh exec <nick>[:<path>]` | Execute an XQuery script on a server |
|
|
143
|
+
| `exsh exec --resource <nick>:<path.xql>` | Execute a stored resource in place, forwarding `-p name=value` params |
|
|
144
|
+
| `exsh find <nick>[:<path>] --query <xpath>` | Find documents matching an XPath expression, with optional `--remove` |
|
|
143
145
|
| `exsh server add <host>` | Register a server |
|
|
144
146
|
| `exsh server ls` | List registered servers |
|
|
145
147
|
| `exsh server rm <nick>` | Remove a server (and its collections) |
|
|
@@ -185,6 +187,9 @@ exsh rm mydata:reports/2025/old.xml
|
|
|
185
187
|
# Delete multiple documents
|
|
186
188
|
exsh rm mydata:reports/2025/a.xml mydata:reports/2025/b.xml
|
|
187
189
|
|
|
190
|
+
# Delete a whole collection (prompts unless --yes is also given)
|
|
191
|
+
exsh rm --recursive mydata:reports/2025
|
|
192
|
+
|
|
188
193
|
# Create a subcollection
|
|
189
194
|
exsh mkdir mydata:reports/2026
|
|
190
195
|
|
|
@@ -200,6 +205,18 @@ exsh exec mydata:/ --no-fix -f query.xq
|
|
|
200
205
|
# List locally available XQuery validators
|
|
201
206
|
exsh exec --list-validators
|
|
202
207
|
|
|
208
|
+
# Execute a stored .xql/.xqm resource in place, rather than downloading it first
|
|
209
|
+
exsh exec --resource mydata:/report.xql
|
|
210
|
+
|
|
211
|
+
# ...forwarding query-string parameters as external variables
|
|
212
|
+
exsh exec --resource mydata:/report.xql -p from=2026-01-01 -p to=2026-12-31
|
|
213
|
+
|
|
214
|
+
# List documents containing a matching element anywhere in their tree
|
|
215
|
+
exsh find mydata:reports --query 'foo[@type="draft"]'
|
|
216
|
+
|
|
217
|
+
# Delete every match, skipping the confirmation prompt
|
|
218
|
+
exsh find mydata:reports --query 'foo[@type="draft"]' --remove --yes
|
|
219
|
+
|
|
203
220
|
# Push a local folder to the server (only transfers changed files)
|
|
204
221
|
exsh sync ./reports mydata:reports
|
|
205
222
|
|
|
@@ -212,6 +229,9 @@ exsh sync --dry-run ./reports mydata:reports
|
|
|
212
229
|
# Push and remove files on the server that no longer exist locally
|
|
213
230
|
exsh sync --delete ./reports mydata:reports
|
|
214
231
|
|
|
232
|
+
# Sync everything except temp files and the build folder (remembered for later runs)
|
|
233
|
+
exsh sync --exclude '*.tmp' --exclude build ./reports mydata:reports
|
|
234
|
+
|
|
215
235
|
# Change owner and group of a document
|
|
216
236
|
exsh chown alice:editors mydata:reports/annual.xml
|
|
217
237
|
|
|
@@ -238,13 +258,26 @@ exsh chmod -R 0644 mydata:data
|
|
|
238
258
|
|
|
239
259
|
**Conflicts** (both sides changed since last sync) are reported and skipped — use `--force` to override.
|
|
240
260
|
|
|
261
|
+
**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:
|
|
262
|
+
|
|
263
|
+
- 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.
|
|
264
|
+
- A pattern without `/` matches any single path segment at any depth — `build` excludes `build/`, `a/build/`, and their contents; `*.tmp` excludes temp files everywhere.
|
|
265
|
+
|
|
266
|
+
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.
|
|
267
|
+
|
|
241
268
|
**Options:**
|
|
242
269
|
|
|
243
270
|
| Flag | Effect |
|
|
244
271
|
|------|--------|
|
|
245
272
|
| `--force` / `-f` | Transfer all files, bypassing change detection |
|
|
246
273
|
| `--dry-run` / `-n` | Show what would happen without transferring |
|
|
274
|
+
| `--fail-fast` | Stop on the first conflict, invalid XML, or transfer error (runs sequentially; the manifest is saved so the run can resume) |
|
|
275
|
+
| `--jobs N` / `-j` | Number of parallel transfer workers (default: 4); `--jobs 1` for fully sequential behaviour |
|
|
247
276
|
| `--delete` | Remove files and empty folders on the destination that no longer exist on the source |
|
|
277
|
+
| `--exclude PATTERN` / `-e` | Skip matching paths on both sides (repeatable); merged into the list stored for this sync pair |
|
|
278
|
+
| `--clear-exclude` | Wipe the stored exclude list before applying any `--exclude` given in the same run |
|
|
279
|
+
| `--keep-excluded` | Keep previously synced copies of newly excluded paths; only stop tracking them |
|
|
280
|
+
| `--yes` / `-y` | Skip the confirmation prompt when deleting previously synced, newly excluded files |
|
|
248
281
|
| `--verbose` / `-v` | Also print unchanged (skipped) files |
|
|
249
282
|
| `--checkpoint-every N` | Flush the manifest every N files (default: 100); allows interrupted syncs to resume near the point of failure |
|
|
250
283
|
|
|
@@ -263,6 +296,8 @@ exsh --install-completion zsh
|
|
|
263
296
|
exsh --install-completion fish
|
|
264
297
|
```
|
|
265
298
|
|
|
299
|
+
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.
|
|
300
|
+
|
|
266
301
|
## Development
|
|
267
302
|
|
|
268
303
|
```bash
|
|
@@ -104,11 +104,13 @@ Configuration is stored at `~/.config/exsh/config.toml`.
|
|
|
104
104
|
| `exsh put <file> <nick>:<path>` | Upload a local file to a collection |
|
|
105
105
|
| `exsh cp <src> <dst>` | Copy a document (local ↔ remote or remote ↔ remote) |
|
|
106
106
|
| `exsh edit <nick>:<path>` | Open a document in `$EDITOR`, re-upload if changed |
|
|
107
|
-
| `exsh rm <nick>:<path>...` | Delete one or more documents |
|
|
107
|
+
| `exsh rm <nick>:<path>...` | Delete one or more documents; `--recursive`/`-r` (with `--yes`/`-y`) to delete a collection |
|
|
108
108
|
| `exsh mkdir <nick>:<path>` | Create a collection |
|
|
109
109
|
| `exsh sync <local> <nick>[:<path>]` | Push a local folder to a remote collection |
|
|
110
110
|
| `exsh sync <nick>[:<path>] <local>` | Pull a remote collection to a local folder |
|
|
111
111
|
| `exsh exec <nick>[:<path>]` | Execute an XQuery script on a server |
|
|
112
|
+
| `exsh exec --resource <nick>:<path.xql>` | Execute a stored resource in place, forwarding `-p name=value` params |
|
|
113
|
+
| `exsh find <nick>[:<path>] --query <xpath>` | Find documents matching an XPath expression, with optional `--remove` |
|
|
112
114
|
| `exsh server add <host>` | Register a server |
|
|
113
115
|
| `exsh server ls` | List registered servers |
|
|
114
116
|
| `exsh server rm <nick>` | Remove a server (and its collections) |
|
|
@@ -154,6 +156,9 @@ exsh rm mydata:reports/2025/old.xml
|
|
|
154
156
|
# Delete multiple documents
|
|
155
157
|
exsh rm mydata:reports/2025/a.xml mydata:reports/2025/b.xml
|
|
156
158
|
|
|
159
|
+
# Delete a whole collection (prompts unless --yes is also given)
|
|
160
|
+
exsh rm --recursive mydata:reports/2025
|
|
161
|
+
|
|
157
162
|
# Create a subcollection
|
|
158
163
|
exsh mkdir mydata:reports/2026
|
|
159
164
|
|
|
@@ -169,6 +174,18 @@ exsh exec mydata:/ --no-fix -f query.xq
|
|
|
169
174
|
# List locally available XQuery validators
|
|
170
175
|
exsh exec --list-validators
|
|
171
176
|
|
|
177
|
+
# Execute a stored .xql/.xqm resource in place, rather than downloading it first
|
|
178
|
+
exsh exec --resource mydata:/report.xql
|
|
179
|
+
|
|
180
|
+
# ...forwarding query-string parameters as external variables
|
|
181
|
+
exsh exec --resource mydata:/report.xql -p from=2026-01-01 -p to=2026-12-31
|
|
182
|
+
|
|
183
|
+
# List documents containing a matching element anywhere in their tree
|
|
184
|
+
exsh find mydata:reports --query 'foo[@type="draft"]'
|
|
185
|
+
|
|
186
|
+
# Delete every match, skipping the confirmation prompt
|
|
187
|
+
exsh find mydata:reports --query 'foo[@type="draft"]' --remove --yes
|
|
188
|
+
|
|
172
189
|
# Push a local folder to the server (only transfers changed files)
|
|
173
190
|
exsh sync ./reports mydata:reports
|
|
174
191
|
|
|
@@ -181,6 +198,9 @@ exsh sync --dry-run ./reports mydata:reports
|
|
|
181
198
|
# Push and remove files on the server that no longer exist locally
|
|
182
199
|
exsh sync --delete ./reports mydata:reports
|
|
183
200
|
|
|
201
|
+
# Sync everything except temp files and the build folder (remembered for later runs)
|
|
202
|
+
exsh sync --exclude '*.tmp' --exclude build ./reports mydata:reports
|
|
203
|
+
|
|
184
204
|
# Change owner and group of a document
|
|
185
205
|
exsh chown alice:editors mydata:reports/annual.xml
|
|
186
206
|
|
|
@@ -207,13 +227,26 @@ exsh chmod -R 0644 mydata:data
|
|
|
207
227
|
|
|
208
228
|
**Conflicts** (both sides changed since last sync) are reported and skipped — use `--force` to override.
|
|
209
229
|
|
|
230
|
+
**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:
|
|
231
|
+
|
|
232
|
+
- 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.
|
|
233
|
+
- A pattern without `/` matches any single path segment at any depth — `build` excludes `build/`, `a/build/`, and their contents; `*.tmp` excludes temp files everywhere.
|
|
234
|
+
|
|
235
|
+
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.
|
|
236
|
+
|
|
210
237
|
**Options:**
|
|
211
238
|
|
|
212
239
|
| Flag | Effect |
|
|
213
240
|
|------|--------|
|
|
214
241
|
| `--force` / `-f` | Transfer all files, bypassing change detection |
|
|
215
242
|
| `--dry-run` / `-n` | Show what would happen without transferring |
|
|
243
|
+
| `--fail-fast` | Stop on the first conflict, invalid XML, or transfer error (runs sequentially; the manifest is saved so the run can resume) |
|
|
244
|
+
| `--jobs N` / `-j` | Number of parallel transfer workers (default: 4); `--jobs 1` for fully sequential behaviour |
|
|
216
245
|
| `--delete` | Remove files and empty folders on the destination that no longer exist on the source |
|
|
246
|
+
| `--exclude PATTERN` / `-e` | Skip matching paths on both sides (repeatable); merged into the list stored for this sync pair |
|
|
247
|
+
| `--clear-exclude` | Wipe the stored exclude list before applying any `--exclude` given in the same run |
|
|
248
|
+
| `--keep-excluded` | Keep previously synced copies of newly excluded paths; only stop tracking them |
|
|
249
|
+
| `--yes` / `-y` | Skip the confirmation prompt when deleting previously synced, newly excluded files |
|
|
217
250
|
| `--verbose` / `-v` | Also print unchanged (skipped) files |
|
|
218
251
|
| `--checkpoint-every N` | Flush the manifest every N files (default: 100); allows interrupted syncs to resume near the point of failure |
|
|
219
252
|
|
|
@@ -232,6 +265,8 @@ exsh --install-completion zsh
|
|
|
232
265
|
exsh --install-completion fish
|
|
233
266
|
```
|
|
234
267
|
|
|
268
|
+
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.
|
|
269
|
+
|
|
235
270
|
## Development
|
|
236
271
|
|
|
237
272
|
```bash
|
|
@@ -181,11 +181,20 @@ EDITOR=nano exsh edit mydata:config.xml
|
|
|
181
181
|
Delete one or more documents from an eXist collection.
|
|
182
182
|
|
|
183
183
|
```
|
|
184
|
-
exsh rm <nick>:<path>...
|
|
184
|
+
exsh rm [--recursive] [--yes] <nick>:<path>...
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
Multiple paths can be supplied in a single invocation. Deletion is permanent — there is no undo.
|
|
188
188
|
|
|
189
|
+
A target that is itself a collection is refused by default (`Error: '<nick>:<path>' is a collection; use -r/--recursive to delete it and everything under it.`) rather than silently deleting the whole subtree. Pass `--recursive` to allow it; unless `--yes` is also given, `rm` first prompts for confirmation naming the collection being removed.
|
|
190
|
+
|
|
191
|
+
### Options
|
|
192
|
+
|
|
193
|
+
| Flag | Effect |
|
|
194
|
+
|------|--------|
|
|
195
|
+
| `--recursive / -r` | Allow a target that is a collection to be deleted, along with everything under it. |
|
|
196
|
+
| `--yes / -y` | Skip the confirmation prompt when deleting a collection. |
|
|
197
|
+
|
|
189
198
|
### Examples
|
|
190
199
|
|
|
191
200
|
```bash
|
|
@@ -194,6 +203,12 @@ exsh rm mydata:reports/2025/old.xml
|
|
|
194
203
|
|
|
195
204
|
# Delete multiple documents at once
|
|
196
205
|
exsh rm mydata:reports/2025/a.xml mydata:reports/2025/b.xml
|
|
206
|
+
|
|
207
|
+
# Delete a whole collection (prompts for confirmation)
|
|
208
|
+
exsh rm --recursive mydata:reports/2025
|
|
209
|
+
|
|
210
|
+
# Delete a whole collection without prompting
|
|
211
|
+
exsh rm --recursive --yes mydata:reports/2025
|
|
197
212
|
```
|
|
198
213
|
|
|
199
214
|
---
|
|
@@ -271,6 +286,7 @@ Execute an XQuery script on an eXist-db server and print the result to stdout.
|
|
|
271
286
|
|
|
272
287
|
```
|
|
273
288
|
exsh exec <nick>[:<path>] [-f FILE] [--no-fix] [--no-validate] [--validator NAME]
|
|
289
|
+
exsh exec --resource <nick>:<path.xql> [-p NAME=VALUE ...]
|
|
274
290
|
```
|
|
275
291
|
|
|
276
292
|
The query is read from `--file` or from stdin. Before sending, `exsh` optionally preprocesses the source and validates it locally:
|
|
@@ -291,10 +307,18 @@ The first installed validator found on `PATH` is used automatically. Use `--vali
|
|
|
291
307
|
|
|
292
308
|
Run `exsh exec --list-validators` to see which validators are available on the current machine.
|
|
293
309
|
|
|
310
|
+
### Executing a stored resource
|
|
311
|
+
|
|
312
|
+
`--resource <nick>:<path.xql>` runs a `.xql`/`.xqm` script that's already stored on the server in place, instead of sending local source. This is a plain `GET` on the resource — the same mechanism eXist uses to execute these MIME types automatically — so it relies on the resource being invoked in its own stored collection context, which matters for scripts that assume that context (e.g. relative `doc()`/`collection()` lookups).
|
|
313
|
+
|
|
314
|
+
`--resource` is mutually exclusive with the `<nick>[:<path>]` argument, and skips local preprocessing/validation entirely, since there's no local source to fix up or check. Repeat `-p/--param NAME=VALUE` to forward query-string parameters, which the resource can read as external variables.
|
|
315
|
+
|
|
294
316
|
### Options
|
|
295
317
|
|
|
296
318
|
| Flag | Description |
|
|
297
319
|
|------|-------------|
|
|
320
|
+
| `--resource <nick>:<path.xql>` | Execute a stored resource in place instead of local code. |
|
|
321
|
+
| `-p / --param NAME=VALUE` | Query-string parameter to forward with `--resource` (repeatable). |
|
|
298
322
|
| `-f / --file FILE` | XQuery file to execute. When omitted, stdin is read. |
|
|
299
323
|
| `--no-fix` | Skip preprocessing (version declaration, namespace imports). |
|
|
300
324
|
| `--no-validate` | Skip local validation even if a validator is installed. |
|
|
@@ -324,6 +348,58 @@ exsh exec mydata:/ --validator basex -f query.xq
|
|
|
324
348
|
|
|
325
349
|
# Check which validators are available locally
|
|
326
350
|
exsh exec --list-validators
|
|
351
|
+
|
|
352
|
+
# Execute a stored resource in place, rather than downloading it first
|
|
353
|
+
exsh exec --resource mydata:/report.xql
|
|
354
|
+
|
|
355
|
+
# ...forwarding query-string parameters as external variables
|
|
356
|
+
exsh exec --resource mydata:/report.xql -p from=2026-01-01 -p to=2026-12-31
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## find
|
|
362
|
+
|
|
363
|
+
Locate documents whose content matches an XPath expression, and optionally delete every match in one step.
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
exsh find <nick>[:<path>] --query <xpath> [--remove] [--yes]
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The expression given to `--query` is evaluated recursively under the target collection (equivalent to `collection(...)//(QUERY)`), and each matching document is printed as a `<nick>:<path>` target — the same format `rm` and every other command accepts. Because `rm` takes its targets as arguments (not from standard input), pipe `find` output through `xargs` to feed it into `rm`:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
exsh find mydata:reports --query 'foo[@type="draft"]' | xargs -r exsh rm
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Deleting matches directly with `--remove` is usually simpler; the pipe form is useful when you want to filter the list first.
|
|
376
|
+
|
|
377
|
+
Without `--remove`, `find` only lists matches — this is inherently a dry run. With `--remove`, matches are deleted as they're printed; unless `--yes` is also given, `find` first prompts for confirmation showing the number of matching documents. If a document reported by the search has already disappeared by the time it is deleted, `find` warns and continues, then exits non-zero.
|
|
378
|
+
|
|
379
|
+
> **Security note:** the `--query` expression is embedded into a server-side XQuery without validation or sandboxing, so it can execute arbitrary XQuery with your configured server credentials (the same trust model as `exsh exec`). Only pass expressions from a trusted source.
|
|
380
|
+
|
|
381
|
+
### Options
|
|
382
|
+
|
|
383
|
+
| Flag | Description |
|
|
384
|
+
|------|-------------|
|
|
385
|
+
| `--query / -q XPATH` | XPath expression to match, evaluated recursively under the target (required). |
|
|
386
|
+
| `--remove` | Delete matching documents instead of just listing them. |
|
|
387
|
+
| `--yes / -y` | Skip the confirmation prompt when used with `--remove`. |
|
|
388
|
+
|
|
389
|
+
### Examples
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
# List documents containing a matching element anywhere in their tree
|
|
393
|
+
exsh find mydata:reports --query 'foo[@type="draft"]'
|
|
394
|
+
|
|
395
|
+
# Match on an attribute nested deeper in the document
|
|
396
|
+
exsh find mydata:/ --query 'section/foo[@type="draft"]'
|
|
397
|
+
|
|
398
|
+
# Delete every match, with a confirmation prompt showing the match count
|
|
399
|
+
exsh find mydata:reports --query 'foo[@type="draft"]' --remove
|
|
400
|
+
|
|
401
|
+
# Delete every match, skipping the confirmation prompt
|
|
402
|
+
exsh find mydata:reports --query 'foo[@type="draft"]' --remove --yes
|
|
327
403
|
```
|
|
328
404
|
|
|
329
405
|
---
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Synchronise a local directory tree with a remote eXist collection, transferring only files that have actually changed.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
exsh sync <source> <dest> [--force] [--fail-fast] [--dry-run] [--delete] [--verbose] [--checkpoint-every N]
|
|
6
|
+
exsh sync <source> <dest> [--force] [--fail-fast] [--dry-run] [--delete] [--verbose] [--jobs N] [--checkpoint-every N]
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
## Direction
|
|
@@ -78,6 +78,7 @@ Use `--force` to override conflict detection and transfer the source uncondition
|
|
|
78
78
|
| `--dry-run / -n` | Print what would happen without moving any data |
|
|
79
79
|
| `--delete` | Remove files and empty folders on the destination that no longer exist on the source |
|
|
80
80
|
| `--verbose / -v` | Also print unchanged (skipped) files |
|
|
81
|
+
| `--jobs N / -j N` | Number of parallel transfer workers (default: 4); set to 1 for sequential |
|
|
81
82
|
| `--checkpoint-every N` | Flush the manifest to disk every N files (default: 100) |
|
|
82
83
|
|
|
83
84
|
## Output
|
|
@@ -126,6 +127,29 @@ exsh sync --delete ./reports mydata:reports
|
|
|
126
127
|
exsh sync --force ./reports mydata:reports
|
|
127
128
|
```
|
|
128
129
|
|
|
130
|
+
## Parallelism
|
|
131
|
+
|
|
132
|
+
By default `exsh sync` runs up to **4 transfers concurrently** — both for file uploads/downloads and for the initial remote directory listing (BFS walk). This significantly reduces wall-clock time when syncing collections with many small files over a high-latency connection.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Use 8 workers for a large collection
|
|
136
|
+
exsh sync --jobs 8 ./data myserver:data
|
|
137
|
+
|
|
138
|
+
# Restore sequential behaviour (one file at a time)
|
|
139
|
+
exsh sync --jobs 1 ./data myserver:data
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`--fail-fast` always runs sequentially regardless of `--jobs`, to guarantee that no file after the first failure is transferred.
|
|
143
|
+
|
|
144
|
+
### Interrupting a sync
|
|
145
|
+
|
|
146
|
+
Pressing **Ctrl+C** during a sync:
|
|
147
|
+
|
|
148
|
+
- Cancels any pending (not yet started) transfers immediately.
|
|
149
|
+
- Waits for at most `--jobs` in-flight transfers to finish (they cannot be interrupted mid-request).
|
|
150
|
+
- Saves the manifest so the next run resumes from where it stopped.
|
|
151
|
+
- Exits with code **130** (the POSIX convention for Ctrl+C).
|
|
152
|
+
|
|
129
153
|
## Large collections and resumability
|
|
130
154
|
|
|
131
155
|
For collections with many files, the manifest is flushed to disk every 100 files by default. If a sync is interrupted (network failure, process kill, etc.), the next run reads the saved manifest and skips files that were already transferred, restarting near the point of failure rather than from scratch.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "exist-shell"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Command-line tool to interact with eXist-db via REST"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -41,7 +41,7 @@ Issues = "https://github.com/ambs/exist-shell/issues"
|
|
|
41
41
|
Changelog = "https://github.com/ambs/exist-shell/blob/main/CHANGELOG.md"
|
|
42
42
|
|
|
43
43
|
[project.scripts]
|
|
44
|
-
exsh = "exist_shell.main:
|
|
44
|
+
exsh = "exist_shell.main:cli"
|
|
45
45
|
|
|
46
46
|
[tool.hatch.build.targets.wheel]
|
|
47
47
|
packages = ["src/exist_shell"]
|
|
@@ -54,9 +54,9 @@ convention = "google"
|
|
|
54
54
|
|
|
55
55
|
[dependency-groups]
|
|
56
56
|
dev = [
|
|
57
|
-
"pytest>=
|
|
57
|
+
"pytest>=9.1.1",
|
|
58
58
|
"pytest-cov>=5",
|
|
59
59
|
"pytest-httpx>=0.30",
|
|
60
|
-
"ruff>=0.15.
|
|
61
|
-
"ty>=0.0.
|
|
60
|
+
"ruff>=0.15.18",
|
|
61
|
+
"ty>=0.0.54",
|
|
62
62
|
]
|
|
@@ -8,6 +8,7 @@ section_T05_put() {
|
|
|
8
8
|
# Prepare local files (persist for T06+)
|
|
9
9
|
printf '<hello>world</hello>' > "${TMPDIR_E2E}/hello.xml"
|
|
10
10
|
printf '\x00\x01\x02\x03' > "${TMPDIR_E2E}/data.bin"
|
|
11
|
+
printf 'xquery version "3.1";\n<computed>{2 + 2}</computed>' > "${TMPDIR_E2E}/script.xq"
|
|
11
12
|
|
|
12
13
|
# T05.1 — upload XML from file
|
|
13
14
|
assert_exit0 "T05.1 put XML file" \
|
|
@@ -67,4 +68,10 @@ section_T05_put() {
|
|
|
67
68
|
# T05.12 — binary file is uploaded without XML validation
|
|
68
69
|
assert_exit0 "T05.12 put binary skips XML validation" \
|
|
69
70
|
"${EXSH[@]}" put testcol:/data.bin -f "${TMPDIR_E2E}/data.bin" --mime application/octet-stream
|
|
71
|
+
|
|
72
|
+
# T05.13 — .xq file stored as application/xquery (eXist's executable resource type;
|
|
73
|
+
# --mime is required since exsh has no built-in guess for this extension). Regression
|
|
74
|
+
# coverage for #141/#144: cat/cp must return the source below, not the query's output.
|
|
75
|
+
assert_exit0 "T05.13 put .xq file as application/xquery" \
|
|
76
|
+
"${EXSH[@]}" put testcol:/script.xq -f "${TMPDIR_E2E}/script.xq" --mime application/xquery
|
|
70
77
|
}
|