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.
Files changed (134) hide show
  1. docopt2-1.0.2/.codecov.yml +10 -0
  2. {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/ci.yml +38 -7
  3. docopt2-1.0.2/.github/workflows/nightly.yml +32 -0
  4. {docopt2-1.0.0 → docopt2-1.0.2}/PKG-INFO +4 -3
  5. {docopt2-1.0.0 → docopt2-1.0.2}/README.md +2 -1
  6. docopt2-1.0.2/docs/assets/check.txt +7 -0
  7. {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/coercion.png +0 -0
  8. docopt2-1.0.2/docs/assets/coercion.txt +11 -0
  9. docopt2-1.0.2/docs/assets/diagnostic.txt +11 -0
  10. docopt2-1.0.2/docs/assets/rich-help.txt +9 -0
  11. {docopt2-1.0.0 → docopt2-1.0.2}/docs/getting-started.md +1 -1
  12. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/completion.md +18 -2
  13. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/diagnostics.md +7 -4
  14. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/typed-results.md +1 -1
  15. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/usage-dsl.md +5 -0
  16. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/overview.md +0 -2
  17. {docopt2-1.0.0 → docopt2-1.0.2}/docs/stylesheets/extra.css +2 -0
  18. {docopt2-1.0.0 → docopt2-1.0.2}/pyproject.toml +7 -2
  19. docopt2-1.0.2/src/docopt2/__init__.py +109 -0
  20. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_completion.py +65 -20
  21. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_core.py +62 -9
  22. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_generate.py +1 -1
  23. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_parser.py +55 -22
  24. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_typed.py +3 -1
  25. docopt2-1.0.2/tests/test_completion_vs_docopt.py +79 -0
  26. docopt2-1.0.2/tests/test_config.py +197 -0
  27. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_config_template.py +7 -1
  28. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_core_extra.py +6 -4
  29. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_dispatch_and_completion.py +60 -0
  30. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_docopt_original.py +3 -3
  31. docopt2-1.0.2/tests/test_docs_blocks.py +172 -0
  32. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_getting_started.py +4 -2
  33. docopt2-1.0.2/tests/test_import_cost.py +42 -0
  34. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_near_miss.py +81 -5
  35. docopt2-1.0.2/tests/test_package.py +18 -0
  36. docopt2-1.0.2/tests/test_property_config.py +41 -0
  37. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_property_config_template.py +8 -2
  38. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_readme.py +1 -1
  39. docopt2-1.0.2/tests/test_screenshots.py +19 -0
  40. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_typed_api.py +6 -4
  41. docopt2-1.0.2/tools/screenshots.py +149 -0
  42. docopt2-1.0.2/tools/shell_check.py +260 -0
  43. {docopt2-1.0.0 → docopt2-1.0.2}/uv.lock +3 -1
  44. docopt2-1.0.0/src/docopt2/__init__.py +0 -80
  45. docopt2-1.0.0/tests/test_config.py +0 -101
  46. {docopt2-1.0.0 → docopt2-1.0.2}/.gitattributes +0 -0
  47. {docopt2-1.0.0 → docopt2-1.0.2}/.github/dependabot.yml +0 -0
  48. {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/codeql.yml +0 -0
  49. {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/docs.yml +0 -0
  50. {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/publish.yml +0 -0
  51. {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/scorecard.yml +0 -0
  52. {docopt2-1.0.0 → docopt2-1.0.2}/.github/workflows/zizmor.yml +0 -0
  53. {docopt2-1.0.0 → docopt2-1.0.2}/.gitignore +0 -0
  54. {docopt2-1.0.0 → docopt2-1.0.2}/CONTRIBUTING.md +0 -0
  55. {docopt2-1.0.0 → docopt2-1.0.2}/LICENSE +0 -0
  56. {docopt2-1.0.0 → docopt2-1.0.2}/NOTICE +0 -0
  57. {docopt2-1.0.0 → docopt2-1.0.2}/SECURITY.md +0 -0
  58. {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/check.png +0 -0
  59. {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/diagnostic.png +0 -0
  60. {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/logo-dark.png +0 -0
  61. {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/logo.png +0 -0
  62. {docopt2-1.0.0 → docopt2-1.0.2}/docs/assets/rich-help.png +0 -0
  63. {docopt2-1.0.0 → docopt2-1.0.2}/docs/concepts/design-boundaries.md +0 -0
  64. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/check.md +0 -0
  65. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/compat.md +0 -0
  66. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/dispatch.md +0 -0
  67. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/examples.md +0 -0
  68. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/fmt.md +0 -0
  69. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/help.md +0 -0
  70. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/round-trip.md +0 -0
  71. {docopt2-1.0.0 → docopt2-1.0.2}/docs/guides/stub.md +0 -0
  72. {docopt2-1.0.0 → docopt2-1.0.2}/docs/index.md +0 -0
  73. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/check.md +0 -0
  74. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/cli.md +0 -0
  75. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/compat.md +0 -0
  76. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/completion.md +0 -0
  77. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/config-templates.md +0 -0
  78. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/dispatch.md +0 -0
  79. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/docopt.md +0 -0
  80. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/examples.md +0 -0
  81. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/exceptions.md +0 -0
  82. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/fmt.md +0 -0
  83. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/grammar.md +0 -0
  84. {docopt2-1.0.0 → docopt2-1.0.2}/docs/reference/stub.md +0 -0
  85. {docopt2-1.0.0 → docopt2-1.0.2}/examples/cli.py +0 -0
  86. {docopt2-1.0.0 → docopt2-1.0.2}/examples/completion.py +0 -0
  87. {docopt2-1.0.0 → docopt2-1.0.2}/examples/dispatch.py +0 -0
  88. {docopt2-1.0.0 → docopt2-1.0.2}/examples/layered.py +0 -0
  89. {docopt2-1.0.0 → docopt2-1.0.2}/examples/naval_fate.py +0 -0
  90. {docopt2-1.0.0 → docopt2-1.0.2}/examples/rich_help.py +0 -0
  91. {docopt2-1.0.0 → docopt2-1.0.2}/examples/round_trip.py +0 -0
  92. {docopt2-1.0.0 → docopt2-1.0.2}/examples/typed.py +0 -0
  93. {docopt2-1.0.0 → docopt2-1.0.2}/mkdocs.yml +0 -0
  94. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/__main__.py +0 -0
  95. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_compat.py +0 -0
  96. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_diagnostics.py +0 -0
  97. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_errors.py +0 -0
  98. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_fmt.py +0 -0
  99. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_format.py +0 -0
  100. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_help.py +0 -0
  101. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_lint.py +0 -0
  102. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_spellcheck.py +0 -0
  103. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/_stub.py +0 -0
  104. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/hypothesis.py +0 -0
  105. {docopt2-1.0.0 → docopt2-1.0.2}/src/docopt2/py.typed +0 -0
  106. {docopt2-1.0.0 → docopt2-1.0.2}/tests/_strategies.py +0 -0
  107. {docopt2-1.0.0 → docopt2-1.0.2}/tests/_vendor/docopt_original.py +0 -0
  108. {docopt2-1.0.0 → docopt2-1.0.2}/tests/conftest.py +0 -0
  109. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_choices.py +0 -0
  110. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_cli.py +0 -0
  111. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_coercion_diagnostics.py +0 -0
  112. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_compat.py +0 -0
  113. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_diagnostics.py +0 -0
  114. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_divergences.py +0 -0
  115. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_env.py +0 -0
  116. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_examples.py +0 -0
  117. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_extended_features.py +0 -0
  118. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_fmt.py +0 -0
  119. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_format.py +0 -0
  120. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_generate.py +0 -0
  121. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_grammar_examples.py +0 -0
  122. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_help.py +0 -0
  123. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_hypothesis.py +0 -0
  124. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_language_agnostic.docopt +0 -0
  125. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_lint.py +0 -0
  126. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_parse_tree.py +0 -0
  127. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_property_coercion.py +0 -0
  128. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_property_differential.py +0 -0
  129. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_property_extended.py +0 -0
  130. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_pydantic.py +0 -0
  131. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_source.py +0 -0
  132. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_spellcheck.py +0 -0
  133. {docopt2-1.0.0 → docopt2-1.0.2}/tests/test_stub.py +0 -0
  134. {docopt2-1.0.0 → docopt2-1.0.2}/tests/typing_surface/check_typed.py +0 -0
@@ -0,0 +1,10 @@
1
+ coverage:
2
+ status:
3
+ project:
4
+ default:
5
+ target: 100%
6
+ threshold: 0%
7
+ patch:
8
+ default:
9
+ target: 100%
10
+ threshold: 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 -o addopts=
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: '[ "${{ needs.test.result }}" = "success" ] && [ "${{ needs.lint.result }}" = "success" ]'
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.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 :: 4 - Beta
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 'help: abc is not a valid int'">
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 'help: abc is not a valid int'">
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".
@@ -0,0 +1,7 @@
1
+ warning: option `--verbose` is declared but never used
2
+ |
3
+ | in the options:
4
+ | --verbose Extra logging.
5
+ | ^^^^^^^^^ declared here
6
+ |
7
+ = help: add `--verbose` to a usage line, or add `[options]` to accept it
@@ -0,0 +1,11 @@
1
+ error: invalid value for `--port`
2
+ |
3
+ | in the arguments:
4
+ | --port=abc
5
+ | ^^^ expected int
6
+ |
7
+ | in the usage:
8
+ | Usage: prog --port=<n>
9
+ | ^^^^^^^^^^ typed as int
10
+ |
11
+ = note: `abc` is not a valid int
@@ -0,0 +1,11 @@
1
+ error: unknown option `--forcce`
2
+ |
3
+ | in the arguments:
4
+ | push --forcce origin
5
+ | ^^^^^^^^ not a known option
6
+ |
7
+ | in the usage:
8
+ | git push [--force] <remote>
9
+ | ^^^^^^^ `--force` is defined here
10
+ |
11
+ = help: did you mean `--force`?
@@ -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=&lt;n&gt;] [--host=&lt;h&gt;] [--log=&lt;lvl&gt;] &lt;root&gt;</span>
5
+
6
+ <span class="dt-fg dt-b">Options:</span>
7
+ <span class="dt-fg"> </span><span class="dt-help">--port=&lt;n&gt; </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=&lt;h&gt; </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=&lt;lvl&gt;</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 &lt;name&gt; move &lt;x&gt; &lt;y&gt; [--speed=&lt;kn&gt;]</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-help">help</span><span class="dt-fg">: closest of 6 usage patterns</span></div>
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="${COMP_WORDS[*]:1:COMP_CWORD-1}"
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 static [usage linter](check.md) reuses the same
10
- grammar with a yellow `warning:` heading, so a warning and a hard error read the same way.
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] &lt;remote&gt;</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-help">help</span><span class="dt-fg">: closest of 3 usage patterns</span></div>
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=&lt;n&gt;</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-help">help</span><span class="dt-fg">: `abc` is not a valid int</span></div>
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 &lt;port&gt;</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-help">help</span><span class="dt-fg">: `eighty` is not a valid int</span></div>
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.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 :: 4 - Beta",
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
- # bash completion shows names only, so strip the tab-joined description column with `cut -f1`.
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
- f"{function}() {{\n"
194
- f" local IFS=$'\\n'\n"
195
- f' local words="${{COMP_WORDS[*]:1:COMP_CWORD-1}}"\n' # completed tokens, not the current word
196
- f' local reply; reply="$({_TRIGGER_ENV}=1 {_WORDS_ENV}="$words" "${{COMP_WORDS[0]}}" | cut -f1)"\n'
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
- # Split each `name\tdescription` reply line and hand it to `_describe`, which renders the two-column
205
- # name/description list and filters by the word being completed. Template, not an f-string, to keep
206
- # the brace- and quote-heavy zsh readable.
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
- " described+=(\"${line%%$'\\t'*}:${line#*$'\\t'}\")\n"
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
- " _describe -t candidates candidate described\n"
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
- "compdef __FUNC__ __PROG__\n"
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
- f" env {_TRIGGER_ENV}=1 {_WORDS_ENV}=(string join \\n -- $tokens[2..-1]) $tokens[1]\n"
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
- " $words = @($commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.ToString() })\n"
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
- ' if ($name -like "$wordToComplete*") {\n'
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"