docopt2 1.0.0__tar.gz → 1.0.2__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.
- docopt2-1.0.2/.codecov.yml +10 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/ci.yml +38 -7
- docopt2-1.0.2/.github/workflows/nightly.yml +32 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/PKG-INFO +4 -3
- {docopt2-1.0.0 → docopt2-1.0.2}/README.md +2 -1
- docopt2-1.0.2/docs/assets/check.txt +7 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/coercion.png +0 -0
- docopt2-1.0.2/docs/assets/coercion.txt +11 -0
- docopt2-1.0.2/docs/assets/diagnostic.txt +11 -0
- docopt2-1.0.2/docs/assets/rich-help.txt +9 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/getting-started.md +1 -1
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/completion.md +18 -2
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/diagnostics.md +7 -4
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/typed-results.md +1 -1
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/usage-dsl.md +5 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/overview.md +0 -2
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/stylesheets/extra.css +2 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/pyproject.toml +7 -2
- docopt2-1.0.2/src/docopt2/__init__.py +109 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_completion.py +65 -20
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_core.py +62 -9
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_generate.py +1 -1
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_parser.py +55 -22
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_typed.py +3 -1
- docopt2-1.0.2/tests/test_completion_vs_docopt.py +79 -0
- docopt2-1.0.2/tests/test_config.py +197 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_config_template.py +7 -1
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_core_extra.py +6 -4
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_dispatch_and_completion.py +60 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_docopt_original.py +3 -3
- docopt2-1.0.2/tests/test_docs_blocks.py +172 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_getting_started.py +4 -2
- docopt2-1.0.2/tests/test_import_cost.py +42 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_near_miss.py +81 -5
- docopt2-1.0.2/tests/test_package.py +18 -0
- docopt2-1.0.2/tests/test_property_config.py +41 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_property_config_template.py +8 -2
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_readme.py +1 -1
- docopt2-1.0.2/tests/test_screenshots.py +19 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_typed_api.py +6 -4
- docopt2-1.0.2/tools/screenshots.py +149 -0
- docopt2-1.0.2/tools/shell_check.py +260 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/uv.lock +3 -1
- docopt2-1.0.0/src/docopt2/__init__.py +0 -80
- docopt2-1.0.0/tests/test_config.py +0 -101
- {docopt2-1.0.0 → docopt2-1.0.2}/.gitattributes +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/.github/dependabot.yml +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/codeql.yml +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/docs.yml +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/publish.yml +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/scorecard.yml +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/zizmor.yml +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/.gitignore +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/CONTRIBUTING.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/LICENSE +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/NOTICE +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/SECURITY.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/check.png +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/diagnostic.png +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/logo-dark.png +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/logo.png +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/rich-help.png +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/concepts/design-boundaries.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/check.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/compat.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/dispatch.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/examples.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/fmt.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/help.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/round-trip.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/stub.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/index.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/check.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/cli.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/compat.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/completion.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/config-templates.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/dispatch.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/docopt.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/examples.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/exceptions.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/fmt.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/grammar.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/stub.md +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/examples/cli.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/examples/completion.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/examples/dispatch.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/examples/layered.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/examples/naval_fate.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/examples/rich_help.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/examples/round_trip.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/examples/typed.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/mkdocs.yml +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/__main__.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_compat.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_diagnostics.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_errors.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_fmt.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_format.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_help.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_lint.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_spellcheck.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_stub.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/hypothesis.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/py.typed +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/_strategies.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/_vendor/docopt_original.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/conftest.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_choices.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_cli.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_coercion_diagnostics.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_compat.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_diagnostics.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_divergences.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_env.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_examples.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_extended_features.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_fmt.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_format.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_generate.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_grammar_examples.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_help.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_hypothesis.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_language_agnostic.docopt +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_lint.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_parse_tree.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_property_coercion.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_property_differential.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_property_extended.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_pydantic.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_source.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_spellcheck.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_stub.py +0 -0
- {docopt2-1.0.0 → docopt2-1.0.2}/tests/typing_surface/check_typed.py +0 -0
|
@@ -46,16 +46,19 @@ jobs:
|
|
|
46
46
|
python-version: ${{ matrix.python-version }}
|
|
47
47
|
allow-prereleases: true
|
|
48
48
|
|
|
49
|
+
# --locked: a lockfile that has drifted from pyproject.toml is an error. Plain `uv sync` would
|
|
50
|
+
# silently re-resolve and test a dependency set that is in nobody's checkout.
|
|
49
51
|
- name: Install dependencies
|
|
50
|
-
run: uv sync
|
|
52
|
+
run: uv sync --locked
|
|
51
53
|
|
|
52
54
|
- name: Test with coverage (100% gate)
|
|
53
55
|
if: matrix.coverage
|
|
54
56
|
run: uv run pytest --cov-report=xml
|
|
55
57
|
|
|
58
|
+
# --no-cov, not `-o addopts=`: clearing addopts would drop --strict-markers along with coverage.
|
|
56
59
|
- name: Test
|
|
57
60
|
if: '!matrix.coverage'
|
|
58
|
-
run: uv run pytest -
|
|
61
|
+
run: uv run pytest --no-cov
|
|
59
62
|
|
|
60
63
|
- name: Upload coverage to Codecov
|
|
61
64
|
if: matrix.coverage
|
|
@@ -80,13 +83,13 @@ jobs:
|
|
|
80
83
|
python-version: "3.14"
|
|
81
84
|
|
|
82
85
|
- name: Install dependencies
|
|
83
|
-
run: uv sync --group typecheck
|
|
86
|
+
run: uv sync --group typecheck --locked
|
|
84
87
|
|
|
85
88
|
- name: Ruff check
|
|
86
|
-
run: uv run ruff check src tests
|
|
89
|
+
run: uv run ruff check src tests tools
|
|
87
90
|
|
|
88
91
|
- name: Ruff format check
|
|
89
|
-
run: uv run ruff format --check src tests
|
|
92
|
+
run: uv run ruff format --check src tests tools
|
|
90
93
|
|
|
91
94
|
- name: Type check (ty)
|
|
92
95
|
run: uv run ty check
|
|
@@ -103,10 +106,38 @@ jobs:
|
|
|
103
106
|
- name: Type check (pyright, floor target 3.10)
|
|
104
107
|
run: uv run pyright --pythonversion 3.10 src/docopt2
|
|
105
108
|
|
|
109
|
+
# The unit tests pin what the generated completion SCRIPTS say; they never run them, and that is exactly
|
|
110
|
+
# where a completion bug hides. This drives real shells through a pty and a real Tab keypress, and holds
|
|
111
|
+
# what they offer against the grammar. It is known to fail on an unfixed script, so it guards rather than
|
|
112
|
+
# decorates. PowerShell exposes its completion engine to code, so it runs on the Windows cell.
|
|
113
|
+
shells:
|
|
114
|
+
runs-on: ${{ matrix.os }}
|
|
115
|
+
strategy:
|
|
116
|
+
fail-fast: false
|
|
117
|
+
matrix:
|
|
118
|
+
os: [ubuntu-latest, windows-latest]
|
|
119
|
+
steps:
|
|
120
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
121
|
+
with:
|
|
122
|
+
persist-credentials: false
|
|
123
|
+
|
|
124
|
+
- name: Install uv
|
|
125
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
126
|
+
|
|
127
|
+
- name: Install zsh and fish
|
|
128
|
+
if: runner.os == 'Linux'
|
|
129
|
+
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends zsh fish
|
|
130
|
+
|
|
131
|
+
- name: Complete in a real terminal
|
|
132
|
+
run: uv run --locked python tools/shell_check.py
|
|
133
|
+
|
|
106
134
|
ci-ok:
|
|
107
|
-
needs: [test, lint]
|
|
135
|
+
needs: [test, lint, shells]
|
|
108
136
|
if: always()
|
|
109
137
|
runs-on: ubuntu-latest
|
|
110
138
|
steps:
|
|
111
139
|
- name: Verify required jobs succeeded
|
|
112
|
-
run:
|
|
140
|
+
run: >-
|
|
141
|
+
[ "${{ needs.test.result }}" = "success" ]
|
|
142
|
+
&& [ "${{ needs.lint.result }}" = "success" ]
|
|
143
|
+
&& [ "${{ needs.shells.result }}" = "success" ]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Nightly
|
|
2
|
+
|
|
3
|
+
# The gate runs Hypothesis derandomised at 300 examples, deliberately: a PR check that flakes is a check
|
|
4
|
+
# people learn to ignore. The cost is that every run sees the same inputs, so a rare violation can sit
|
|
5
|
+
# there forever. This explores 10,000 randomised examples per property, off the critical path, where a
|
|
6
|
+
# failure is a finding rather than a blocked merge.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
schedule:
|
|
10
|
+
- cron: "23 3 * * *"
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ${{ github.workflow }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
explore:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
25
|
+
with:
|
|
26
|
+
persist-credentials: false
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
30
|
+
|
|
31
|
+
- name: Property tests, wide and randomised
|
|
32
|
+
run: uv run --locked pytest --hypothesis-profile=explore -q --no-cov
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docopt2
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Typed successor to docopt: the usage message is the parser spec.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/docopt2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/docopt2
|
|
@@ -11,7 +11,7 @@ License-Expression: MIT
|
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
License-File: NOTICE
|
|
13
13
|
Keywords: argument-parser,cli,command-line,docopt,posix,typed,usage
|
|
14
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
15
|
Classifier: Environment :: Console
|
|
16
16
|
Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
|
@@ -49,6 +49,7 @@ Description-Content-Type: text/markdown
|
|
|
49
49
|
<p align="center">
|
|
50
50
|
<a href="https://github.com/Solganis/docopt2/actions/workflows/ci.yml"><img src="https://github.com/Solganis/docopt2/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
51
51
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/v/docopt2" alt="PyPI version"></a>
|
|
52
|
+
<a href="https://pepy.tech/projects/docopt2"><img src="https://static.pepy.tech/badge/docopt2/month" alt="Downloads"></a>
|
|
52
53
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/pyversions/docopt2" alt="Python"></a>
|
|
53
54
|
<a href="https://codecov.io/gh/Solganis/docopt2"><img src="https://codecov.io/gh/Solganis/docopt2/graph/badge.svg" alt="Coverage"></a>
|
|
54
55
|
<br>
|
|
@@ -235,7 +236,7 @@ Usage:
|
|
|
235
236
|
The same two carets flag a [malformed usage](https://solganis.github.io/docopt2/guides/diagnostics/#a-malformed-usage-at-import-time) at import time - a broken spec fails loudly, not silently - and [a value that will not coerce](https://solganis.github.io/docopt2/guides/diagnostics/#a-value-that-does-not-fit-its-type) to its typed field:
|
|
236
237
|
|
|
237
238
|
<p align="center">
|
|
238
|
-
<img src="docs/assets/coercion.png" width="511" alt="A docopt2 error: 'invalid value for --port' with a caret under abc in the argument vector and a second caret under --port=<n> in the usage, plus '
|
|
239
|
+
<img src="docs/assets/coercion.png" width="511" alt="A docopt2 error: 'invalid value for --port' with a caret under abc in the argument vector and a second caret under --port=<n> in the usage, plus 'note: abc is not a valid int'">
|
|
239
240
|
</p>
|
|
240
241
|
|
|
241
242
|
**[Closest usage line.](https://solganis.github.io/docopt2/guides/diagnostics/#the-usage-line-you-were-closest-to)** When several invocations are possible, docopt2 finds the one you got furthest into and carets the single element it still needs - not a generic "no match".
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="https://github.com/Solganis/docopt2/actions/workflows/ci.yml"><img src="https://github.com/Solganis/docopt2/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
15
15
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/v/docopt2" alt="PyPI version"></a>
|
|
16
|
+
<a href="https://pepy.tech/projects/docopt2"><img src="https://static.pepy.tech/badge/docopt2/month" alt="Downloads"></a>
|
|
16
17
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/pyversions/docopt2" alt="Python"></a>
|
|
17
18
|
<a href="https://codecov.io/gh/Solganis/docopt2"><img src="https://codecov.io/gh/Solganis/docopt2/graph/badge.svg" alt="Coverage"></a>
|
|
18
19
|
<br>
|
|
@@ -199,7 +200,7 @@ Usage:
|
|
|
199
200
|
The same two carets flag a [malformed usage](https://solganis.github.io/docopt2/guides/diagnostics/#a-malformed-usage-at-import-time) at import time - a broken spec fails loudly, not silently - and [a value that will not coerce](https://solganis.github.io/docopt2/guides/diagnostics/#a-value-that-does-not-fit-its-type) to its typed field:
|
|
200
201
|
|
|
201
202
|
<p align="center">
|
|
202
|
-
<img src="docs/assets/coercion.png" width="511" alt="A docopt2 error: 'invalid value for --port' with a caret under abc in the argument vector and a second caret under --port=<n> in the usage, plus '
|
|
203
|
+
<img src="docs/assets/coercion.png" width="511" alt="A docopt2 error: 'invalid value for --port' with a caret under abc in the argument vector and a second caret under --port=<n> in the usage, plus 'note: abc is not a valid int'">
|
|
203
204
|
</p>
|
|
204
205
|
|
|
205
206
|
**[Closest usage line.](https://solganis.github.io/docopt2/guides/diagnostics/#the-usage-line-you-were-closest-to)** When several invocations are possible, docopt2 finds the one you got furthest into and carets the single element it still needs - not a generic "no match".
|
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<span class="dt-fg">Serve a directory over HTTP.</span>
|
|
2
|
+
|
|
3
|
+
<span class="dt-fg dt-b">Usage:</span>
|
|
4
|
+
<span class="dt-fg"> serve [--port=<n>] [--host=<h>] [--log=<lvl>] <root></span>
|
|
5
|
+
|
|
6
|
+
<span class="dt-fg dt-b">Options:</span>
|
|
7
|
+
<span class="dt-fg"> </span><span class="dt-help">--port=<n> </span><span class="dt-fg"> Port to bind.</span><span class="dt-dim"> [env: PORT, config: server.port, default: 8080]</span>
|
|
8
|
+
<span class="dt-fg"> </span><span class="dt-help">--host=<h> </span><span class="dt-fg"> Interface to bind.</span><span class="dt-dim"> [env: HOST, default: 127.0.0.1]</span>
|
|
9
|
+
<span class="dt-fg"> </span><span class="dt-help">--log=<lvl></span><span class="dt-fg"> Log verbosity.</span><span class="dt-dim"> [config: logging.level, default: info]</span>
|
|
@@ -145,7 +145,7 @@ and exits non-zero. Move a ship, but forget the second coordinate:
|
|
|
145
145
|
<span class="dt-fg"> |</span><span class="dt-fg"> naval_fate ship <name> move <x> <y> [--speed=<kn>]</span>
|
|
146
146
|
<span class="dt-fg"> |</span><span class="dt-fg"> </span><span class="dt-caret">^^^</span><span class="dt-label"> required here</span>
|
|
147
147
|
<span class="dt-fg"> |</span>
|
|
148
|
-
<span class="dt-fg"> = </span><span class="dt-
|
|
148
|
+
<span class="dt-fg"> = </span><span class="dt-note">note</span><span class="dt-fg">: of 6 usage patterns, your arguments came closest to this one</span></div>
|
|
149
149
|
|
|
150
150
|
docopt2 finds the usage line you got closest to and carets the one element it still needs (the full usage
|
|
151
151
|
is printed beneath, trimmed here). Other mismatches - an unknown option, a value that will not fit its
|
|
@@ -29,9 +29,19 @@ the usage text. For `prog="naval"`, the bash script is:
|
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
_naval_completion() {
|
|
32
|
+
local -a typed=()
|
|
33
|
+
local index part
|
|
34
|
+
for (( index = 1; index < COMP_CWORD; index++ )); do
|
|
35
|
+
part=${COMP_WORDS[index]}
|
|
36
|
+
if [[ ${#typed[@]} -gt 0 && ( $part == [:=] || ${COMP_WORDS[index-1]} == [:=] ) ]]; then
|
|
37
|
+
typed[$(( ${#typed[@]} - 1 ))]+=$part
|
|
38
|
+
else
|
|
39
|
+
typed+=("$part")
|
|
40
|
+
fi
|
|
41
|
+
done
|
|
32
42
|
local IFS=$'\n'
|
|
33
|
-
local words="${
|
|
34
|
-
local reply; reply="$(_DOCOPT2_COMPLETE=1 _DOCOPT2_WORDS="$words" "${COMP_WORDS[0]}" | cut -f1)"
|
|
43
|
+
local words="${typed[*]}"
|
|
44
|
+
local reply; reply="$(_DOCOPT2_COMPLETE=1 _DOCOPT2_WORDS="$words" "${COMP_WORDS[0]}" 2>/dev/null | cut -f1)"
|
|
35
45
|
COMPREPLY=( $(compgen -W "$reply" -- "${COMP_WORDS[COMP_CWORD]}") )
|
|
36
46
|
}
|
|
37
47
|
complete -F _naval_completion naval
|
|
@@ -40,6 +50,12 @@ complete -F _naval_completion naval
|
|
|
40
50
|
It invokes the command the user typed (`${COMP_WORDS[0]}`), so `naval` must be on `PATH` under the name
|
|
41
51
|
you passed as `prog`.
|
|
42
52
|
|
|
53
|
+
The loop is not decoration. `COMP_WORDBREAKS` contains `=` and `:`, so bash hands the completion function
|
|
54
|
+
`--port=8080` as three separate words - `--port`, `=`, `8080` - and `host:port` as three more. Passing those
|
|
55
|
+
shards to the program would destroy the parse context and silently return no candidates at all for the rest
|
|
56
|
+
of the line, and `--opt=<value>` is exactly the form the usage DSL teaches. bash emits the separator as its
|
|
57
|
+
own word, so the shards glue back together unambiguously.
|
|
58
|
+
|
|
43
59
|
### Installing it
|
|
44
60
|
|
|
45
61
|
`generate_completion` returns text; how it reaches the shell is up to you. Write it to a file you ship, or
|
|
@@ -6,8 +6,11 @@ token, in the argument vector and in the usage that rejected it, with a two-span
|
|
|
6
6
|
|
|
7
7
|
Every error, whether the user mistyped an argument or the developer wrote a malformed usage, is lowered
|
|
8
8
|
to one shape: a bold `error:` heading with a one-line summary, then captioned source snippets with
|
|
9
|
-
carets, then optional `note:` and `help:` lines. The
|
|
10
|
-
|
|
9
|
+
carets, then optional `note:` and `help:` lines. The two are not interchangeable, and follow rustc's
|
|
10
|
+
split: a `note:` gives context - why the input was rejected, or why this usage line is the one shown - while
|
|
11
|
+
a `help:` proposes a concrete fix, such as a "did you mean". A diagnostic with nothing to suggest carries no
|
|
12
|
+
`help:` at all. The static [usage linter](check.md) reuses the same grammar with a yellow `warning:` heading,
|
|
13
|
+
so a warning and a hard error read the same way.
|
|
11
14
|
|
|
12
15
|
## A mismatch at parse time
|
|
13
16
|
|
|
@@ -116,7 +119,7 @@ docopt(doc, "push")
|
|
|
116
119
|
<span class="dt-fg"> |</span><span class="dt-fg"> git push [--force] <remote></span>
|
|
117
120
|
<span class="dt-fg"> |</span><span class="dt-fg"> </span><span class="dt-caret">^^^^^^^^</span><span class="dt-label"> required here</span>
|
|
118
121
|
<span class="dt-fg"> |</span>
|
|
119
|
-
<span class="dt-fg"> = </span><span class="dt-
|
|
122
|
+
<span class="dt-fg"> = </span><span class="dt-note">note</span><span class="dt-fg">: of 3 usage patterns, your arguments came closest to this one</span></div>
|
|
120
123
|
|
|
121
124
|
The snippet shows only the closest line - `git push`, not `commit` or `add` - because the caret is placed
|
|
122
125
|
in *that* line. Typing `git commit` instead carets `--message=<msg>` on the second line; `git deploy prod`
|
|
@@ -155,7 +158,7 @@ docopt(doc, "--port=abc", schema=Args) # raises the diagnostic below
|
|
|
155
158
|
<span class="dt-fg"> |</span><span class="dt-fg"> Usage: prog --port=<n></span>
|
|
156
159
|
<span class="dt-fg"> |</span><span class="dt-fg"> </span><span class="dt-caret">^^^^^^^^^^</span><span class="dt-label"> typed as int</span>
|
|
157
160
|
<span class="dt-fg"> |</span>
|
|
158
|
-
<span class="dt-fg"> = </span><span class="dt-
|
|
161
|
+
<span class="dt-fg"> = </span><span class="dt-note">note</span><span class="dt-fg">: `abc` is not a valid int</span></div>
|
|
159
162
|
|
|
160
163
|
Only a **user value** gets this two-span treatment. A schema that disagrees with the usage - a field with
|
|
161
164
|
no matching element, an unsupported annotation - is the developer's error, not the user's, and raises
|
|
@@ -196,7 +196,7 @@ docopt("Usage: prog <port>", "eighty", schema=P) # raises the diagnostic below
|
|
|
196
196
|
<span class="dt-fg"> |</span><span class="dt-fg"> Usage: prog <port></span>
|
|
197
197
|
<span class="dt-fg"> |</span><span class="dt-fg"> </span><span class="dt-caret">^^^^^^</span><span class="dt-label"> typed as int</span>
|
|
198
198
|
<span class="dt-fg"> |</span>
|
|
199
|
-
<span class="dt-fg"> = </span><span class="dt-
|
|
199
|
+
<span class="dt-fg"> = </span><span class="dt-note">note</span><span class="dt-fg">: `eighty` is not a valid int</span></div>
|
|
200
200
|
|
|
201
201
|
A **schema** that disagrees with the usage raises `DocoptLanguageError` (a programmer error, not a user
|
|
202
202
|
one): a field with no matching element, two elements colliding on one field, a non-optional field bound to
|
|
@@ -151,6 +151,11 @@ documents the same source chain, so users see where a value resolves from withou
|
|
|
151
151
|
an empty `APP_PORT=` never silently overrides the config or the default with an empty string. The same
|
|
152
152
|
holds for a `null` or empty config value.
|
|
153
153
|
|
|
154
|
+
!!! warning "A config key must name a value, not a table"
|
|
155
|
+
An option takes one value, so `[config: server]` against `{"server": {"port": 80}}` is a mistake: the
|
|
156
|
+
key stops one level short. docopt2 says so, with a caret and the keys that would have worked, instead
|
|
157
|
+
of handing `--server` the string `{'port': 80}`. Scalars, dates and times pass through as written.
|
|
158
|
+
|
|
154
159
|
### Generate a config skeleton
|
|
155
160
|
|
|
156
161
|
Once options declare `[config: key]`, [`generate_config_template`](../reference/config-templates.md) turns
|
|
@@ -19,5 +19,3 @@ exported names in `docopt2.__all__`.
|
|
|
19
19
|
re-exported for drop-in compatibility with the original docopt.
|
|
20
20
|
|
|
21
21
|
For a task-oriented view, start from the [Guides](../guides/typed-results.md) instead.
|
|
22
|
-
|
|
23
|
-
<!-- TODO: keep this list in sync with docopt2.__all__ if the public surface changes. -->
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
[data-md-color-scheme="slate"] .docopt2-term .dt-warn { color: #e3b341; }
|
|
111
111
|
[data-md-color-scheme="slate"] .docopt2-term .dt-label { color: #e3b341; }
|
|
112
112
|
[data-md-color-scheme="slate"] .docopt2-term .dt-help { color: #56d364; }
|
|
113
|
+
[data-md-color-scheme="slate"] .docopt2-term .dt-note { color: #56d4dd; }
|
|
113
114
|
[data-md-color-scheme="slate"] .docopt2-term .dt-dim { color: #8b949e; }
|
|
114
115
|
|
|
115
116
|
/* Light scheme: GitHub-light surface with the light-theme equivalents. */
|
|
@@ -124,4 +125,5 @@
|
|
|
124
125
|
[data-md-color-scheme="default"] .docopt2-term .dt-warn { color: #9a6700; }
|
|
125
126
|
[data-md-color-scheme="default"] .docopt2-term .dt-label { color: #9a6700; }
|
|
126
127
|
[data-md-color-scheme="default"] .docopt2-term .dt-help { color: #1a7f37; }
|
|
128
|
+
[data-md-color-scheme="default"] .docopt2-term .dt-note { color: #1b7c83; }
|
|
127
129
|
[data-md-color-scheme="default"] .docopt2-term .dt-dim { color: #57606a; }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "docopt2"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.2"
|
|
4
4
|
description = "Typed successor to docopt: the usage message is the parser spec."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@ authors = [
|
|
|
11
11
|
]
|
|
12
12
|
keywords = ["docopt", "cli", "command-line", "argument-parser", "usage", "posix", "typed"]
|
|
13
13
|
classifiers = [
|
|
14
|
-
"Development Status ::
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
15
|
"Environment :: Console",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"Operating System :: OS Independent",
|
|
@@ -73,6 +73,9 @@ dev = [
|
|
|
73
73
|
"typing_extensions>=4.6",
|
|
74
74
|
# Dev-only: property-based tests (differential vs vanilla docopt, coercion round-trips).
|
|
75
75
|
"hypothesis>=6",
|
|
76
|
+
# Dev-only: tomllib is stdlib only from 3.11, and without it the config-template round-trip
|
|
77
|
+
# would go unverified on the 3.10 floor - the one place a skip could hide a defect.
|
|
78
|
+
"tomli>=2 ; python_version < '3.11'",
|
|
76
79
|
]
|
|
77
80
|
# Documentation site (mkdocs-material + mkdocstrings). Build-only; not shipped with the
|
|
78
81
|
# package and not part of the zero runtime-dependency guarantee (that concerns
|
|
@@ -91,6 +94,8 @@ typecheck = [
|
|
|
91
94
|
|
|
92
95
|
[tool.pytest.ini_options]
|
|
93
96
|
testpaths = ["tests"]
|
|
97
|
+
# The repo root, so tests can import `tools.screenshots` - the single source of what each screenshot shows.
|
|
98
|
+
pythonpath = ["."]
|
|
94
99
|
# --strict-markers: an unknown @pytest.mark.* is an error, not a silent typo.
|
|
95
100
|
# Coverage is always measured and gated at 100% (line + branch).
|
|
96
101
|
addopts = [
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from docopt2._completion import complete, generate_completion
|
|
6
|
+
from docopt2._core import Arguments, Cli, Dispatch, Source, docopt, parse_tree
|
|
7
|
+
from docopt2._errors import DocoptExit, DocoptLanguageError
|
|
8
|
+
from docopt2._parser import (
|
|
9
|
+
Argument,
|
|
10
|
+
BranchPattern,
|
|
11
|
+
Command,
|
|
12
|
+
Either,
|
|
13
|
+
LeafPattern,
|
|
14
|
+
OneOrMore,
|
|
15
|
+
Option,
|
|
16
|
+
Optional,
|
|
17
|
+
OptionsShortcut,
|
|
18
|
+
Pattern,
|
|
19
|
+
Required,
|
|
20
|
+
Tokens,
|
|
21
|
+
formal_usage,
|
|
22
|
+
parse_argv,
|
|
23
|
+
parse_defaults,
|
|
24
|
+
parse_pattern,
|
|
25
|
+
parse_section,
|
|
26
|
+
transform,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING: # real types for the lazily-loaded tooling below, at zero runtime import cost
|
|
30
|
+
from docopt2._compat import check_compat
|
|
31
|
+
from docopt2._fmt import format_usage
|
|
32
|
+
from docopt2._format import format_argv
|
|
33
|
+
from docopt2._generate import generate_config_template, generate_examples
|
|
34
|
+
from docopt2._lint import check
|
|
35
|
+
from docopt2._stub import generate_stub
|
|
36
|
+
|
|
37
|
+
__version__: str
|
|
38
|
+
|
|
39
|
+
# This module is the public facade: it only re-exports names; the runtime (docopt, Cli, Dispatch,
|
|
40
|
+
# Arguments, parse_tree) lives in _core, and the parser primitives (Tokens, formal_usage, parse_*,
|
|
41
|
+
# transform) are re-exported for drop-in compatibility with the original docopt module.
|
|
42
|
+
__all__ = [
|
|
43
|
+
"Argument",
|
|
44
|
+
"Arguments",
|
|
45
|
+
"BranchPattern",
|
|
46
|
+
"Cli",
|
|
47
|
+
"Command",
|
|
48
|
+
"Dispatch",
|
|
49
|
+
"DocoptExit",
|
|
50
|
+
"DocoptLanguageError",
|
|
51
|
+
"Either",
|
|
52
|
+
"LeafPattern",
|
|
53
|
+
"OneOrMore",
|
|
54
|
+
"Option",
|
|
55
|
+
"Optional",
|
|
56
|
+
"OptionsShortcut",
|
|
57
|
+
"Pattern",
|
|
58
|
+
"Required",
|
|
59
|
+
"Source",
|
|
60
|
+
"Tokens",
|
|
61
|
+
"check",
|
|
62
|
+
"check_compat",
|
|
63
|
+
"complete",
|
|
64
|
+
"docopt",
|
|
65
|
+
"formal_usage",
|
|
66
|
+
"format_argv",
|
|
67
|
+
"format_usage",
|
|
68
|
+
"generate_completion",
|
|
69
|
+
"generate_config_template",
|
|
70
|
+
"generate_examples",
|
|
71
|
+
"generate_stub",
|
|
72
|
+
"parse_argv",
|
|
73
|
+
"parse_defaults",
|
|
74
|
+
"parse_pattern",
|
|
75
|
+
"parse_section",
|
|
76
|
+
"parse_tree",
|
|
77
|
+
"transform",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
# Tooling a plain docopt() call never needs loads on first access, so `import docopt2` stays lean.
|
|
81
|
+
_LAZY_MODULES = {
|
|
82
|
+
"check": "docopt2._lint",
|
|
83
|
+
"check_compat": "docopt2._compat",
|
|
84
|
+
"format_argv": "docopt2._format",
|
|
85
|
+
"format_usage": "docopt2._fmt",
|
|
86
|
+
"generate_config_template": "docopt2._generate",
|
|
87
|
+
"generate_examples": "docopt2._generate",
|
|
88
|
+
"generate_stub": "docopt2._stub",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def __getattr__(name: str) -> object:
|
|
93
|
+
"""Resolve deferred names (PEP 562): the version and the tooling functions, imported only when asked."""
|
|
94
|
+
if name == "__version__":
|
|
95
|
+
# importlib.metadata drags in ~22ms of stdlib (email, zipfile, inspect) the parser never needs.
|
|
96
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
return version("docopt2")
|
|
100
|
+
except PackageNotFoundError: # pragma: no cover - only from a source tree with no installed dist
|
|
101
|
+
return "0.0.0"
|
|
102
|
+
module_name = _LAZY_MODULES.get(name)
|
|
103
|
+
if module_name is not None:
|
|
104
|
+
from importlib import import_module
|
|
105
|
+
|
|
106
|
+
value = getattr(import_module(module_name), name)
|
|
107
|
+
globals()[name] = value # cache: later lookups find it directly and skip __getattr__
|
|
108
|
+
return value
|
|
109
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -188,35 +188,70 @@ def _function_name(prog: str) -> str:
|
|
|
188
188
|
|
|
189
189
|
|
|
190
190
|
def _render_bash(prog: str, function: str) -> str:
|
|
191
|
-
#
|
|
191
|
+
# COMP_WORDBREAKS holds `=` and `:`, so bash splits `--opt=value` into three COMP_WORDS. Forwarding the
|
|
192
|
+
# shards destroys the parse context and kills every later completion; bash emits the separator as its
|
|
193
|
+
# own word, so the loop glues them back.
|
|
194
|
+
template = (
|
|
195
|
+
"__FUNC__() {\n"
|
|
196
|
+
" local -a typed=()\n"
|
|
197
|
+
" local index part\n"
|
|
198
|
+
" for (( index = 1; index < COMP_CWORD; index++ )); do\n"
|
|
199
|
+
" part=${COMP_WORDS[index]}\n"
|
|
200
|
+
" if [[ ${#typed[@]} -gt 0 && ( $part == [:=] || ${COMP_WORDS[index-1]} == [:=] ) ]]; then\n"
|
|
201
|
+
" typed[$(( ${#typed[@]} - 1 ))]+=$part\n"
|
|
202
|
+
" else\n"
|
|
203
|
+
' typed+=("$part")\n'
|
|
204
|
+
" fi\n"
|
|
205
|
+
" done\n"
|
|
206
|
+
" local IFS=$'\\n'\n"
|
|
207
|
+
' local words="${typed[*]}"\n' # completed tokens, glued back, without the program name
|
|
208
|
+
' local reply; reply="$(__TRIGGER__=1 __WORDS__="$words" "${COMP_WORDS[0]}" 2>/dev/null | cut -f1)"\n'
|
|
209
|
+
' COMPREPLY=( $(compgen -W "$reply" -- "${COMP_WORDS[COMP_CWORD]}") )\n'
|
|
210
|
+
"}\n"
|
|
211
|
+
"complete -F __FUNC__ __PROG__\n"
|
|
212
|
+
)
|
|
192
213
|
return (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
f' COMPREPLY=( $(compgen -W "$reply" -- "${{COMP_WORDS[COMP_CWORD]}}") )\n'
|
|
198
|
-
f"}}\n"
|
|
199
|
-
f"complete -F {function} {prog}\n"
|
|
214
|
+
template.replace("__PROG__", prog)
|
|
215
|
+
.replace("__FUNC__", function)
|
|
216
|
+
.replace("__TRIGGER__", _TRIGGER_ENV)
|
|
217
|
+
.replace("__WORDS__", _WORDS_ENV)
|
|
200
218
|
)
|
|
201
219
|
|
|
202
220
|
|
|
203
221
|
def _render_zsh(prog: str, function: str) -> str:
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
222
|
+
# Commands reply with an empty description, and `_describe` renders one as a dangling `--`, so bare
|
|
223
|
+
# names go to `compadd` instead. The two groups are added under `if`, not `&&`: an empty group would
|
|
224
|
+
# otherwise leave a non-zero status and zsh would read that as "nothing matched".
|
|
207
225
|
template = (
|
|
208
226
|
"#compdef __PROG__\n"
|
|
209
227
|
"__FUNC__() {\n"
|
|
210
228
|
' local joined="${(pj:\\n:)words[2,CURRENT-1]}"\n' # completed tokens, not the current word
|
|
211
|
-
" local -a lines described\n"
|
|
229
|
+
" local -a lines described bare\n"
|
|
212
230
|
' lines=("${(@f)$(__TRIGGER__=1 __WORDS__=$joined ${words[1]})}")\n'
|
|
213
|
-
" local line\n"
|
|
231
|
+
" local line desc\n"
|
|
214
232
|
" for line in $lines; do\n"
|
|
215
|
-
"
|
|
233
|
+
" desc=\"${line#*$'\\t'}\"\n"
|
|
234
|
+
" if [[ -n $desc ]]; then\n"
|
|
235
|
+
" described+=(\"${line%%$'\\t'*}:$desc\")\n"
|
|
236
|
+
" else\n"
|
|
237
|
+
" bare+=(\"${line%%$'\\t'*}\")\n"
|
|
238
|
+
" fi\n"
|
|
216
239
|
" done\n"
|
|
217
|
-
"
|
|
240
|
+
" if (( $#described )); then\n"
|
|
241
|
+
" _describe -t candidates candidate described\n"
|
|
242
|
+
" fi\n"
|
|
243
|
+
" if (( $#bare )); then\n"
|
|
244
|
+
" compadd -a bare\n"
|
|
245
|
+
" fi\n"
|
|
218
246
|
"}\n"
|
|
219
|
-
|
|
247
|
+
# Autoloaded from `$fpath`, zsh runs this file's body AS the completion function, so a body that
|
|
248
|
+
# only defines and registers offers nothing on the first Tab. `CURRENT` exists only while the
|
|
249
|
+
# completion system runs, which tells that install apart from a plain `source`.
|
|
250
|
+
"if (( ${+CURRENT} )); then\n"
|
|
251
|
+
' __FUNC__ "$@"\n'
|
|
252
|
+
"else\n"
|
|
253
|
+
" compdef __FUNC__ __PROG__\n"
|
|
254
|
+
"fi\n"
|
|
220
255
|
)
|
|
221
256
|
return (
|
|
222
257
|
template.replace("__PROG__", prog)
|
|
@@ -232,7 +267,10 @@ def _render_fish(prog: str, function: str) -> str:
|
|
|
232
267
|
# `commandline -opc` is the completed tokens before the cursor (the partial word is excluded);
|
|
233
268
|
# fish filters the candidates we return by that partial word itself.
|
|
234
269
|
f" set -l tokens (commandline -opc)\n"
|
|
235
|
-
|
|
270
|
+
# fish documents that a command substitution splits its output on newlines. A completion function
|
|
271
|
+
# happens not to re-split today, but that is undocumented; `string collect` states the intent rather
|
|
272
|
+
# than depending on the accident.
|
|
273
|
+
f" env {_TRIGGER_ENV}=1 {_WORDS_ENV}=(string join \\n -- $tokens[2..-1] | string collect) $tokens[1]\n"
|
|
236
274
|
f"end\n"
|
|
237
275
|
f"complete -c {prog} -f -a '({function})'\n"
|
|
238
276
|
)
|
|
@@ -244,15 +282,22 @@ def _render_powershell(prog: str, _function: str) -> str:
|
|
|
244
282
|
template = (
|
|
245
283
|
"Register-ArgumentCompleter -Native -CommandName __PROG__ -ScriptBlock {\n"
|
|
246
284
|
" param($wordToComplete, $commandAst, $cursorPosition)\n"
|
|
247
|
-
|
|
285
|
+
# PowerShell hands over the whole line, so a mid-line Tab would read the tokens right of the cursor.
|
|
286
|
+
" $typed = @($commandAst.CommandElements | Where-Object { $_.Extent.EndOffset -le $cursorPosition })\n"
|
|
287
|
+
# A quoted token arrives with its quotes attached; the program must get the value, not the quoting.
|
|
288
|
+
" $words = @($typed | Select-Object -Skip 1 | ForEach-Object {\n"
|
|
289
|
+
" if ($_ -is [System.Management.Automation.Language.StringConstantExpressionAst])"
|
|
290
|
+
" { $_.Value } else { $_.ToString() }\n"
|
|
291
|
+
" })\n"
|
|
248
292
|
" if ($wordToComplete -ne '' -and $words.Count -gt 0) { $words = @($words | Select-Object -SkipLast 1) }\n"
|
|
249
293
|
" $env:__TRIGGER__ = '1'\n"
|
|
250
294
|
' $env:__WORDS__ = ($words -join "`n")\n'
|
|
251
295
|
" try {\n"
|
|
252
|
-
" & $commandAst.CommandElements[0].ToString() | ForEach-Object {\n"
|
|
296
|
+
" & $commandAst.CommandElements[0].ToString() 2>$null | ForEach-Object {\n"
|
|
253
297
|
' $parts = $_ -split "`t", 2\n' # reply line is name`tdescription
|
|
254
298
|
" $name = $parts[0]\n"
|
|
255
|
-
|
|
299
|
+
# StartsWith, not -like: a `*` or `[` in the partial word is a literal prefix, not a wildcard.
|
|
300
|
+
" if ($name.StartsWith($wordToComplete, [System.StringComparison]::Ordinal)) {\n"
|
|
256
301
|
" $tip = if ($parts.Count -ge 2 -and $parts[1]) { $parts[1] } else { $name }\n"
|
|
257
302
|
" [System.Management.Automation.CompletionResult]::new($name, $name, 'ParameterValue', $tip)\n"
|
|
258
303
|
" }\n"
|