././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1788300752.241782 shtab-1.12.1/0000755000175100017510000000000015245646720012356 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2354586 shtab-1.12.1/.github/0000755000175100017510000000000015245646720013716 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9296122 shtab-1.12.1/.github/FUNDING.yml0000644000175100017510000000012515245646712015532 0ustar00runnerrunnergithub: [casperdcl, tqdm] thanks_dev: u/gh/tqdm custom: https://tqdm.github.io/merch ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2356648 shtab-1.12.1/.github/ISSUE_TEMPLATE/0000755000175100017510000000000015245646720016101 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9306748 shtab-1.12.1/.github/ISSUE_TEMPLATE/config.yml0000644000175100017510000000021415245646712020067 0ustar00runnerrunnercontact_links: - name: FAQs and Known Issues url: https://tqdm.github.io/shtab/#faqs about: Frequently asked questions and known issues ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2358487 shtab-1.12.1/.github/workflows/0000755000175100017510000000000015245646720015753 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9306748 shtab-1.12.1/.github/workflows/test.yml0000644000175100017510000000777315245646712017474 0ustar00runnerrunnername: ci on: push: pull_request: schedule: [{cron: '0 9 * * 1'}] # M H d m w (Mondays at 9:00) workflow_dispatch: jobs: test: if: github.event_name != 'pull_request' || !contains('OWNER,MEMBER,COLLABORATOR', github.event.pull_request.author_association) strategy: matrix: python: ['3.x'] os: [ubuntu, macos, windows] include: [{python: 3.9, os: ubuntu}] # min supported (see pyproject.toml) runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v7 with: {fetch-depth: 0} - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python }} - if: matrix.os == 'ubuntu' run: sudo apt-get update && sudo apt-get install -y zsh fish tcsh # generated-script tests - run: pip install -e .[dev] - run: pytest --cov-report=xml --cov-report=term ${{ matrix.os == 'windows' && '-k "not bash" --cov-fail-under=75' || '' }} - uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} flags: ${{ matrix.python }}-${{ matrix.os }} diff: if: (github.event_name != 'pull_request' || !contains('OWNER,MEMBER,COLLABORATOR', github.event.pull_request.author_association)) && github.ref != 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: {fetch-depth: 0} - uses: actions/setup-python@v7 with: {python-version: '3.x'} - name: diff examples run: | pip install -e . git checkout main SHELLS=$(python -c "import shtab; print(' '.join(shtab.SUPPORTED_SHELLS))") git checkout - ref=$(git rev-parse --abbrev-ref HEAD) mkdir -p "$ref" main for s in $SHELLS; do echo "::group::$s" for r in main "$ref"; do git checkout - python examples/pathcomplete.py -s $s > "$r/pathcomplete.$s" || : python examples/customcomplete.py completion $s > "$r/customcomplete.$s" || : done for ex in pathcomplete customcomplete; do d=$(diff {main,"$ref"}/$ex.$s || :) if test -n "$d"; then echo "::warning file=examples/$ex.py,line=1,title=$s::changes detected" cat <> "$GITHUB_STEP_SUMMARY" # warning: $ex.$s \`\`\`diff $d \`\`\` EOF fi done echo "::endgroup::" done cat "$GITHUB_STEP_SUMMARY" test -z "$(cat "$GITHUB_STEP_SUMMARY")" deploy: needs: test runs-on: ubuntu-latest environment: pypi permissions: {contents: write, id-token: write} steps: - uses: actions/checkout@v7 with: fetch-depth: 0 token: ${{ secrets.GH_TOKEN || github.token }} - uses: actions/setup-python@v7 with: {python-version: '3.x'} - id: dist uses: casperdcl/deploy-pypi@v3 with: build: true upload: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') name: gh release create --draft run: | changelog=$(git log --pretty='format:%d%n- %s%n%b---' $(git tag --sort=v:refname | tail -n2 | head -n1)..HEAD) tag="${GITHUB_REF#refs/tags/}" gh release create --title "shtab $tag stable" --draft --notes "$changelog" "$tag" dist/${{ steps.dist.outputs.whl }} dist/${{ steps.dist.outputs.targz }} env: GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: build docs run: | pushd docs pip install -U -r requirements.txt PYTHONPATH=. pydoc-markdown --build --site-dir=../../../dist/site popd - if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') || github.event_name == 'workflow_dispatch' }} uses: casperdcl/push-dir@v1 with: message: update static site branch: gh-pages history: false dir: dist/site nojekyll: true name: tqdm[bot] email: 68520887+tqdm-bot@users.noreply.github.com ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9306748 shtab-1.12.1/.gitignore0000644000175100017510000000020215245646712014341 0ustar00runnerrunner*.py[cod] __pycache__/ # Packages /*.egg*/ /build/ /dist/ /docs/build/ # Unit test / coverage reports /.coverage* /coverage.xml ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9306748 shtab-1.12.1/.mailmap0000644000175100017510000000115215245646712013777 0ustar00runnerrunnerBenjamin Morledge-Hampton Casper da Costa-Luis David Yoga David Yoga Helio Machado <0x2b3bfa0+git@googlemail.com> Henning Schroeder Nikita Nikulin <42353747+w3lld1@users.noreply.github.com> Oded Messer Saugat Pachhai (सौगात) <18718008+skshetry@users.noreply.github.com> Takuya Wakazono <28638872+pastalian@users.noreply.github.com> Thomas Waldmann ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9306748 shtab-1.12.1/.pre-commit-config.yaml0000644000175100017510000000336715245646712016651 0ustar00runnerrunnerdefault_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-added-large-files - id: check-builtin-literals - id: check-case-conflict - id: check-docstring-first - id: check-executables-have-shebangs - id: check-merge-conflict - id: check-toml - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending - id: sort-simple-yaml - id: trailing-whitespace - repo: local hooks: - id: todo name: Check TODO language: pygrep args: [-i] entry: TODO types: [text] exclude: ^.pre-commit-config.yaml$ - id: pytest name: pytest language: python entry: pytest args: [-vv] types: [python] pass_filenames: false additional_dependencies: [pytest-cov, click] - repo: https://github.com/codespell-project/codespell rev: v2.4.3 hooks: - id: codespell - repo: https://github.com/PyCQA/bandit rev: 1.9.4 hooks: - id: bandit args: [-c, pyproject.toml] - repo: https://github.com/PyCQA/flake8 rev: 7.3.0 hooks: - id: flake8 args: [-j8] additional_dependencies: - flake8-broken-line - flake8-bugbear - flake8-comprehensions - flake8-debugger - flake8-isort - flake8-pyproject - repo: https://github.com/asottile/pyupgrade rev: v3.21.2 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/pre-commit/mirrors-mypy rev: v2.3.1 hooks: - id: mypy additional_dependencies: [types-setuptools] - repo: https://github.com/google/yapf rev: v0.43.0 hooks: - id: yapf args: [-i] additional_dependencies: [toml] - repo: https://github.com/PyCQA/isort rev: 9.0.1 hooks: - id: isort ci: autoupdate_schedule: monthly ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9306748 shtab-1.12.1/.prospector.yml0000644000175100017510000000021515245646712015356 0ustar00runnerrunnerpydocstyle: disable: - D204 - D205 - D212 - D413 - D415 pylint: run: false mccabe: options: max-complexity: 31 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9306748 shtab-1.12.1/CONTRIBUTING.md0000644000175100017510000000451415245646712014614 0ustar00runnerrunner# Contributing ## Tests When contributing pull requests, it's a good idea to run basic checks locally: ```bash shtab (main)$ pip install pre-commit shtab (main)$ pre-commit install # install pre-commit hooks into git workspace shtab (main)$ pre-commit run -a # run hooks on all files ``` Example scripts may also be installed via `pip install -e .` after modifying `pyproject.toml`: ```diff diff --git a/pyproject.toml b/pyproject.toml index edfd3b2..44c3748 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,3 +7,3 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] -include = ["shtab", "shtab.*"] +include = ["shtab", "shtab.*", "examples"] @@ -77,2 +77,6 @@ dev = ["pytest>=6", "pytest-cov", "click"] shtab = "shtab.main:main" +pathcomplete = "examples.pathcomplete:main" +customcomplete = "examples.customcomplete:main" +click-process = "examples.click_process:process" +click-subcommand = "examples.click_subcommand:main" ``` ## Layout Most of the magic lives in [`shtab/__init__.py`](./shtab/__init__.py). - [shtab/](./shtab/) - [`__init__.py`](./shtab/__init__.py) - `complete()` - primary API, calls shell-specific versions - `complete_bash()` - `complete_zsh()` - `complete_tcsh()` - `complete_fish()` - `complete_powershell()` - ... - `add_argument_to()` - convenience function for library integration - `Optional()`, `Required()`, `Choice()` - legacy helpers for advanced completion (e.g. dirs, files, `*.txt`) - [`main.py`](./shtab/main.py) - `get_main_parser()` - returns `shtab`'s own parser object - `main()` - `shtab`'s own CLI application Given that the number of completions a program may need would likely be less than a million, the focus is on readability rather than premature speed optimisations. The generated code itself, on the other hand, should be fast. Helper functions such as `replace_format` allow use of curly braces `{}` in string snippets without clashes between Python's `str.format` and shell parameter expansion. The generated shell code itself is also meant to be readable. ## Releases Tests and deployment are handled automatically by continuous integration. Simply tag a commit `v{major}.{minor}.{patch}` and wait for a draft release to appear at . Tidy up the draft's description before publishing it. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9306748 shtab-1.12.1/LICENCE0000644000175100017510000000054215245646712013345 0ustar00runnerrunnerMozilla Public License Version 2.0 ================================== This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this project, You can obtain one at https://mozilla.org/MPL/2.0/. * files: * MPL-2.0 2020-2026 (c) Casper da Costa-Luis https://github.com/casperdcl ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2417479 shtab-1.12.1/PKG-INFO0000644000175100017510000001663515245646720013466 0ustar00runnerrunnerMetadata-Version: 2.4 Name: shtab Version: 1.12.1 Summary: Automagic shell tab completion for Python CLI applications Author-email: Casper da Costa-Luis Maintainer-email: tqdm developers License: MPL-2.0 Project-URL: documentation, https://tqdm.github.io/shtab Project-URL: repository, https://github.com/tqdm/shtab Project-URL: changelog, https://github.com/tqdm/shtab/releases Keywords: tab,completion,argparse,shell,bash,zsh,tcsh,fish,powershell,pwsh Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: MacOS X Classifier: Environment :: Other Environment Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Other Audience Classifier: Intended Audience :: System Administrators Classifier: Operating System :: MacOS Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: BSD Classifier: Operating System :: POSIX :: BSD :: FreeBSD Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: SunOS/Solaris Classifier: Operating System :: Unix Classifier: Programming Language :: Other Scripting Engines Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Unix Shell Classifier: Topic :: Desktop Environment Classifier: Topic :: Education :: Computer Aided Instruction (CAI) Classifier: Topic :: Education :: Testing Classifier: Topic :: Office/Business Classifier: Topic :: Other/Nonlisted Topic Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Pre-processors Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: System Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Shells Classifier: Topic :: System :: System Shells Classifier: Topic :: Terminals Classifier: Topic :: Utilities Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-File: LICENCE Provides-Extra: dev Requires-Dist: pytest>=6; extra == "dev" Requires-Dist: pytest-cov; extra == "dev" Requires-Dist: click; extra == "dev" Dynamic: license-file |Logo| shtab ===== |Tests| |Coverage| |Quality| |PyPI| |Conda| |PyPI-Downloads| |Licence| - What: Automatically generate shell tab completion scripts for Python CLI apps - Why: Speed & correctness. Alternatives like `argcomplete `_ and `pyzshcomplete `_ are slow and have side-effects - How: ``shtab`` processes an ``argparse.ArgumentParser`` object to generate a tab completion script for your shell Features -------- - Outputs tab completion scripts for multiple shells - ``bash``, ``zsh``, ``fish``, ``tcsh``, ``powershell`` - Supports - `argparse `_ - `docopt `_ (via `argopt `_) - `click `_ - ````, ``--options`` and ``sub commands`` - Choices (``--say={hello,goodbye}``) - Paths (``--file={*.y*ml,*.toml}``, ``--dir=*/``) - Dynamic shell commands (``--branch=$(git branch)``) ------------------------------------------ .. contents:: Table of Contents :backlinks: top Installation ------------ Choose one of: - ``pip install shtab``, or - ``conda install -c conda-forge shtab`` See `operating system-specific instructions in the docs `_. Usage ----- There are two ways of using ``shtab``: - `CLI Usage `_: ``shtab``'s own CLI interface for external applications - may not require any code modifications whatsoever - end-users execute ``shtab your_cli_app.your_parser_object`` - `Library Usage `_: as a library integrated into your CLI application - adds a couple of lines to your application - argument mode: end-users execute ``your_cli_app --print-completion {bash,zsh,tcsh,fish,powershell}`` - subparser mode: end-users execute ``your_cli_app completion {bash,zsh,tcsh,fish,powershell}`` Examples -------- See `the docs for usage examples `_. FAQs ---- Not working? Check out `frequently asked questions `_. Alternatives ------------ All these execute the underlying script *every* time ```` is pressed (slow and have side-effects): - `argcomplete `_ - `pyzshcomplete `_ (only provides ``zsh`` completion) - `click `_ (don't want to migrate away from ``click``? Use ``shtab``'s one-liner click integration `in the CLI `_ or `in a library `_) Contributions ------------- Please do open `issues `_ & `pull requests `_! Some ideas: - support ``python -m`` prefix (`#55 `_) See `CONTRIBUTING.md `_ for more guidance. |git-fame| |Hits| .. |Logo| image:: https://github.com/tqdm/shtab/raw/main/meta/logo.png .. |Tests| image:: https://img.shields.io/github/actions/workflow/status/tqdm/shtab/test.yml?logo=github&label=tests :target: https://github.com/tqdm/shtab/actions :alt: Tests .. |Coverage| image:: https://codecov.io/gh/tqdm/shtab/branch/main/graph/badge.svg :target: https://codecov.io/gh/tqdm/shtab :alt: Coverage .. |Quality| image:: https://app.codacy.com/project/badge/Grade/0ce50da5c6e04236a891b092c7012753 :target: https://app.codacy.com/gh/tqdm/shtab/dashboard :alt: Quality .. |Conda| image:: https://img.shields.io/conda/v/conda-forge/shtab.svg?label=conda&logo=conda-forge :target: https://anaconda.org/conda-forge/shtab :alt: conda-forge .. |PyPI| image:: https://img.shields.io/pypi/v/shtab.svg?label=pip&logo=PyPI&logoColor=white :target: https://pypi.org/project/shtab :alt: PyPI .. |PyPI-Downloads| image:: https://static.pepy.tech/personalized-badge/shtab?left_text=downloads%2Fmonth :target: https://pepy.tech/project/shtab :alt: Downloads .. |git-fame| image:: https://git-fame.cdcl.ml/gh/tqdm/shtab?min=1&w=1&M=1&C=1&enum=1 :alt: git-fame :target: https://git-fame.cdcl.ml/gh/tqdm/shtab?w=1&M=1&C=1&enum=1 .. |Hits| image:: https://cgi.cdcl.ml/hits?q=shtab&style=social&r=https://github.com/tqdm/shtab&a=hidden :target: https://cgi.cdcl.ml/hits?q=shtab&a=plot&r=https://github.com/tqdm/shtab&style=social :alt: Hits .. |LICENCE| image:: https://img.shields.io/pypi/l/shtab.svg :target: https://raw.githubusercontent.com/tqdm/shtab/main/LICENCE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9306748 shtab-1.12.1/README.rst0000644000175100017510000001114115245646712014044 0ustar00runnerrunner|Logo| shtab ===== |Tests| |Coverage| |Quality| |PyPI| |Conda| |PyPI-Downloads| |Licence| - What: Automatically generate shell tab completion scripts for Python CLI apps - Why: Speed & correctness. Alternatives like `argcomplete `_ and `pyzshcomplete `_ are slow and have side-effects - How: ``shtab`` processes an ``argparse.ArgumentParser`` object to generate a tab completion script for your shell Features -------- - Outputs tab completion scripts for multiple shells - ``bash``, ``zsh``, ``fish``, ``tcsh``, ``powershell`` - Supports - `argparse `_ - `docopt `_ (via `argopt `_) - `click `_ - ````, ``--options`` and ``sub commands`` - Choices (``--say={hello,goodbye}``) - Paths (``--file={*.y*ml,*.toml}``, ``--dir=*/``) - Dynamic shell commands (``--branch=$(git branch)``) ------------------------------------------ .. contents:: Table of Contents :backlinks: top Installation ------------ Choose one of: - ``pip install shtab``, or - ``conda install -c conda-forge shtab`` See `operating system-specific instructions in the docs `_. Usage ----- There are two ways of using ``shtab``: - `CLI Usage `_: ``shtab``'s own CLI interface for external applications - may not require any code modifications whatsoever - end-users execute ``shtab your_cli_app.your_parser_object`` - `Library Usage `_: as a library integrated into your CLI application - adds a couple of lines to your application - argument mode: end-users execute ``your_cli_app --print-completion {bash,zsh,tcsh,fish,powershell}`` - subparser mode: end-users execute ``your_cli_app completion {bash,zsh,tcsh,fish,powershell}`` Examples -------- See `the docs for usage examples `_. FAQs ---- Not working? Check out `frequently asked questions `_. Alternatives ------------ All these execute the underlying script *every* time ```` is pressed (slow and have side-effects): - `argcomplete `_ - `pyzshcomplete `_ (only provides ``zsh`` completion) - `click `_ (don't want to migrate away from ``click``? Use ``shtab``'s one-liner click integration `in the CLI `_ or `in a library `_) Contributions ------------- Please do open `issues `_ & `pull requests `_! Some ideas: - support ``python -m`` prefix (`#55 `_) See `CONTRIBUTING.md `_ for more guidance. |git-fame| |Hits| .. |Logo| image:: https://github.com/tqdm/shtab/raw/main/meta/logo.png .. |Tests| image:: https://img.shields.io/github/actions/workflow/status/tqdm/shtab/test.yml?logo=github&label=tests :target: https://github.com/tqdm/shtab/actions :alt: Tests .. |Coverage| image:: https://codecov.io/gh/tqdm/shtab/branch/main/graph/badge.svg :target: https://codecov.io/gh/tqdm/shtab :alt: Coverage .. |Quality| image:: https://app.codacy.com/project/badge/Grade/0ce50da5c6e04236a891b092c7012753 :target: https://app.codacy.com/gh/tqdm/shtab/dashboard :alt: Quality .. |Conda| image:: https://img.shields.io/conda/v/conda-forge/shtab.svg?label=conda&logo=conda-forge :target: https://anaconda.org/conda-forge/shtab :alt: conda-forge .. |PyPI| image:: https://img.shields.io/pypi/v/shtab.svg?label=pip&logo=PyPI&logoColor=white :target: https://pypi.org/project/shtab :alt: PyPI .. |PyPI-Downloads| image:: https://static.pepy.tech/personalized-badge/shtab?left_text=downloads%2Fmonth :target: https://pepy.tech/project/shtab :alt: Downloads .. |git-fame| image:: https://git-fame.cdcl.ml/gh/tqdm/shtab?min=1&w=1&M=1&C=1&enum=1 :alt: git-fame :target: https://git-fame.cdcl.ml/gh/tqdm/shtab?w=1&M=1&C=1&enum=1 .. |Hits| image:: https://cgi.cdcl.ml/hits?q=shtab&style=social&r=https://github.com/tqdm/shtab&a=hidden :target: https://cgi.cdcl.ml/hits?q=shtab&a=plot&r=https://github.com/tqdm/shtab&style=social :alt: Hits .. |LICENCE| image:: https://img.shields.io/pypi/l/shtab.svg :target: https://raw.githubusercontent.com/tqdm/shtab/main/LICENCE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2367918 shtab-1.12.1/docs/0000755000175100017510000000000015245646720013306 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2369773 shtab-1.12.1/docs/custom_theme/0000755000175100017510000000000015245646720016002 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1788300745.931284 shtab-1.12.1/docs/custom_theme/main.html0000644000175100017510000000305515245646712017620 0ustar00runnerrunner {% extends "base.html" %} {% block extrahead %} {% endblock %} {% block footer %} {% include "partials/footer.html" %} {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2371733 shtab-1.12.1/docs/custom_theme/partials/0000755000175100017510000000000015245646720017621 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9314134 shtab-1.12.1/docs/custom_theme/partials/toc.html0000644000175100017510000000164415245646712021302 0ustar00runnerrunner {% set title = lang.t("toc") %} {% if config.mdx_configs.toc and config.mdx_configs.toc.title %} {% set title = config.mdx_configs.toc.title %} {% endif %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9314134 shtab-1.12.1/docs/index.md0000644000175100017510000001415715245646712014750 0ustar00runnerrunner![shtab](https://tqdm.github.io/img/shtab-banner.png) [![Tests](https://img.shields.io/github/actions/workflow/status/tqdm/shtab/test.yml?logo=github&label=tests)](https://github.com/tqdm/shtab/actions) [![Coverage](https://codecov.io/gh/tqdm/shtab/branch/main/graph/badge.svg)](https://codecov.io/gh/tqdm/shtab) [![Quality](https://app.codacy.com/project/badge/Grade/0ce50da5c6e04236a891b092c7012753)](https://app.codacy.com/gh/tqdm/shtab/dashboard) [![PyPI](https://img.shields.io/pypi/v/shtab.svg?label=pip&logo=PyPI&logoColor=white)](https://pypi.org/project/shtab) [![Conda](https://img.shields.io/conda/v/conda-forge/shtab.svg?label=conda&logo=conda-forge)](https://anaconda.org/conda-forge/shtab) [![Downloads](https://static.pepy.tech/personalized-badge/shtab?left_text=downloads%2Fmonth)](https://pepy.tech/project/shtab) [![LICENCE](https://img.shields.io/pypi/l/shtab.svg)](https://raw.githubusercontent.com/tqdm/shtab/main/LICENCE) - What: Automatically generate shell tab completion scripts for Python CLI apps - Why: Speed & correctness. Alternatives like [argcomplete](https://pypi.org/project/argcomplete) and [pyzshcomplete](https://pypi.org/project/pyzshcomplete) are slow and have side-effects - How: `shtab` processes an `argparse.ArgumentParser` object to generate a tab completion script for your shell ## Features - Outputs tab completion scripts for multiple shells - `bash`, `zsh`, `fish`, `tcsh`, `powershell` - Supports - [`argparse`](https://docs.python.org/library/argparse) - [`docopt`](https://pypi.org/project/docopt) (via [`argopt`](https://pypi.org/project/argopt)) - [`click`](https://pypi.org/project/click) - ``, `--options` and `sub commands` - Choices (`--say={hello,goodbye}`) - Paths (`--file={*.y*ml,*.toml}`, `--dir=*/`) - Dynamic shell commands (`--branch=$(git branch)`) ------------------------------------------------------------------------ ## Installation ### scripts !!! tip TL;DR where to save a completion script for a program called `NAME` shell | location --|-- [`bash`](https://github.com/scop/bash-completion/blob/main/doc/configuration.md) | `/etc/bash_completion.d/NAME` [`zsh`](https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org) | `/usr/local/share/zsh/site-functions/_NAME` [`tcsh`](https://github.com/tcsh-org/tcsh/blob/master/complete.tcsh) | `/etc/profile.d/completion_NAME.csh`, source in `~/.cshrc` or `~/.tcshrc` [`fish`](https://fishshell.com/docs/current/completions.html#where-to-put-completions) | `~/.config/fish/completions/NAME.fish` [`powershell`](https://learn.microsoft.com/en-us/powershell/scripting/learn/shell/creating-profiles#adding-customizations-to-your-profile) | `~\.config\powershell\completions\NAME.ps1`, source in `$PROFILE` For more information, click on the shells above, and/or see [CLI Usage](use.md#cli-usage). ### `shtab` === "pip" ```sh pip install shtab ``` === "conda" ```sh conda install -c conda-forge shtab ``` `bash` users who have never used any kind of tab completion before should also follow the OS-specific instructions below. === "Ubuntu/Debian" Recent versions should have completion already enabled. For older versions, first run `sudo apt install --reinstall bash-completion`, then make sure these lines appear in `~/.bashrc`: ```sh # enable bash completion in interactive shells if ! shopt -oq posix; then if [ -f /usr/share/bash-completion/bash_completion ]; then . /usr/share/bash-completion/bash_completion elif [ -f /etc/bash_completion ]; then . /etc/bash_completion fi fi ``` === "MacOS" First run `brew install bash-completion`, then add the following to `~/.bash_profile`: ```sh if [ -f $(brew --prefix)/etc/bash_completion ]; then . $(brew --prefix)/etc/bash_completion fi ``` ## FAQs - Ensure that `shtab` and the application you're trying to complete are both accessible from your environment. - Ensure that `prog` matches the executable name: - if using [`project.scripts.MY-PROG=...`](https://setuptools.pypa.io/en/latest/userguide/entry_point.html), then - set the main parser's name (`argparse.ArgumentParser(prog="MY-PROG")`, `argopt(prog="MY-PROG")`, `click.group("MY-PROG")`, `click.command("MY-PROG")`, etc) - or override it using `shtab MY_PROG.get_main_parser --prog=MY-PROG` - if executing a script file `./MY_PROG.py` (with a [shebang]() `#!/usr/bin/env python`) directly, then use `prog="MY_PROG.py"` - Any suppressed argument (`help=argparse.SUPPRESS`, click `hidden=True`/`deprecated=True`) is skipped. - Default completion (when no choices are specified) is disabled. Enable it explicitly via e.g. `parser.add_argument('positional').complete = shtab.FILE`. - Some shells (e.g. `zsh`, `fish`) print information during tab completion: - subparser `description` takes precedence over `help` - argument `metavar` takes precedence over `dest` - [Ask a general question on StackOverflow](https://stackoverflow.com/questions/tagged/shtab). - [Report bugs and open feature requests on GitHub][issues]. ## Alternatives All these execute the underlying script *every* time `` is pressed (slow and have side-effects): - [argcomplete](https://pypi.org/project/argcomplete) - [pyzshcomplete](https://pypi.org/project/pyzshcomplete) (only provides `zsh` completion) - [click](https://pypi.org/project/click) (don't want to migrate away from `click`? Use `shtab`'s one-liner `click` integration [in the CLI](use.md#click) or [in a library](use.md#library-usage)) ## Contributions Please do open [issues] & [pull requests](https://github.com/tqdm/shtab/pulls)! See [CONTRIBUTING.md](https://github.com/tqdm/shtab/blob/main/CONTRIBUTING.md) for more guidance. [issues]: https://github.com/tqdm/shtab/issues [![git-fame](https://git-fame.cdcl.ml/gh/tqdm/shtab?min=1&w=1&M=1&C=1&enum=1)](https://git-fame.cdcl.ml/gh/tqdm/shtab?w=1&M=1&C=1&enum=1) [![Hits](https://cgi.cdcl.ml/hits?q=shtab&style=social&r=https://github.com/tqdm/shtab&a=hidden)](https://cgi.cdcl.ml/hits?q=shtab&a=plot&r=https://github.com/tqdm/shtab&style=social) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9314134 shtab-1.12.1/docs/pydoc-markdown.yml0000644000175100017510000000453615245646712017000 0ustar00runnerrunnerloaders: - type: python search_path: [..] processors: - type: filter documented_only: false - type: pydoc_markdown_shtab.ShtabProcessor - type: crossref renderer: type: mkdocs markdown: source_linker: type: github repo: tqdm/shtab use_branch: true source_position: before signature source_format: |- [[view source]]({url}) pages: - title: Home name: index source: index.md - title: Usage name: use source: use.md - title: Reference name: ref contents: - shtab.FILE - shtab.DIRECTORY - shtab.glob - shtab.cmd - shtab.complete - shtab.add_argument_to - shtab.click.add_command_to - shtab.click.option # external links - title: ↗ Source Code href: https://github.com/tqdm/shtab - title: ↗ Changelog href: https://github.com/tqdm/shtab/releases - title: ↗ Issues href: https://github.com/tqdm/shtab/issues - title: ↗ Contributing href: https://github.com/tqdm/shtab/blob/main/CONTRIBUTING.md - title: ↗ Licence href: https://github.com/tqdm/shtab/blob/main/LICENCE mkdocs_config: site_name: shtab documentation site_description: Automagic shell tab completion for Python CLI applications site_url: https://tqdm.github.io/shtab/ site_author: Casper da Costa-Luis repo_name: tqdm/shtab repo_url: https://github.com/tqdm/shtab copyright: | © Casper da Costa-Luis @casperdcl 2021-2026 extra: generator: false theme: name: material logo: https://github.com/tqdm/shtab/raw/main/meta/logo.png favicon: https://github.com/tqdm/shtab/raw/main/meta/logo.png custom_dir: ../../custom_theme/ features: [content.tabs.link] palette: - scheme: default primary: white toggle: icon: material/toggle-switch-off-outline name: Switch to dark mode - scheme: slate primary: orange toggle: icon: material/toggle-switch name: Switch to light mode plugins: - search - minify: minify_js: true minify_html: true markdown_extensions: - admonition - toc: permalink: '#' - pymdownx.superfences - pymdownx.tabbed: alternate_style: true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9314134 shtab-1.12.1/docs/pydoc_markdown_shtab.py0000644000175100017510000000203715245646712020064 0ustar00runnerrunnerimport re from functools import partial from pydoc_markdown.contrib.processors.pydocmd import PydocmdProcessor sub = partial(re.sub, flags=re.M) class ShtabProcessor(PydocmdProcessor): def _process(self, node): if not getattr(node, "docstring", None): return super()._process(node) # convert parameter lists to markdown list c = sub(r"^([a-z]\w+)(:.*?)$", r"* __\1__\2", node.docstring.content) # fix file cross-references c = sub(r"<(?:\.\./)+(\S+)>", r"[\1](https://github.com/tqdm/shtab/blob/main/\1)", c) # fix code cross-references c = sub(r"([sS]ee )`(shtab\.)?(\w+)`", r"\1[`\2\3`](ref.md#\3)", c) # convert REPL code blocks to code c = sub(r"^(>>>|\.\.\.)(.*?)$", r"```\n\1\2\n```", c) c = sub(r"^(>>>|\.\.\.)(.*?)\n```\n```\n(>>>|\.\.\.)", r"\1\2\n\3", c) c = sub(r"^(>>>|\.\.\.)(.*?)\n```\n```\n(>>>|\.\.\.)", r"\1\2\n\3", c) node.docstring.content = sub(r"^(```)(\n>>>)", r"\1python\2", c) return super()._process(node) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9314134 shtab-1.12.1/docs/requirements.txt0000644000175100017510000000022315245646712016570 0ustar00runnerrunnermkdocs-material git+https://github.com/tqdm/jsmin@fix-pip#egg=jsmin mkdocs-minify-plugin pydoc-markdown>=4,!=4.8.0 pygments pymdown-extensions>=11 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9314134 shtab-1.12.1/docs/use.md0000644000175100017510000002404615245646712014433 0ustar00runnerrunner# Usage There are two ways of using `shtab`: - [CLI Usage](#cli-usage): `shtab`'s own CLI interface for external applications - may not require any code modifications whatsoever - end-users execute `shtab your_cli_app.your_parser_object` - [Library Usage](#library-usage): as a library integrated into your CLI application - adds a couple of lines to your application - argument mode: end-users execute `your_cli_app --print-completion {bash,zsh,tcsh,fish,powershell}` - subparser mode: end-users execute `your_cli_app completion {bash,zsh,tcsh,fish,powershell}` ## CLI Usage The only requirement is that external CLI applications provide an importable `argparse.ArgumentParser` object (or alternatively an importable function which returns a parser object). This may require a trivial code change. Then simply put the output of `shtab --shell=your_shell your_cli_app.your_parser_object` [somewhere](index.md#scripts) your shell looks for completions. Below are various examples of enabling `shtab`'s own tab completion scripts. !!! info If both `shtab` and the module it's completing are globally importable, eager usage is an option. "Eager" means automatically updating completions each time a terminal is opened, and likely should *not* use the `-u, --error-unimportable` flag. Terminal start might be slow if scripts are very complex. === "bash" ```sh shtab -u --shell=bash shtab.main.get_main_parser \ | sudo tee /etc/bash_completion.d/shtab ``` *Eager* ```sh # Install locally echo 'eval "$(shtab --shell=bash shtab.main.get_main_parser)"' \ >> ~/.bash_completion # Install system-wide (pkg-config bash-completion --variable=compatdir) echo 'eval "$(shtab --shell=bash shtab.main.get_main_parser)"' \ | sudo tee /etc/bash_completion.d/shtab # Install system-wide (pkg-config bash-completion --variable=completionsdir) echo 'eval "$(shtab --shell=bash shtab.main.get_main_parser)"' \ | sudo tee /usr/share/bash-completion/completions/shtab ``` === "zsh" !!! info `zsh` requires completion script files to be named `_{EXECUTABLE}` (with an underscore prefix). ```sh # note the underscore `_` prefix shtab -u --shell=zsh shtab.main.get_main_parser \ | sudo tee /usr/local/share/zsh/site-functions/_shtab ``` *Eager* Place the generated script somewhere in `$fpath`. For example, add these lines *to the top* of `~/.zshrc`: ```sh mkdir -p ~/.zsh/completions fpath=($fpath ~/.zsh/completions) # must be before `compinit` lines shtab --shell=zsh shtab.main.get_main_parser -o ~/.zsh/completions/_shtab ``` === "tcsh" ```sh shtab -u --shell=tcsh shtab.main.get_main_parser \ | sudo tee /etc/profile.d/completion_shtab.csh ``` *Eager* ```sh # Install locally echo 'shtab --shell=tcsh shtab.main.get_main_parser | source /dev/stdin' \ >> ~/.cshrc # Install system-wide echo 'shtab --shell=tcsh shtab.main.get_main_parser | source /dev/stdin' \ | sudo tee /etc/profile.d/completion_shtab.csh ``` === "fish" ```sh # Install locally shtab -u --shell=fish shtab.main.get_main_parser \ -o ~/.config/fish/completions/shtab.fish # Install system-wide (pkg-config fish --variable=completionsdir) shtab -u --shell=fish shtab.main.get_main_parser \ | sudo tee /usr/share/fish/vendor_completions.d/shtab.fish ``` === "powershell" !!! info PowerShell 7+ (`pwsh`) is fully supported, while Windows PowerShell 5 (`powershell.exe`) is only partially supported. ```powershell New-Item -Path ~\.config\powershell\completions -ItemType Directory -Force shtab --shell=powershell shtab.main.get_main_parser --error-unimportable ` | Out-File -FilePath ~\.config\powershell\completions\shtab.ps1 # Add to $PROFILE: . ~\.config\powershell\completions\shtab.ps1 ``` Eager: Add the following to your PowerShell profile (`$PROFILE`): ```powershell shtab --shell=powershell shtab.main.get_main_parser ` | Out-String | Invoke-Expression ``` Or save to a file and dot-source it from your profile: ```powershell New-Item -Path ~\.config\powershell\completions -ItemType Directory -Force shtab --shell=powershell shtab.main.get_main_parser ` | Out-File -FilePath ~\.config\powershell\completions\shtab.ps1 # Add to $PROFILE: . ~\.config\powershell\completions\shtab.ps1 ``` ### argparse Any existing `argparse`-based scripts should be supported with minimal effort. For example, starting with this existing code: ```{.py title="main.py" linenums="1" #main.py} #!/usr/bin/env python import argparse def get_main_parser(): parser = argparse.ArgumentParser(prog="MY-PROG", ...) parser.add_argument(...) parser.add_subparsers(...) ... return parser if __name__ == '__main__': parser = get_main_parser() args = parser.parse_args() ... ``` Assuming this code example is installed in `MY_PROG.cli`, simply run: === "bash" ```sh shtab --shell=bash -u MY_PROG.cli.get_main_parser \ | sudo tee /etc/bash_completion.d/MY-PROG ``` === "zsh" ```sh shtab --shell=zsh -u MY_PROG.cli.get_main_parser \ | sudo tee /usr/local/share/zsh/site-functions/_MY-PROG ``` === "tcsh" ```sh shtab --shell=tcsh -u MY_PROG.cli.get_main_parser \ | sudo tee /etc/profile.d/MY-PROG.completion.csh ``` === "fish" ```sh shtab --shell=fish -u MY_PROG.cli.get_main_parser \ | sudo tee /usr/share/fish/vendor_completions.d/MY-PROG.fish ``` === "powershell" ```powershell New-Item -Path ~\.config\powershell\completions -ItemType Directory -Force shtab --shell=powershell -u MY_PROG.cli.get_main_parser ` | Out-File -FilePath ~\.config\powershell\completions\MY-PROG.ps1 . ~\.config\powershell\completions\MY-PROG.ps1 ``` ### click Speedup `click`'s completions (and get support for more shell types) by changing from e.g. `_MY_PROG_COMPLETE=bash_source MY-PROG` to `shtab MY_PROG.cli.main --prog MY-PROG -s bash` ## Library Usage !!! tip For more, see: - `shtab`'s own [examples/](https://github.com/tqdm/shtab/tree/main/examples) folder - [git-fame#135](https://github.com/casperdcl/git-fame/commit/d2577f821368fd2337e975dc6a31ce73ab09e7ed) for how to migrate to using `shtab` in a real project Complex projects with subparsers and custom completions for paths matching certain patterns (e.g. `--file=*.txt` or `--branch=$(git branch)`) are fully supported (see [examples/customcomplete.py](https://github.com/tqdm/shtab/blob/main/examples/customcomplete.py) or even [treeverse/dvc:commands/completion.py](https://github.com/treeverse/dvc/blob/main/dvc/commands/completion.py) for example). Add direct support to scripts for a little more configurability: === "argparse" ```{.py title="pathcomplete.py" linenums="1" hl_lines="5 7 8 10 13"} #!/usr/bin/env python import argparse, shtab parser = argparse.ArgumentParser(prog="pathcomplete") shtab.add_argument_to(parser, ["-s", "--print-completion"]) # magic! # file & directory tab complete parser.add_argument("file", nargs="?").complete = shtab.FILE parser.add_argument("--dir", default=".").complete = shtab.DIRECTORY parser.add_argument("--config")\ .complete = shtab.glob('*.toml', '*.yml', '*.yaml', '*.json') # WARNING: shtab.cmd is (re)run by your shell on each tab press, so could be slow parser.add_argument("--branch", help="git branch from current workdir")\ .complete = shtab.cmd("git branch") def main(args=None): args = parser.parse_args(args=args) print(f"received ={args.file} --dir={args.dir}" f" --config={args.config} --branch={args.branch}") if __name__ == '__main__': main() ``` === "docopt" Simply use [argopt](https://pypi.org/project/argopt) to create a parser object from [docopt](https://pypi.org/project/docopt) syntax: ```{.py title="docopt_greeter.py" linenums="1" hl_lines="16"} #!/usr/bin/env python """Greetings and partings. Usage: greeter [options] [] [] Options: -g, --goodbye : Say "goodbye" (instead of "hello") Arguments: : Your name [default: Anon] : My name [default: Casper] """ import argopt, shtab parser = argopt.argopt(__doc__) shtab.add_argument_to(parser, ["-s", "--print-completion"]) # magic! def main(args=None): args = parser.parse_args(args=args) msg = "k thx bai!" if args.goodbye else "hai!" print(f"{args.me} says '{msg}' to {args.you}") if __name__ == '__main__': main() ``` === "click.command" ```{.py title="click_process.py" linenums="1" hl_lines="5"} #!/usr/bin/env python import click, shtab.click @click.command('click-process') @shtab.click.option() # magic! @click.argument('out-dir', type=click.Path(file_okay=False), required=False) @click.option('--config', type=click.File(), help="Config file.") @click.option('-q', '--quiet', is_flag=True, help="Suppress output.") def process(config, out_dir, quiet): """Click example CLI with shtab.""" if not quiet: print(f"Reading from {config} and writing to {out_dir}") if __name__ == '__main__': process() ``` === "click.group" ```{.py title="click_subcommand.py" linenums="1" hl_lines="17"} #!/usr/bin/env python import click, shtab.click @click.group('click-subcommand') def main(): """Main (root) CLI group command.""" @main.command() @click.argument('out-dir', type=click.Path(file_okay=False)) @click.option('--config', type=click.File(), help="Config file.") @click.option('-q', '--quiet', is_flag=True, help="Suppress output.") def process(config, out_dir, quiet): """Click example CLI with shtab.""" if not quiet: print(f"Reading from {config} and writing to {out_dir}") shtab.click.add_command_to(main) # magic! if __name__ == '__main__': main() ``` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2380848 shtab-1.12.1/examples/0000755000175100017510000000000015245646720014174 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9314134 shtab-1.12.1/examples/click_process.py0000755000175100017510000000142215245646712017374 0ustar00runnerrunner#!/usr/bin/env python from importlib.metadata import version import click import shtab.click ARG_HELP = version('click') >= '8.5' @click.command('click-process') @shtab.click.option() # magic! @click.argument('out-dir', type=click.Path(file_okay=False), required=False, **({'help': "Output directory."} if ARG_HELP else {})) @click.option('--config', type=click.File(), help="Config file.") @click.option('-q', '--quiet', is_flag=True, help="Suppress output.") def process(config, out_dir, quiet): """Click example CLI with shtab.""" if not quiet: print(f"Reading from {config} and writing to {out_dir}") if __name__ == '__main__': process() # pylint: disable=no-value-for-parameter ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9320602 shtab-1.12.1/examples/click_subcommand.py0000755000175100017510000000135615245646712020054 0ustar00runnerrunner#!/usr/bin/env python from importlib.metadata import version import click import shtab.click ARG_HELP = version('click') >= '8.5' @click.group('click-subcommand') def main(): """Main (root) CLI group command.""" @main.command() @click.argument('out-dir', type=click.Path(file_okay=False), **({'help': "Output directory."} if ARG_HELP else {})) @click.option('--config', type=click.File(), help="Config file.") @click.option('-q', '--quiet', is_flag=True, help="Suppress output.") def process(config, out_dir, quiet): """Click example CLI with shtab.""" if not quiet: print(f"Reading from {config} and writing to {out_dir}") shtab.click.add_command_to(main) # magic! if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9320602 shtab-1.12.1/examples/customcomplete.py0000644000175100017510000000723315245646712017617 0ustar00runnerrunner#!/usr/bin/env python """ `argparse`-based CLI app with custom file completion as well as subparsers. See `pathcomplete.py` for a more basic version. """ import argparse from textwrap import dedent import shtab # for completion magic # WARNING: shtab.cmd is (re)run by your shell on each tab press, so could be slow COMPLETE_TOKEN = shtab.cmd("head -c5 /dev/random | base32") # override powershell function to a Windows-compatible approximate equivalent COMPLETE_TOKEN['preamble']['powershell'] = ( # type: ignore[index] f"function {COMPLETE_TOKEN['powershell']}" " { Get-Random -Count 1 -InputObject (10000..99999) }") def process(args): print(f"received ={args.token} [={args.suffix}]" f" --input-file={args.input_name} --output-name={args.output_name}" f" --compose-file={args.compose_file} --greeting={args.greeting}" f" --hidden-opt={args.hidden_opt}") main_parser = argparse.ArgumentParser(prog="customcomplete") subparsers = main_parser.add_subparsers(dest="subcommand", required=True) parser = subparsers.add_parser("completion", help="print tab completion") shtab.add_argument_to(parser, "shell", parent=main_parser) # magic! # mypy: disable-error-code="attr-defined" parser = subparsers.add_parser("process", help="parse files") # dynamic command tab completion builtin shortcut parser.add_argument("token").complete = COMPLETE_TOKEN # file tab completion builtin shortcut parser.add_argument("-i", "--input-file", dest="input_name").complete = shtab.FILE # directory tab completion builtin shortcut parser.add_argument( "-o", "--output-name", help=("output file name. Completes directory names to avoid users" " accidentally overwriting existing files."), ).complete = shtab.DIRECTORY # glob pattern tab completion builtin shortcut parser.add_argument("--compose-file", metavar="yaml").complete = shtab.glob("docker-compose*.yml", "docker-compose*.yaml") parser.add_argument("suffix", choices=['json', 'csv'], default='json', nargs='?', help="Output format") # custom tab completion function parser.add_argument("--greeting").complete = { 'bash': "_shtab_txt_or_hello", # NOTE: _files -g '(*.txt)' doesn't work inside $() # NOTE: without subdir recursion: "($(printf '%s ' *.txt(N)) hello salut hola ciao)", 'zsh': "($(find -name '*.txt' | sed -e 's,^./,,') hello salut hola ciao)", # NOTE: f:{*.txt} doesn't work alongside ``/() 'fish': "(_shtab_txt_or_hello)", 'tcsh': "`_shtab_txt_or_hello`", 'preamble': { 'bash': dedent(""" _shtab_txt_or_hello(){ # NOTE: function name `*_file*` implies `compopt -o filenames` compgen -f -X '!*.txt' -- $1 compgen -d -- $1 # recurse into subdirs compgen -W 'hello salut hola ciao' -- $1 }"""), 'fish': dedent(""" function _shtab_txt_or_hello set comp (commandline -ct) __fish_complete_path "$comp" | string match -e '*.txt' __fish_complete_path "$comp" | string match -e '*/' # recurse into subdirs string match -e -- "$comp" hello salut hola ciao end"""), 'tcsh': "alias _shtab_txt_or_hello" " 'find -name \"*.txt\" | sed -e \"s,^./,,\" ; echo hello salut hola ciao'", }} # yapf: disable # SUPPRESS help to exclude from CLI --help & completions parser.add_argument("--hidden-opt", action='store_true', help=argparse.SUPPRESS) parser.set_defaults(func=process) def main(args=None): args = main_parser.parse_args(args=args) args.func(args) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9320602 shtab-1.12.1/examples/docopt_greeter.py0000755000175100017510000000110515245646712017554 0ustar00runnerrunner#!/usr/bin/env python """ Greetings and partings. Usage: greeter [options] [] [] Options: -g, --goodbye : Say "goodbye" (instead of "hello") Arguments: : Your name [default: Anon] : My name [default: Casper] """ import argopt import shtab parser = argopt.argopt(__doc__) shtab.add_argument_to(parser, ["-s", "--print-completion"]) # magic! def main(args=None): args = parser.parse_args(args=args) msg = "k thx bai!" if args.goodbye else "hai!" print(f"{args.me} says '{msg}' to {args.you}") if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9320602 shtab-1.12.1/examples/pathcomplete.py0000755000175100017510000000213415245646712017237 0ustar00runnerrunner#!/usr/bin/env python """ `argparse`-based CLI app with file/dir tab completion. Uses `add_argument().complete = shtab.(FILE|DIR|glob(...)|cmd(...))`. See `customcomplete.py` for a more advanced version. """ import argparse import shtab # for completion magic # mypy: disable-error-code="attr-defined" parser = argparse.ArgumentParser(prog="pathcomplete") shtab.add_argument_to(parser, ["-s", "--print-completion"]) # magic! # file & directory tab complete parser.add_argument("file", nargs="?").complete = shtab.FILE parser.add_argument("--dir", default=".").complete = shtab.DIRECTORY parser.add_argument("--config").complete = shtab.glob('*.toml', '*.yml', '*.yaml', '*.json') # WARNING: shtab.cmd is (re)run by your shell on each tab press, so could be slow parser.add_argument("--branch", help="git branch from current workdir").complete = shtab.cmd("git branch") def main(args=None): args = parser.parse_args(args=args) print(f"received ={args.file} --dir={args.dir}" f" --config={args.config} --branch={args.branch}") if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2382543 shtab-1.12.1/meta/0000755000175100017510000000000015245646720013304 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9320602 shtab-1.12.1/meta/logo.png0000644000175100017510000003742115245646712014762 0ustar00runnerrunner‰PNG  IHDRÈÈ­X®ž IDATxÚí½wœ%Wy ýœªº±sšœsÒŒF !B �d‚°Ákƒ×Bø3’°YÛË~þXï:` ÛÃl¬f‘� !3Œ¤Éy¦sî›oU�³Üž™uêæîÛ=÷èWê;•ëœ÷9o8 ꩞꩞꩞꩞꩞꩞æ.‰zT/í¹óÃb”8�*jHhV¨% –†Õ®¤Û¤PM(F¥„H aÄ„RºC „bTRÙBùöߨzÎÖYpiÓ�"LwÌÂ0Ö aìA©] ¶)جAÑŽÀ@)Pä}î!ä$8ƒN*8.âÒ= 1Ž>ò€¬—@�šJWÝñ—"Å@£\‹R¯V¨WWí“ÌAé 8+à9„øЧ i:ø�¿vê%TdÎÓ–;ÿÀ4”{‚_RJÝ \‡RÑÚÉG!ô xÄwQò± j<ð��ÕͲ: ÕI;ï¼Ïp•Ø ¼K¡îD©�€±0JWd€§…_Eª¯~äÁáz‰Ö©HÚzÇ}K…¿¦”ú ”Ú¹` ÐÃ’ððË>ûÒ£Y÷[ꀗ®¼õ/D:2°[¡îÞ‰R ‹ð3%BüDñ—¦m}ëåo}Ü­—|0n¹_¤£\ ê£JñfPfå*nA0¤¡¡�p$B0 ±, Ã41 Äd1(…”.®ãà:v6K6›!•L‘H&pl› : !žˆ?jp#?|á›ÿ³î§Ô™�¶Ýqÿfà*%ß”†aš477ÓÜÒJCcáhÁp˲¦Še^¹õrø¡2u<.3@¶ÜqŸa ~O)ùç@¸ÐëLÓdé²åt-[AcK+ÂEp Ó ª Nò¢´7°³F‡‡è9žx"Q„21¾-”úÕC�<8^är�ã—ïR}V)õë…~w4eÕÚõt,Y†@ƒª)@.�¢ˆ��Ñ}î CÃ#…ŠÆO¼ùð#ŽÖYôšãC æ¿)%o+äüæ¦&ÖlÜL[GB¢W 3ö«"̯‚¬#¥9OyÂãôÉ㌎O¢J~ à͇~0UdñšU!ñJÉ·ä;7³aË6Ú—.Gá#|:yœ}²T ×u±]‰ëJWáJ‰”©jÊ#¹ˆ¯aL!0 �e7Ó4< ¬@&ñVŠ¡þ^Ž;†í¸ùÌ­/ ú­Cýùeé¸[‹ùã6¾ë^a¤ù»|p!X·~=+×o´ÓÊÓÝÐ;̶ã’q\²vî¯ãÊ"5ˆ×Ë2SB´LB“ e “í(^$ 4!]ËWÒÜÚÎÑC/1:¦×&JÉ߀ÌSÀ—êd±E«î¸ÿ½JÉ/ø}g(bçž½´´uh3hËÒf¶­j¥!Äq]Î Æøù™aÒ¶Äv]ÒY‡tÆ!m;.A±>Hqû¦  Z„æ¤æÓ=wö=\×åä‘Ct÷öûÕ #±ûðß讲Xà¸óë”’PªMwNKK+»ö^M(ìкjM;o¹~3KÛ›.6v_H‰t–þü$Ÿ{â ¶T…‡y ¤ÿCˆ†DÃÂA1³x5½R’‡q®§×’/M4¦ß×û•Ï\V¦–¹X?¬cëõŸ@©tÇÛÚ;¸âêë†BžÇßzÕÞñÚ�47„fÁ´L6¯îäªõ<{¨‡Œ#}ê )›¸°1å·Ç~¼öÏÜÄôG¶+If²$ÒÙI 3E«ïMhíè 91N2•Ö}ÇÎ�ø·áÃÏ×Yàiû�÷¯Sð9��ÕØÔÌîk^A0ðÔWmYÊ�¯ÚŽiäW°- l^ÚÄ“/uO ›˜± çUØ”‚ŒíOgA)‚C¼ÿC´µ·Ó×Û‹+=a7Byö;— Æbü(¥øu”òT †i±óÊ« ƒÓä±LÞrý Q¸õ¹{ÓJÞ¼{…·B(zÝ&RYúG㤳þ ƒ¡0›7oò1èÔ¯lˇÃu@pºú¶? u»îø†Í[hjjÔÖ¼¯Úº”æ†PqŽœ€[®Ùˆ¨ÈEj O­4û>®T M$ˆ%Ó¾€.Y¾œH8¤«y–(K]_d§¸9Ú \ái#˜+V­ÑTî¿-+ÛKzîêemDƒf…´H¹G³ÁD2ËD"­ý~Ó0Y½j•>¨£¸©È‚NrJ¼Žt-]J(Ì ‹1ua€«MEj�©Nûò–HAµùL¡ôü¯P3Šba‚X*K2cký¤¶Ž?3ëª: ùƒLk‰îXkk‹ÖQU †'R(Yz3'¬yjóPj·r4…F{L=<‘¹hÎ "444æ ½Óæí·Ȩ²`“ÐöZc>žÊâHÉh¬´nGYÇ¥?–Î/ØõMDɾŠR )¥çyVÀ$ÒjÒNW)QdÁò¡?]¬=ÎHÙ¹ÏÁ3%=ö\ß(i[æìùòK<Ì6©”ç ‚P0¨³±¢B-Þö³Ë!Ì+üØÑ†CUîïwô0OVæ‰NV•f¨•¹v[åó8Cßd*aÖ5ÈÂMJø…có™ )Ûåk?xY„/ò≾w¨¯BíUÔ,ºMoÐÕ,ÆÞ¼*Ýì¥g¦öÜøîË=t4‡yë�Û1Mÿ:äØÙA|dߤ°ùB•1)KÎC§ouH- ~&–¶Ø/ÙéÒ¿<{Š3¼ã5;XÑÕ2Kã©,Oí;ÉÿÿÌ1©|äTh •«T1ÏT³4ªw~ÔáX¤€øDX¦ª5CT=Ì÷Ÿâ''Ÿf÷Êv­í 1Âv\Î Œóããƒ$'»n_¡÷›Q æÞ¾ wÚ˜F��E ˆòÕ “É 9¹À†gÿ+ºÇ9Ð=~QÄ.˜šu~1æ”Èo©Î÷T…½‡Sk‡X'ä24±.˜Q#ð.8²ùjçœ<£)Šu–„ævJ¯0Ei9#„˜¬Ô,àê>ÈeÈ…±^6»§¢3å½v Q!‡¼�•+¨û ‹ß(–Д¼¢­)ì1^\Íri”ïnªˆ÷+~È­F&RSÝle˜uBÔI¨k ˆÚ@8`z «*·2®Ô�Û ÂÓ‡ÎÚëãÂ(ª)É7Ì[g±’§¡PÅTáµ¼ò•ÌâŸ)|‚ Óª"ž©ËQx¤£È"öA˜â¤:�›¨Ö+©’8x;Õ¥¼¸¨k�: ÚÅ´3x i¹žpµüŒV^D<+¤¡°«º �t¥¯…¾%½8ÜTµYöõ‡ÄL;ɳÄËF›½_xDß2Y—´]_(·æyÍ�&žzè�Àê×ý‡(zÔ�� žú&]˱*Â�QÞí ¢Äs4ÊCà…šñþJÙ �uI¼ 3Y‡ÞÑ8®¬/2ï€ìzë=¡eÂ7¢¸r@ nÚzÇ}KQª~TÒ»)‰v¢¸}ûö|ùàt3IL1›<3˳/´i¤TmsÝu×�6Ìx%0¦q+|fwy™¹Ôú“;�ÊØô�&&{2×�¯ydÛí÷6!ŒwÛ¨_®URæâ¹ÉTÚgb´²'GúÍöœ'M"Uˆ•¦òZ]S£zñT–þ±ª®8æ�ío½7¨ ã õ_Pry=û‹sO<ûH©"î7ËdÌÙX£±Ã3†׃Xs ȶ;ïß)•ú2J^SÏöü²,´nÔL[©ô®) EÿhŒX*[ÏôùdÛ÷¿M)õ%”j®gy€ø´ø‹™Pˆ"Ö0™WÖv8;0®Z[Ï Ûî¼ÿ½Jª¨gw¡rìmS—܈‚Ƕϼ…š4©’tÇYÒ% |y�ÊÃñ.%Õç@üœÜJJ¦vn\¿¡ªÃÜß Uy#TjFÿ,•g¹‚YïTèªÐšvÄYa^mw÷ÙÛŽK÷ÐãÉòLªÛׯ�·|¡²3›Ø)�”-¤ˆ(ÞxjQ²íÎ_§¤ûÅ|ϰL“MÖ²|ÅJš[Z †Ã—–VÎ+E•ZœF®R@¤˜²Ü²š£šˆò¸±šr0°4ãVfVî~½y/�%•bd,AÏH<7ÕOiú€°!ƒoÜö•³n…‰6 \à ¥L5,àô÷?ÍË~"á™›ïabQ²õöûÚ•’ÿDýÌá�[7³eûN‘hžÒR…U³ªÀb/hÞÅ‘C3Î,³ccɽysmžD70‘HÓ3#m»ÃÕ‡�…TªY5™\§Wo”!ˆ?ñi7_p\»åwq4 ;nû=á @© ºs‚‹W¿úF–._Y Œ;)O_©²úaÍE íºƒÆ´fï|Q bÉ4}# ⻼÷˜gwL*šP¼M ~Y<ùþ[ Í7_ußܘ`ŸKo ·¹g ÜrËX¶beqÓfVrn) œHZTbÎ(Ý Sy^N„Âk' �27 êȹ!Ž÷Ž•GíùèJ!”b�ãòp&Äüà³,[pdû÷¥’ŸÐ�g‚×ÝôÚÉÙë7\4ï Qö*ðŽÅJ¨¸Äô޼Šd&ËèDŠÁXêrèC%¤ämJqÝ“Ÿá]¯û?^0€(Äo ä.ÝñW\½—Î¥K 3ƒ ®ÎJœjG•$¥ú½¢À›ªRëèKa^©ÉT–‰d†áXŠŒír¹%¥Xåº<þýOóž×ßÃC5ÈöÛï�HÔ鎯Z¶„ [¶M1Q”6To»rJTH/wþs•/ƒt?ôØ/ôç+�²R…±ëÚ+;K2ms²g„±dö’¦¨Rç©Rºš˜�Dš7é³ÍOृ“ÅÎô!ݱ‚*Q©ø—'>Í]7ßÃ7jeð+HµÞ;³W]sÍŒ5'¦›A¶+I¦ïqª!. ú.t)¥«Çs)1>ÂØ`B´-YE¤±É÷&N6Crb Ó²ˆ6·MF¤ÄÅñâ®TØŽCÖvIgR›xÆ&™u¦WscÉ}Eë’kÙ{Ó;Jºí…"–®M2cl°�¾Óûï pý4IøÊ“Ÿá¦×}€Ÿ×$ Ûîø=S)uŸ6²µe#mížf�ROÛ$&J!ŠÉÙÕ±*šY.ŠÎ+öŽI©é?ÏÉÏâ:JJÎ>À†+o ¹cÙ´s•R(%<‚žc/’I%±³ÂM­¬ÚóZ0ƒdIÆvq\™·á±Æ Ÿ’O¹P¦ ©¥�¦–vVoÚÁØð8¾ÿûŒõ}Ë’&)ù÷Ç?Å+nù Ã5X¯×ù†lß¹Sk¥l'·X^ �tQ`íç3)›ëæ„5k;dl‡ŒíæþíH²Ž‹í*;Í�¯þ™ÞÃÓn{üضüÒû˜vïÑÓ/rìáO‚œUMجØ{smzÂ%_UÙøWkÇR®~ý»8{tÇ_ø{”ò^àH*6šóƒOóë7U°¾l@¶ßz·�¨ßÑå̶MëiljöÌL‰"ž¶g¨�bgé(QƒŽ+I¥mé,‰´MšHÚœûñCÁ1—I¦Ç8ýØçI wW RU($^›AÞžw2 Ù�>*«=¯¹m Ë6½GÿÍŠ>þ©ò¢’o°õö{MÚV¡uë×cL>`æ–ɺ‘ ÏÞÐlÓÏËdNõŽò³cœŒ‘qdM£3v¾f]èøà¹™þ£OݧJ„ÃçX1+Žª8dŽ‚rµ®Ûq=BDufÖÕ–ÁÞyDæ^›½Ž5FÂt-YêÙ³‘ó?Š[ãûR¶¤Ò6ÇÎ�ðü‰ºGSª'0¡éöµ¥/~YB§Ç\û�ÏÒÔJ·É ØÝè–¸nhj¥kíÛt€Rñkógb)y§Î1ؾe£6té8©”çÂöú®ê‚LÖáXwŒÞñTm64š6__“p:ÖѲâR}´ LC_üYÍÔ£ÂÇÄ4¬ˆ? Â(~s‡A¦´÷]µù­æRŠ·?öIÊš ¡$'}Ç[>l¸HmÛǪµk´­ÚiG^š]£)w\Éù� Î'jH[èM‰µ¯¼ƒSŽMâÔ …ÕêÍËóhj%¥V(…0PJÎú{ᘊжv'KwÜ@`J$«1ô5¯’)ïPÔrž`¸IŸ7Ê-Ýë±{!´ÉóPë’„¯!ÿ™§(‚\ ¥w‰/ ×�«Ï®% ‘0�]Ó{íN«™Ü<=BÄÅBMp¼o‚¬#©­ÎIúw D[Ørë{9ú˜ qêyá6±ç=ÿ Ó zÜ{rZQqá/Þ=wÕ”E§ž�º±†9í\! 1¢$•Jâ:Þ�†õ�i„¦ó`¸¡Í§tƒENäÂÄFd¶Ö&Ë6¾†Ó¿ø™WÖ©¸­@Jzca7¡¼g*Ù²q–iÎð)r¿/tÆ»¸_3^/™Êò‹“ì#ëJTˆ6³åÖߤqõù À´^›azÿ-ôü ¿�Ù¦nK$„iê‹~bDßßocÛ„¶‚hhnG;r’lqNúÌÍÑëZ¹ÉO;ÝúäçKØ•†´R¯ÑZ¹r…vHkÆ‘Ó@˜¸PŠ3ýãüôÄ£I›…œÑf6ßú›4¬¿:OVÎñ{™-�úÆ8éº ôi�¯kïÕݙƖŽéŠpê&3Å;éS7wT›YMm]#;tù»[féš3@¶Þþ{†BÝàmCgW—§Ó�€¬ãN×,S Š'³<¬Ÿ±šŸ@Ù°‚`ÌöýŒHË4s)ibó-¿AÃ:ïhcê­ÖÜͧ'„ «5ª�M`thGc^- Ù¬hù…ç±HÓ•„Zð ì+gÒ1Jß”›k#QƬÍ«nÔ4 n˜C b¶žSgk цϰ­”jÒDžº¹ñvç&øé‰b™…±hK ÚLÇî×ÏÚ¿ìª[f |°¡…Í·¾—†õWÍŠx­Øû†9[êÌ‚e­ „z·ÓulúΟÕ¿yM¦ó<Ö±ú•¹oñ 6©tyæÕ…MÆ´A²¶eü´ô«çÌI¸B)×󺵫VhûVÙ®œµZ&ëpðìƒñ ) !XsýŒ¼ô˜–\y3Ëwßäi ZØúKïgðèóŒž:@ ÒÄ’7вbÓœ¼oÐ2éj‰´üëÃþóg°ïJ*lH®Xq@ëxu­Ú¢÷d‚Šôòu' à=UTKÇòIqv¼B*7>÷·ˆWü^ñ‘ž¢Xj—.':»:š¡Ž+§u™HsàÌpÍ´€¯EšØðº÷°öúÛA‘FŸ�°"i`ŞײüŠWOY¯]Uâ°eÒ  '§^ltˆ�þ~íñ·¬íWM¡cžåŠ^I[×J= î騢²9sMÌ6K£�­£;È&x¯ˆ[4±ª¢PÛtÇZZZ<·b�\ØQ)ÅÙþqŽôŽ/˜ÅZ !LB“ i`ZÓ00„@ˆüWzREçðìŸÅ¯©^Œå–I%8sò¸öx{ÀU¯XûœöøÊ­oÂ04¢$Ó l*6ND¦À zVmË®¥ÿä¯\Š‚ÝÀ3Õ7±½:  ±¹yö H(¤Êm®+9|v˜óc©š‡"`4†-"‹`Àœæ+¨2Ö&/Š‘ïÞSÚHJLv&Í©#‡pýÄïÚvÂ�O[Þaê.VlØ]}íq�$˜­Þt×:úOzú ¸¶ê€lëG ‰½ÎÓÎ ˆ„ÃCRJ2¶ÃþƒŒ¦j7|+4…ƒ4F‚„&…¶ö/Ô”M§8uä éŒÞ¼±+fïXö=­œ¬Øúk„µcMÈѺ؋âÑœßÔ¾ÌÏQ¿ªúQ,aÀRO5Ü܈ežS{Æâ~r¤¿fá0„ ­!ÄêÎf:š#“p,ãØÁI¥õ#S—÷öO !²žiZ«Y·ã•hCKn|J ºß& ßT�žÇ›;¢YÇêžïþMñª¬( "M(<½Ñö–æKuÁ”׋§ùéñR58ûŸZ¢AZB¾m‹)I×e çý½=¾]ÚB©÷]¹ßmÕöIY·ç}„µÁ r ¥k íGdÁ˜ÝÍÝ Fˆ4ï!9þ´—.ÛdZD�Dõ|E ÏuÈ£ ‘Yާ8Ô=V“‘ªHÀ¤£9BÀ2YÔvÔYu\Ɔéï>G6›ÉWq¨ÿ|űìÊÖ…tç4´ÝÊš­Wë�™œ4‡ª0‰ƒJ ³x š:wxDl^¬ B5¡„ç‡CÓó²o$ÁÑÞ  WÖ B@Gc˜¦h¨¼2R¹‘‘JJÏqÊŽb©Bî¡.u嘱_¡pl›L*I|bŒ±á!¤›¿!VïÝ~ÒÙ¾ìÛÚ F+;^ùnLÓ§€3@Õf8‘0½×zil]A¿wy‰€ÅÖê¢hÐ� .eVÏp‚c}—ª• ǦÁ’Ö(Á¦ÙÉd²$ãã$b$âdRI¤ck§ª‰TdÝð¾ÇÝ=+¿ð›‡lãÕ¦¥Ó§›¾•¬Þ”-*£½w´¹CÏ•dKU}@[«“½C{†âë‹Í(§ù'$´èj‰ä|�_'›µfx —TlñèÅ&Kª÷ï~Ymì|·öX²þý¬Ýv��v�àôSÍù±¸8'ÖlH"�­èÂâR±¡º€äFãh‰œæè�•mdT:5G´7E ®ÐR©4ƒ}ÝŒöŸG:6‹=íjI©»v=G{Ã~_©n×ß9cjÕ™¦Õð¤WZ{Ì\ ÏõlQG›ADrl¶†\[]@|ì‰óà †Œ‰B>mNSk4H[S¸ s3Y›¾îsŒö�E¹Î¢#d(Þ±ñœºví“Â2Fý5pëÍì¾é·°>y)“9@ª©=¦jœ%h�`+°'{ÊKƒ¬~ìs·þçÂWX,ÖÄÒ®<‘ÊöqŒç#µ‡”Š¡¡AúNÃͦ=¡¸uÕ�ºaÝ>Z"ó–N´õfö¾áw G}BºÊ§·B…]È=\Ïg #i÷DºŒ$Fά ‰\0m¶£.]×Çù›{BšÂÚ €#�ÉrîÔ1âC=‹ lhHËV °céË¢)|¤ Bi^r»oú-‘<33Úýå «-ZƒxÈ›3Ù“„¢m$Æ=cÍ"@ _VºX$A.†8Ç©@"‹ŽæüpŒOÄ8{ô%œt¼HiÊ‚x{Љ/�Ø¡°é4N]±ÜcA?WlZ V!ä”ëÔÔEJÔôî‹jj§°©×X†2š‚Y£«!e,i7—4�³Z"GL�SðÔŠK6¼Ÿí¯x�`ž¼ts3'–´Ài©€Èé0:#“37*¡¨Î’JÑZ5@bBi¼tÇÑZ_˜sØJm‚®ÖHÞ�HƒƒCt åæ„›®ëˆeö,L¬jëN·FŽ ‘ìTHK©Ê¹YBT.¦Qê{f'믺—µÛn˜±*˜—¥3Îèta�“âv§š=s±žÑ­†Òô© R¸ãB`¢v&íˆ1g€tµD&Ÿ§´Õu_/}'Ô†Ñd¹¼ym{V¾¨šÃGB BSœ¾Ê[j>c‚ÆŽ[Ùvý¯ÓÚ¹ª��=èaªR¨Æk%€ìy°§›Ç–fódÞ6U � ÄµèـdÒzçÖ4ç¦Ì[‚„ƒþŸ4ÐÛCyÛ4,¡¸}]ׯý)Ñà©ùÆU_ó†6±öŠßdõÖ뱬z¸¹öaÌÏ +2'Àš­ ÓOCGªÈ¸Û £fªX=Kƒd³(ézN3c™ÕÏÄ€iÐÚà_°CƒƒôÇ®æoÛùKšž_ô-+¸žå[îbͶWih-кË™5óUg¸¤m‘ÌIDAT H=áõÞøX,†QœÌurk(�írN)®›e~I‰“ÍG<ü‚êÒÑöõ;bœ?ú¢/øåu½¼fããXæÈâ�h‰ ‘–W²|óY¾~áhKÁöÎP�ù‚#Û ‰§ÀhÖùÄ=*T�sò‹ÿçjÛ÷ŸÔ½€�IybË8Uê” ZDB–6_²Ù,§@I}(:h(Þ¿ë;–}—Å×»×À ®¤¡õJÚW^CçŠ-4·¯¸4LV`ï+ì¾Éyr�Š»ùá´!q2†Ô¶ø¸ô¾¥”8Ud2�ÐÂ�Nmi÷@ËÄÉV§uº­1äSé)Î?Œ“ÑûH�¦äž½ûXÓþt�Up€@x�í{‰4/'j@Ì“­òTjjVåÈñ‡þDm»ãþ} u‹·ó 0ITÐQ�ú2Òß�ÔD3š,ÉmÛŒ!¼Çf›Áµ\ñºÿNÇòÍ‹ ™‹HÉÄddJágT 9�9é—Àí/^ýϺÍul2é>�ém4�UdâtfB26FSûRo@‚•$d™“ �#ǤË`Ï9íµoßt†æðaM-bÛ ¥cù–E„39ug6„ÊΈ„UScˆÜóíÈvCæ¸=¥ÛšÂÒj�L*‰’�95�(îÍKD)õ<™›~zŠ��°T3n+7³xe&ªŽ†ô¯žƶuSø;rÏÊkô^¾í>–­Û›G`Ô¤ esNdMŽK—“ïåNšMj¶z†lËåB¬Zåü g ì¡\)çÔäœVpÔŒ6tÃjqý?BpämP\#WI€X®qÆ1eŠY=ÚâcH×Á0-O§ZˆÊ íõNÿøð€öØ/­ïIÌAO0¹ŠM{oóR:Ý‘1ïñU eem‡S§ûO°¤«•5«»|;�öô�ðó_'O±~Ý2ö^±‘`Ð*Qèg¾¯º¤‘Ütα—ñÎ0¸ç>WUÉ ¬ví¡øøˆßS-ë¥íËß|ÀÝzÇ}ÏÁl@¤”¤D›Û=DÉ ˜YMØX)ÉØð wÅ#„»cÙËÚþíëö¼�`¨IïÈ:ƒS¢+sÝI¥³üûןâìùKßµmó*ÞúK¯ ì±goÿßøös$“i¤›åäÉÓœ:q„wÞv%¦áæZ¥ÕÍ'gÃpa¿ºp®�›‹Jf@]¨ RSúrå¿ÈÓlÕ^3>|^§=””ÅMùSº’ö)¥Þîé‡LŒz¹n!åbB;Æ$›JjÍ«]Í©ÑÆÐÑv/ f6°bÓU³CåäàÌKïÕŸÿâgÏM“´ã§úøî“ûxÓ®&h¤ÁŸ¬ÁÇøßÿø=>þÙ} Œ^þ¦ˆàšu{Ø´&:þO…4G®¤Àlö¼§”ãÃ/éž“Š#sЧuÃo'Fé\µÑó²HÈ‚xyùôitôkÍß»|ØUJyJø’�¿J ¨ž Ní<…{��ìÅ4MZš£ØÙ4±ñ!†ùÔ÷¾ÃóO…øãßYE$œ XH©øé¾#Óàˆ¥CÙù¤RšÀZ¢ífŸDZÇt,îwB¤ç …zÁjö‰c#¸v30[ý[¦A8`’¶K×"–Ïø’L*©=¶¼yHîZ³Go6¹…N¡Y&@^—K;9DÿùC<ºÿÏì›>ZôñƒiØü—ß^G$œ›•)•ö¸R11+T¹ô3¸�õû}ß3ozÿœ¬0•KG~0»õŽû~¼Ã#ÊEb|˜fÍì{Má@y€ø8§vVWIÕî�x›‹M´v®ö.Dåνi%3�>©£�9ÀϺŸÏþ»¾øcŸîÇv½{=Á ÐÎ 滕¾�YíÚ{ öÕ>ÜH 8|2™7Oþìs€â{R3¬@I5³CŠê)s)Þ]L2é$ãÃû4ÁbJò윢$ßFˆ,J½ÔÝJ;ãif !h‰)qñN¿šP×zn ØžW6´ïöX|ô¢aâYªéL–t:K&c“JgH¥2¤ÓYRé ñDšx"E"‘"“±ÉÚŽãà8.Ž#q]©–i0³ôœ=Æ·Ÿx�ƒ'Ë›ÅñÏ>7H$dæ–»›7 ¢ª§H+´7îïA)mþ=�‘˜s@Bª¡7#âϯóÔñ¡>Ú—¯ñ6³¢AÆ“ÙI»¸’ÅSüýB �>Ò£@L 8ŽK"™$‘Lç¶DЉX’ññ8±Éd†¬m㺥.Í£”"à¤G8zè_ýÖqb©ÊåÁÇÿW?-M~ySγæÓF3  Ÿ˜ºïÜa‹ƒGnÿÃÒ>¼,@|ãOÕ¶;ïÿ¥Ô뼎œ?EÛ²Õž½{ !èl Ó?>ÿ3Z�†<…?ýX&c31‘"žH‹§ˆÇ“ŒŒÅ�O¤ÈfmOMHÅzyö¹çùú÷úªò-±”"Vµ,�Ç‘–ÖŠÉ.î³S:•`tðg:­™‘’o–üØòóL} !þ¥fñ©d‚‰á~Z:½#Ñp€VÛe¬#©Š2ÙL@Ä,Da04<ÁD,ÁD,ÁèXŒ±ñ8ét÷â “—î˜ õ�æé_¼ÀãÏŽVý{–´ Æ”ó*µnW;Wj öžGi-…à‡­AJ®�ÊÍ~øÁàßuÇ{Oõ#ÜÖfiK„€iÌ'"¹(•ׯì} Q\©è¥·„Ááq’©Ln¡ !.nápˆlbˆÇ¿ýxà³ß›8~ûí-ìÙôqÒ)pSÕß }‚@ûŒý—ÌÆÞ3ô.ø÷«ß_ºê³*#^Æß+ä{Ác”a"ÎpßY:W¬Ó^ ˆ„¤26‰t–´íú.°$*]» ŸZSˆYm¡B®Þ»�h4ÂñçhnjôÈØg�ý{â×ì\»³ã¢r¡?ÚÔ¿ ß÷��qàXþ¾_ï¼¥��ý?ëøÀÇŽú8éªv´E¡b\—«¨<„">>ÊÄè WòPY–]%¾3œZz éý–RÜîu¼ûäšÚºE|#SÑp€hØÒddõìßœà>Jvö±@ Èî][ؽkF·x™†±g þ8Ü  ø5[f½ÛXì(Ž�ûžw×øû�n¦½Õ*Akm—ÀWhÏì=wRÿþ‚o¿á†Ë ”Ÿö?öâc < A麜9´·¦×ÜÐ-=lèͯ™[âôü5Äšì X¤)á»éÓ»ßÜÀßt]í(¥mY8iÊw[+Àô®XǦçô3ºÒTþ¹±³Ê¤p›Ú'„þ…±qÎ9 m§¨]@y×ùvÐÿUú[ïÑqep!ðŸüû®[øäm¢«íÒ²‚AËÐv%�­.ô—K¯�ûºÏáØƒ:‹¤[*¾[3€ìÿòƒ aü1BhcŒöqöÐ>\Ç©½ K‡ðƒC@üôü¤ž¬š ]»Ë»aãí7Gø»ÿº‘Î6ë⹆€Mk½GÛµ6Ye[Ù-°Ö °Z=�IéröØs~&û—ßðÁòGçU4ttø¡ú@|ȯzêçØþg}§š{>Ä´èÓ´Mgb©, >C‚®êû½å¦vn{õt¡÷›øÔÿ»‰%�Y¾ÜÛnédæÚA¿ug›×…kS[xž‚Èí%½=$cupd€/T"ï­JfP¥ÿÍá›”’¿£;'•ˆqdß³t,]AçʵDšç»'þí 3Žeú`àKà›“7ëj·øòŸmæûÏ�s¶7Íö Q^{m �Q¯¡´×îjâ[ÿ°‰Ïµ�ÓÝîx};¿ùËK Œy­† —ÊeÝfï9$'ýØO{|ýõ÷pº&yñ‘O«­w~ø^›Pêf}tF1Ô×ÍP_7áH”–Ž. M‚¡Üp]QHÞΞÞÓ±KíϤaæ¥�ÒÄ>þl‘ã«§Tï�µ%…U;—À]o-õW_7^·W*–Q%�ŸÜtfÒ´ýrÊ~‰g[‡µ‚KÁòŸLûüéS$c/鲨5Tê+­jÔGz ½íöûÞ©�‚º>ßùéT’ôù3óX¹ ¡�ô5¤ Cß‚Ø×Êx� WP¹v‘y£r·«‰”L&9ñòwüê G¥Ë •z^ÕtîáG5àMBßY¡EŸ–“€žÏ• G•á•ØÊj¯¾Û"]——_x×ÐÁa‚?¹ùw+×°SU£ôÐ#Ž!Äñù§Ì›ç –ÆIÏB÷_@æÙ¹ m.Ȥª ¬’’Cžg|Hï{‚juÙ_ɯªº×vø¡²�©ðaÜŠ//( ’é��Ïçætª§yKRJ¿ø½§õ3­|tïïV¶š“°Æ �ý¹:üð'¾o(®Âx?B¬½êt©0öÍ\בªvÚ[䊥̔N§ÙÿÓÒ}ò›~%§ Áý¯»‡Š�!°æòc=ò`øÂÖÛ?ô�Â0o˜®{ °ir9…ùs§†™ã/Aü).6eWßÀ«!HDM8ìétšžsg9{ôû8¶¿Ü ƒ&Ëÿ®Æ{XóññGù¤<<µõÍ1DÀéÄ0¶£Ô:…Z*„õê\NÚÒ”n·Ìô¼‹^]úÛ‰)v®ªt¯<_!›IÓsþô”ŽÅâ’œ^úßäOqQ˜§wD¾ôo!¦ û´ЫG´ÐWžÄˆüÍŒó…çe“£T”ÂvlÒÉ8cÃ=LŒìGÉücñ ÁÏ”âƒ7ß[�*ÆšïúêÈ£—ÀÀäöÃj=çï>%¶¨»¥vr±ç!ñ\�^†¶ð(Ël6Íñ—þ­nSã2 î¸éîrgZ«a@æ0ÊâÓÛÛ…‰ŸAò§y»H3¥žª ÇÃඛšÏ¹lñ�«=ñcHŽëÙ(Zy« �VO^™h<Œâ}¯»›ª¯Õm\6¹ê{0�»Å娮QƒZC0dÜ­$ï¸ùƒÌÉBö—�Q“Õ�*Îú*½"ÔQ60†…à)ů¿‡Á¹|öåãƒ~è#¸ÓÆq×}‹9Pô¸@Ÿ<-ßp]¾qó‹[�¶H%}�‹E“¯bW(ï{„Ãav]óŸæ67¦ŒõW3>QiòhblˆîSßÖÕô_Bð£ßȵLŽË€”œT.½·|hþ»']6€L�)Dkb)MÑUºÄ« ¬ ËV¬-œêù˜@�n}Ï›Ý|_\Lr³Ø¹Øw$�Áˆ„+,ÕÕ¾O�O62ëð"´@�E„9 ~MnáX’`iÎsÝï((›©ËY†Y‹¯iGfí‹§.ÑZX*O•­æFÂ8�ŽK ?W!É)Û‚Ž‡[  £Àí⹩t ¢jEN˜]5KÍDzSdŽ 0 %�ñÄ«dZ¡šù_À:‹Ù€¸ «Æ¡Ìb�IÛù|�¹�5'×[¸æ=Ó04ì(ú2×%0éûé€p5ÿ®Y3̪Q0f »é�Ÿ¹x,k—褋J$.=Ç^£�6 @ Ó nžßªˆ¦X<Û¬,ÙyÕ Í¯P%“—) w²ìf^W¤(*‹íð÷AT…¼£P„n„†ër«ÄºqH€Ôc€]1¨]÷bx½X(LHÜZÅšçjÌÔPqX§ R”¬¨êçZ1'7Ü+~Ö3½ÙKAºú¿£7CKû©<¹ øù ¸ðט<טqu¹"4 ä¤Hfí³]½É¿ðç\t ëÿƒ•ïay¿x¬½;7™Ýù{fÌY|›�’˜“€Ì„Bz¿îïÌßî XÔbÄKèóýÛ(bŸ ×�Òªì÷+Úî…•wMN�šçºÎW�ûqèùPYŸææ4H`†©4UàuZâÂ_áñ{ê¿§·¨ñÓfÐlŠ9n9>ÈBHSÞß\«ÞSSÒ’7Àè› õhé&–¼hbåÓSÁðú=11—ÚÄš#8Ìõõ/..¸@¢Pí﫹È[ZÐõ.8û¨¯C¿Q5 cRƒh‰Ba˜º¹3~WÝ7±æŽBÁ¨´¦1âI„.+]Çgj#RƒVU`›w—vÛ¦-¹ì2šµëJýâGNníU+�fð~W>û/¾R5KÆšg8Š�¥í3 ’Ó=¸®DyUŽ™”Ï8Vã”ò­1M¡¿ê,íq�VmZ¯¹L*¥½<™&9©Aü4ƒŸvÀŠB2¾j�X5G1 m®�ëG9.±€EëÌ—M$zpÝ,¦ô6=kÁ>Yã!« EØ"[!Ø®=<>6äýD�:ÝÃè¤L«*ñU�ĨF™[¡ Y36Ý~k8†™ÊÐn–‰±1´3—D·€0©©Å-ý ÊŽ–v©LAx¹ö°í8Œ¾¨1½H?ù<Ú2(¥‚,e �bµB9›¶0”Â�à¬î¥û{Ïù›Y�¯ÊÝ~N^e;TÚcÓݾÁ¡¾ž\ÇÛM¦8õòiT¾r¨ e± Ñ ^3~WBË­}�â„®ô{Ï=K:�Ò ]°š_Æäª«•žÝ½’Jgø+ঋ‡2öœRÛÎrê¨~mŽþHY¶(dlO>ùµH¡”° yy�|åÛôflF½Í¬8'�å©yÍÐvD®ä⺅ó­-¼V‡Ê>½�§êâ!sPûŽG ›>­ ïf¿ócöW€bdcæ¹ �B·|Д�çu}£ˆs}hdôœy‚���üí ›¡íMÚ9 J ®,ÕÿQx²°g§NÁÈ¿jßýìéôž}B{ùÐÏ?ü4‰+Å™F1ò ³N*–Ì ÃQʨ?³HÍPйx,™bø†Ýì5 ¯†CÅð@/K7 óT-uAxˆfpS ÒÔN’0ñMÈ6@t˜�SÒ0þ,ŒüàݾÑ}ö$G_úº4¥È~íûüó/Ž‘,°–(¤6‘E¨OYÍ©Z€TŠªœsºyý¨ÎV6xŠ•L0Ø?H{×BÁ`:ØÌµ„×Cpˆ&�YP©ùåCXXjR ;N œQHŸ…Ø 0ò5H¿ì)OJ)N?ÌуûÊÛ™>ÿ_äå2¾0xŸ‹–¨¢ïUhôÂ*ó¸Yγ6­$ø×÷ñŸÂ,Ó®ËÙuõ›èìè(-7ÜØ£`�}d¼ú@XË °m¹µÆ ³¤bÎf2zé{ý'IL¦9ó‘OòÙ#çÈp©ë‡×æä9^èyÅWµ¨AJqª ö!Jtòg�7C4†éÙ±ž�Bxç�”qú»_FÑNK[+†ÏÊIZÌj‚ÐRˆl‚ÐF¬³D4WÙ)‡’ÖQ°–Bp-„6Ctg®Ëzx-Úsæ”(Þ½TJÑßsž_<ÿ(±1ÿõV—Ø¿>Æ—žü9qMMî5õO)çsUëĬfÍ_ékþÇÝìxÅ.nˬhlÞÅ–�×ÑÞÞ^áæ7gŽÉ,H;ŒšÚ›[ä(…•NÀ æþV´H£#Cœ8²�±‘_ä÷n$éÇŸã‹ýΔ©1ªy®ª5@¨Á/ô†ÀüĽ\³s#7åÏ ƒÎe¯dÃæ47·°’R’‘¡Μx‘‘áÉ”+I=ù<ÿü—ÿÄÉ"„µ\a/åššsÒ§†Ž¥‡|E•6ψÇJßÞ-d—´³ÎE2~Žî³ûw#„#Äœ�6“IÒ{þ4<ÉÙSO“Jõæ›ØŒ=ú ÿøà¿pÿÉáÊÙÜ \[Q³ˆyÒ"Õë’PTÇGC`üéÝì¸f;oôÿz§pd ËWífɲå466Ï?,>�·³†‡éï9Áðà>¤O·u¯4‘àÄ?}“¯>ü4ãš[–©Q¼´…,QÛÔ4 b„¾\0<»Çßóv–¿éFÞÒ^ì'GÖÒµlí�KhiiŲóZLRIñ£ÃC œaläE¤´K¸öÉó<ñ§Ÿç™žì< T’R6Uë€@e;&ÂwìÈ+vùÀ;xíÊ%ì¡Äž†¦¥m;m+iim§±©‰`(\Õ‰Žc“ˆÇ™cl´ŸÑ¡#d³ÃåÈ‹�süÑgøÖ¿Á€²°T ,Y�Úž…¤”ÁSFphǵãÞ_aõ«¯âæÆËËÏe“px%M­«ilj#m"� …ƒ˜† ÂÐy¼°Wº.¶�%“É�N¥H&$âcÄÇ{H$Πd¶"—LÓ»ÿßû«âp<ã)Äù ‘%@ k ŽjBµ@©;5ÏÖ’VÌûÞÍŽ]¹1¢³Ù/DÓj$hÆ 4b˜AŒÉ6 ¥®›Áq8v lj¡¤]-YP‰çžäé¿ýWö�^´û¥æo¡¿K…F©}Xˆ€P‚ÐÏ5 ygg\Ù�õ�w²uÇ®kвŠE´HŒRØÃܘç>óœšHù΄X,…BS 0r®5Ç\úÙGÊ…¡˜9²Êžß7Äxïí,¿n'W.ig§eÒ°@¹�ñçºûÙÿÍqà»ÏWª¨É¦Ýá©4s6?Ö\Ö„‚ÊÌUÅÎï¹ ÏÖÕ~åVÖo^Ãö¶f6‡4«Ö,J‘�HrºˆCOïãà×ÀhÆ)ºMÂ-œjA£æJhç:ej†ªLn](Ì» ÀXÒŠñö›YzÅ&6uµ±±1ÊjË$¬Ô¼ã$ÓôŽÅ8y¦—ã�>ÃÉŸ"-嬾K¥6à¹Ð2¥þ®…¦¥ª?ר ÿP (t`è† @�Ø´ëM7²lËÖ´·°²1ÊÊP�NÓ PihDNÊÝŒÍh2E÷xœîóœýÁóœûÉK$3ά®à2Ïïù‚%ó²vÈ|›"�Ðç�s· PèF¶éþúv‘ lYMà†Ý´oXEWk ÚÂAZM¦A£e!°Ä¥ö¥ŽTØŽKFJ¶ÃD:ËD*ÍèD‚‘sý þìe_:Nz"5m”ûÔÞ­^¿½À(”¹‚Eͧ€ÖBej‡J@‘ל* f÷›ù!JA8MQ1‚œì!çJ”m£Ò62‘B%³ý‰bGèéÀðÒ"¥@R.,:@T-f-%¿å×Ì<”;›FÑæTHÐü[—ïBÓ>8sßÌ5«ý`‘>ÿžù»R¥TXjr¶Z�ºˆZ¥(L�¹TªÆ0ÊÐ~€³šŽBµ‰,P£¨ AR,Sµ…ªEA¬õ$ŠÂ(ŒRÌ)C„åh�| \ì™R&,Å‚¢(¬«ºËZ z¡µE#ª†Q!Í! ,U ¥j’J€¢ �f)Žb"S…Ba¡9ü̪ÉhíÔõ¢/îó3·òiEåÌ®BAñÛ¿ …l1$¿‰*F>íA!‘« ù …8éPÜ:•egÌ«R*0¢ÈU©Ã)Á1/ÖÄò¤ÚšDÌ¢ârD÷�¥ø•ÒÕ¤ÚšÄ #—+ …hšr'�ÈÆ\”ÔˆXÈÌ…—­€Ô“¿Æ©”æ(&Ä[ @òÃå B�êå›~çå+ •šB"Z~N~=ÕY”y^òzª§zª§zª§zª§zª§zª§Ešþ/×±»çzDIóIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9320602 shtab-1.12.1/pyproject.toml0000644000175100017510000000752415245646712015303 0ustar00runnerrunner[build-system] requires = ["setuptools>=77", "setuptools_scm[toml]>=3.4"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] [tool.setuptools.packages.find] include = ["shtab", "shtab.*"] [project.urls] documentation = "https://tqdm.github.io/shtab" repository = "https://github.com/tqdm/shtab" changelog = "https://github.com/tqdm/shtab/releases" [project] name = "shtab" dynamic = ["version"] authors = [{name = "Casper da Costa-Luis", email = "casper.dcl@physics.org"}] maintainers = [{name = "tqdm developers", email = "devs@tqdm.ml"}] description = "Automagic shell tab completion for Python CLI applications" readme = "README.rst" requires-python = ">=3.9" keywords = ["tab", "completion", "argparse", "shell", "bash", "zsh", "tcsh", "fish", "powershell", "pwsh"] license = {text = "MPL-2.0"} # Trove classifiers (https://pypi.org/pypi?%3Aaction=list_classifiers) classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: MacOS X", "Environment :: Other Environment", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: End Users/Desktop", "Intended Audience :: Other Audience", "Intended Audience :: System Administrators", "Operating System :: MacOS", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", "Operating System :: POSIX :: BSD", "Operating System :: POSIX :: BSD :: FreeBSD", "Operating System :: POSIX :: Linux", "Operating System :: POSIX :: SunOS/Solaris", "Operating System :: Unix", "Programming Language :: Other Scripting Engines", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Unix Shell", "Topic :: Desktop Environment", "Topic :: Education :: Computer Aided Instruction (CAI)", "Topic :: Education :: Testing", "Topic :: Office/Business", "Topic :: Other/Nonlisted Topic", "Topic :: Software Development", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Pre-processors", "Topic :: Software Development :: User Interfaces", "Topic :: System", "Topic :: System :: Installation/Setup", "Topic :: System :: Shells", "Topic :: System :: System Shells", "Topic :: Terminals", "Topic :: Utilities"] [project.optional-dependencies] dev = ["pytest>=6", "pytest-cov", "click"] [project.scripts] shtab = "shtab.main:main" [tool.bandit] exclude_dirs = ["tests"] skips = ["B604"] [tool.flake8] max_line_length = 99 extend_ignore = ["E261", "N400", "P101"] exclude = [".git", "__pycache__", "build", "dist", ".eggs"] [tool.yapf] spaces_before_comment = [15, 20] arithmetic_precedence_indication = true allow_split_before_dict_value = false coalesce_brackets = true column_limit = 99 each_dict_entry_on_separate_line = false space_between_ending_comma_and_closing_bracket = false split_before_named_assigns = false split_before_closing_bracket = false blank_line_before_nested_class_or_def = false [tool.isort] line_length = 99 known_first_party = ["shtab", "tests", "examples"] [tool.mypy] warn_unused_configs = true warn_unused_ignores = true show_error_codes = true no_namespace_packages = true [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-v --tb=short -rxs -W=error --durations=5 --cov=shtab --cov-fail-under=85" [tool.coverage.run] branch = true [tool.coverage.report] show_missing = true ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1788300752.242081 shtab-1.12.1/setup.cfg0000644000175100017510000000004615245646720014177 0ustar00runnerrunner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2392857 shtab-1.12.1/shtab/0000755000175100017510000000000015245646720013457 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9320602 shtab-1.12.1/shtab/__init__.py0000644000175100017510000016757515245646712015616 0ustar00runnerrunnerimport hashlib import logging import re from argparse import (ONE_OR_MORE, REMAINDER, SUPPRESS, ZERO_OR_MORE, Action, ArgumentParser, _ActionsContainer, _AppendAction, _AppendConstAction, _CountAction, _HelpAction, _StoreConstAction, _VersionAction) from collections import defaultdict from contextlib import contextmanager from functools import total_ordering from importlib.metadata import PackageNotFoundError, version from itertools import starmap from shlex import join, quote from string import Template from textwrap import dedent from typing import Any, Callable from typing import Optional as Opt from typing import TypeVar, Union try: __version__ = version('shtab') except PackageNotFoundError: __version__ = "UNKNOWN" __all__ = [ "complete", "add_argument_to", "glob", "cmd", "SUPPORTED_SHELLS", "FILE", "DIRECTORY", "DIR"] log = logging.getLogger(__name__) ShellType = str CompleteType = dict[ShellType, Union[str, dict[ShellType, str]]] _ActionsContainerT = TypeVar('_ActionsContainerT', bound=_ActionsContainer) SUPPORTED_SHELLS: list[ShellType] = [] _SUPPORTED_COMPLETERS: dict[ShellType, Callable] = {} CHOICE_FUNCTIONS: dict[str, CompleteType] = { "file": { "bash": "_shtab_compgen_files", "zsh": "_files", "tcsh": "f", "fish": "(__fish_complete_path (commandline -ct))", "powershell": "_shtab_powershell_compgen_files", }, "directory": { "bash": "_shtab_compgen_dirs", "zsh": "_files -/", "tcsh": "d", "fish": "(__fish_complete_directories)", "powershell": "_shtab_powershell_compgen_dirs", }} # yapf: disable FILE = CHOICE_FUNCTIONS["file"] DIRECTORY = DIR = CHOICE_FUNCTIONS["directory"] FLAG_OPTION = ( _StoreConstAction, _HelpAction, _VersionAction, _AppendConstAction, _CountAction, ) def sha(obj): return hashlib.sha256(repr(obj).encode()).hexdigest()[:8] def glob(*patterns: str) -> CompleteType: """ Example: `glob("*.yml", "*.yaml")` Consider native shell alternatives in special cases: - any file: `shtab.FILE` (instead of `glob("*")`) - any directory: `shtab.DIRECTORY` (instead of `glob("*/")`) """ return { "bash": f"_shtab_glob_compgen_{sha(patterns)}", "zsh": f"_files -g '({'|'.join(patterns)})'", "tcsh": f"f:{{{','.join(patterns)}}}", "fish": f"(_shtab_glob_compgen_{sha(patterns)})", "powershell": f"_shtab_glob_compgen_{sha(patterns)}", "preamble": { "bash": dedent(f""" # $1=COMP_WORDS[1] _shtab_glob_compgen_{sha(patterns)}() {{ for ext in {join(patterns)}; do compgen -f -X "!$ext" -- $1 done compgen -d -- $1 # recurse into subdirs }} """), "fish": dedent(f""" function _shtab_glob_compgen_{sha(patterns)} set comp (commandline -ct) for pattern in {join(patterns)} __fish_complete_path "$comp" | string match -e -- "$pattern" end __fish_complete_path "$comp" | string match -e "*/" # recurse into subdirs end """), "powershell": dedent(rf""" function _shtab_glob_compgen_{sha(patterns)} {{ param([string]$WordToComplete) $dir = '' if ($WordToComplete -match '^(.*[\\/])') {{ $dir = $Matches[1] }} Get-ChildItem -Path "$WordToComplete*" ` -Include {_powershell_list(patterns)} -File -ErrorAction SilentlyContinue | ForEach-Object {{ $dir + $_.Name }} Get-ChildItem -Path "$WordToComplete*" -Directory -ErrorAction SilentlyContinue | ForEach-Object {{ $dir + $_.Name + [System.IO.Path]::DirectorySeparatorChar }} }}"""), }} # yapf: disable def cmd(command: str) -> CompleteType: """ command: shell command to run to generate completions Example: `cmd("git branch")` """ return { "bash": f"_shtab_cmd_compgen_{sha(command)}", "zsh": f"($({command}))", "tcsh": f"`{command}`", "fish": f"({command})", "powershell": f"_shtab_cmd_compgen_{sha(command)}", "preamble": { "bash": dedent(f""" # $1=COMP_WORDS[1] _shtab_cmd_compgen_{sha(command)}() {{ compgen -W "$({command})" -- $1 }}"""), "powershell": dedent(f""" function _shtab_cmd_compgen_{sha(command)} {{ param([string]$WordToComplete) $items = @(Invoke-Expression {_powershell_escape(command)} 2>$null) $items | ForEach-Object {{ $_.ToString() }} | Where-Object {{ $_ -like "$WordToComplete*" }} }}"""), }} # yapf: disable class _ShtabPrintCompletionAction(Action): pass OPTION_END = _HelpAction, _VersionAction, _ShtabPrintCompletionAction OPTION_MULTI = _AppendAction, _AppendConstAction, _CountAction def mark_completer(shell): def wrapper(func): if shell not in SUPPORTED_SHELLS: SUPPORTED_SHELLS.append(shell) _SUPPORTED_COMPLETERS[shell] = func return func return wrapper def get_completer(shell): try: return _SUPPORTED_COMPLETERS[shell] except KeyError: supported = ",".join(SUPPORTED_SHELLS) raise NotImplementedError(f"shell ({shell}) must be in {supported}") @total_ordering class Choice: """ WARNING: deprecated. Use `.complete = ...` instead. Placeholder to mark a special completion ``. >>> ArgumentParser.add_argument(..., choices=[Choice("")]) """ def __init__(self, choice_type: str, required: bool = False) -> None: """ choice_type : internal `type` name required : controls result of comparison to empty strings """ self.required = required self.type = choice_type def __repr__(self) -> str: return self.type + ("" if self.required else "?") def __cmp__(self, other: object) -> int: if self.required: return 0 if other else -1 return 0 def __eq__(self, other: object) -> bool: return self.__cmp__(other) == 0 def __lt__(self, other: object) -> bool: return self.__cmp__(other) < 0 class Optional: """ WARNING: deprecated. Use `.complete = ...` instead. Example: `ArgumentParser.add_argument(..., choices=Optional.FILE)`. """ FILE = [Choice("file")] DIR = DIRECTORY = [Choice("directory")] class Required: """ WARNING: deprecated. Use `.complete = ...` instead. Example: `ArgumentParser.add_argument(..., choices=Required.FILE)`. """ FILE = [Choice("file", True)] DIR = DIRECTORY = [Choice("directory", True)] def complete2pattern(opt_complete: CompleteType, shell: str, choice_type2fn: dict[str, str], preambles: list[str]) -> str: if isinstance(opt_complete, dict): if preamble := opt_complete.get("preamble", {}).get(shell, ""): # type: ignore[union-attr] preambles.append(preamble) if isinstance(opt_complete, dict): return opt_complete.get(shell, "") # type: ignore[return-value] return choice_type2fn[opt_complete] def wordify(string: str): """Replace non-word chars [\\W] with underscores [_]""" return re.sub("\\W", "_", string) def get_public_subcommands(sub) -> dict[str, str]: """returns {'subcommand': "help text", ...} for a given subparser""" public_parsers = {id(sub.choices[i.dest]): i.help for i in sub._get_subactions()} # check SUPPRESS to be forward-compatible with python/cpython#67037 return { k: h for k, v in sub.choices.items() if (h := public_parsers.get(id(v), SUPPRESS)) != SUPPRESS} def is_subparser(positional): return isinstance(positional.choices, dict) and positional._get_subactions() and all( isinstance(v, ArgumentParser) for v in positional.choices.values()) def get_bash_commands(root_parser, root_prefix, choice_functions=None): """ Recursive subcommand parser traversal, returning lists of information on commands (formatted for output to the completions script). printing bash helper syntax. Returns: subparsers : list of subparsers for each parser option_strings : list of options strings for each parser compgens : list of shtab `.complete` functions corresponding to actions choices : list of choices corresponding to actions nargs : list of number of args allowed for each action (if not 0 or 1) preambles : list of preamble functions """ choice_type2fn = {k: v["bash"] for k, v in CHOICE_FUNCTIONS.items()} if choice_functions: choice_type2fn.update(choice_functions) subparsers = [] option_strings = [] compgens = [] choices = [] nargs = [] preambles = [] def recurse(parser, prefix): """Recurse through subparsers, appending to the return lists""" # positional arguments discovered_subparsers = [] for i, positional in enumerate(parser._get_positional_actions()): if positional.help == SUPPRESS: continue if hasattr(positional, 'complete'): # shtab `.complete = ...` functions comp_pattern = complete2pattern(positional.complete, 'bash', choice_type2fn, preambles) compgens.append(f"{prefix}_pos_{i}_COMPGEN={quote(comp_pattern)}") elif positional.choices: # choices (including subparsers & shtab `.complete` functions) log.debug(f"choices:{prefix}:{sorted(positional.choices)}") if is_subparser(positional): public_cmds = get_public_subcommands(positional) this_positional_choices = [] for choice in positional.choices: if isinstance(choice, Choice): # append special completion type to `compgens` # NOTE: overrides `.complete` attribute log.debug(f"Choice.{choice.type}:{prefix}:{positional.dest}") compgens.append(f"{prefix}_pos_{i}_COMPGEN=" f"{quote(choice_type2fn[choice.type])}") elif is_subparser(positional): # subparser, so append to list of subparsers & recurse log.debug("subcommand:%s", choice) if choice in public_cmds: discovered_subparsers.append(str(choice)) this_positional_choices.append(str(choice)) recurse(positional.choices[choice], f"{prefix}_{wordify(choice)}") else: log.debug("skip:subcommand:%s", choice) else: # simple choice this_positional_choices.append(str(choice)) if this_positional_choices: choices.append(f"{prefix}_pos_{i}_choices=({join(this_positional_choices)})") # skip default `nargs` values if positional.nargs not in (None, "1", "?"): nargs.append(f"{prefix}_pos_{i}_nargs={quote(str(positional.nargs))}") if discovered_subparsers: subparsers.append(f"{prefix}_subparsers=({join(discovered_subparsers)})") log.debug(f"subcommands:{prefix}:{discovered_subparsers}") # optional arguments option_strings_list = join( sum((opt.option_strings for opt in parser._get_optional_actions() if opt.help != SUPPRESS), [])) option_strings.append(f"{prefix}_option_strings=({option_strings_list})") for optional in parser._get_optional_actions(): if optional.help == SUPPRESS: continue for option_string in optional.option_strings: if hasattr(optional, 'complete'): # shtab `.complete = ...` functions comp_pattern_str = complete2pattern(optional.complete, 'bash', choice_type2fn, preambles) compgens.append( f"{prefix}_{wordify(option_string)}_COMPGEN={quote(comp_pattern_str)}") elif optional.choices: # choices (including shtab `.complete` functions) this_optional_choices = [] for choice in optional.choices: # append special completion type to `compgens` # NOTE: overrides `.complete` attribute if isinstance(choice, Choice): log.debug(f"Choice.{choice.type}:{prefix}:{optional.dest}") func_str = choice_type2fn[choice.type] compgens.append(f"{prefix}_{wordify(option_string)}_COMPGEN=" f"{quote(func_str)}") else: # simple choice this_optional_choices.append(str(choice)) if this_optional_choices: choices.append(f"{prefix}_{wordify(option_string)}_choices=" f"({join(this_optional_choices)})") # Check for nargs. if optional.nargs is not None and optional.nargs != 1: nargs.append(f"{prefix}_{wordify(option_string)}_nargs=" f"{quote(str(optional.nargs))}") return subparsers, option_strings, compgens, choices, nargs, preambles return recurse(root_parser, root_prefix) @mark_completer("bash") def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None): """ Returns bash syntax autocompletion script. See `complete` for arguments. """ root_prefix = wordify(f"_shtab_{root_prefix or parser.prog}") subparsers, option_strings, compgens, choices, nargs, extra_preambles = get_bash_commands( parser, root_prefix, choice_functions=choice_functions) preamble = "\n".join(list(dict.fromkeys(([preamble] if preamble else []) + extra_preambles))) # References: # - https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html # - https://opensource.com/article/18/3/creating-bash-completion-script # - https://stackoverflow.com/questions/12933362 return Template("""\ # AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab # Usage: # 1) Copy this to somewhere (e.g. ~/.local/share/bash_completion/${prog}). # 2) Add the following line to your .bashrc: # source ~/.local/share/bash_completion/${prog} # See also: https://github.com/scop/bash-completion/blob/main/doc/configuration.md ${subparsers} ${option_strings} ${compgens} ${choices} ${nargs} ${preamble} # $1=COMP_WORDS[1] _shtab_compgen_files() { compgen -f -- $1 # files } # $1=COMP_WORDS[1] _shtab_compgen_dirs() { compgen -d -- $1 # recurse into subdirs } # $1=COMP_WORDS[1] _shtab_replace_nonword() { echo "${1//[^[:word:]]/_}" } # set default values (called for the initial parser & any subparsers) _set_parser_defaults() { local subparsers_var="${prefix}_subparsers[@]" sub_parsers=${!subparsers_var-} local current_option_strings_var="${prefix}_option_strings[@]" current_option_strings=${!current_option_strings_var} completed_positional_actions=0 _set_new_action "pos_$completed_positional_actions" true } # $1=action identifier # $2=positional action (bool) # set all identifiers for an action's parameters _set_new_action() { current_action="${prefix}_$(_shtab_replace_nonword $1)" local current_action_compgen_var=${current_action}_COMPGEN current_action_compgen="${!current_action_compgen_var-}" local current_action_choices_var="${current_action}_choices[@]" current_action_choices="${!current_action_choices_var-}" local current_action_nargs_var="${current_action}_nargs" if [ -n "${!current_action_nargs_var-}" ]; then current_action_nargs="${!current_action_nargs_var}" else current_action_nargs=1 fi current_action_args_start_index=$(( $word_index + 1 - $pos_only )) current_action_is_positional=$2 } # Notes: # `COMPREPLY`: what will be rendered after completion is triggered # `completing_word`: currently typed word to generate completions for # `${!var}`: evaluates the content of `var` and expand its content as a variable # hello="world" # x="hello" # ${!x} -> ${hello} -> "world" ${root_prefix}() { local completing_word="${COMP_WORDS[COMP_CWORD]}" local previous_word="${COMP_WORDS[COMP_CWORD-1]}" local completed_positional_actions local current_action local current_action_args_start_index local current_action_choices local current_action_compgen local current_action_is_positional local current_action_nargs local current_option_strings local sub_parsers COMPREPLY=() local prefix=${root_prefix} local word_index=0 local pos_only=0 # "--" delimiter not encountered yet _set_parser_defaults word_index=1 # determine what arguments are appropriate for the current state # of the arg parser while [ $word_index -ne $COMP_CWORD ]; do local this_word="${COMP_WORDS[$word_index]}" if [[ $pos_only = 1 || " $this_word " != " -- " ]]; then if [[ -n $sub_parsers && " ${sub_parsers[@]} " == *" $this_word "* ]]; then # valid subcommand: add it to the prefix & reset the current action prefix="${prefix}_$(_shtab_replace_nonword $this_word)" _set_parser_defaults fi if [[ " ${current_option_strings[@]} " == *" $this_word "* ]]; then # a new action should be acquired (due to recognised option string or # no more input expected from current action); # the next positional action can fill in here _set_new_action $this_word false fi if [[ "$current_action_nargs" != "*" ]] && \\ [[ "$current_action_nargs" != "+" ]] && \\ [[ "$current_action_nargs" != "?" ]] && \\ [[ "$current_action_nargs" != *"..." ]] && \\ (( $word_index + 1 - $current_action_args_start_index - $pos_only >= \\ $current_action_nargs )); then $current_action_is_positional && let "completed_positional_actions += 1" _set_new_action "pos_$completed_positional_actions" true fi else pos_only=1 # "--" delimiter encountered fi let "word_index+=1" done # Generate the completions COMPREPLY=() if [[ $pos_only = 0 && "$completing_word" == -* && ( -z "$current_action_compgen" || "$current_action_is_positional" = true ) ]]; then # optional argument started: use option strings while IFS= read -r line; do COMPREPLY+=("$line"); done < <( compgen -W "${current_option_strings[*]}" -- "$completing_word") elif [[ "$previous_word" =~ ^[0-9\\&]*[\\<\\>]\\>?$ ]]; then # handle redirection operators compopt -o filenames 2>/dev/null || : # bash>=4 while IFS= read -r line; do COMPREPLY+=("$line"); done < <(compgen -f -- "$completing_word") else # use choices & compgen local action_compgen_word="$completing_word" # handle tab-completing in the middle of a line (#248 <- #116) [[ -n "$current_action_compgen" && "$completing_word" == -* ]] && action_compgen_word="" [ -n "$current_action_compgen" ] && { [[ "$current_action_compgen" =~ _(file|dir|glob|FILE|DIR|GLOB)|File|Dir|Glob ]] && compopt -o filenames 2>/dev/null || : # bash>=4 while IFS= read -r line; do COMPREPLY+=("$line"); done < <( "$current_action_compgen" "$action_compgen_word") } while IFS= read -r line; do COMPREPLY+=("$line"); done < <( compgen -W "${current_action_choices[*]}" -- "$completing_word") fi return 0 } complete -F ${root_prefix} ${prog}""").safe_substitute( subparsers="\n".join(subparsers), option_strings="\n".join(option_strings), compgens="\n".join(compgens), choices="\n".join(choices), nargs="\n".join(nargs), preamble=f"\n# Custom Preamble\n{preamble}\n# End Custom Preamble\n" if preamble else "", root_prefix=root_prefix, prog=parser.prog, ) def head(string): return str(string).strip().split("\n")[0] if string else "" @contextmanager def get_formatter(parser): formatter = parser._get_formatter() backup_width = formatter._width try: formatter._width = 999 # large number to effectively disable wrapping def inner(str_or_parser): return head( formatter._format_text(str_or_parser if isinstance(str_or_parser, str) else formatter._expand_help(str_or_parser))) yield inner finally: formatter._width = backup_width def escape_zsh(string): """ Backslash-escape for interpolation into a double-quoted `_arguments` spec. NOTE: cannot use `shlex.quote` (a single-quoted word only valid at top level). """ # excessive but safe return head(re.sub(r"([^\w\s.,()-])", r"\\\1", str(string))) if string else "" @mark_completer("zsh") def complete_zsh(parser, root_prefix=None, preamble="", choice_functions=None): """ Returns zsh syntax autocompletion script. See `complete` for arguments. """ prog = parser.prog preambles = [preamble] if preamble else [] root_prefix = wordify(f"_shtab_{root_prefix or prog}") choice_type2fn = {k: v["zsh"] for k, v in CHOICE_FUNCTIONS.items()} if choice_functions: choice_type2fn.update(choice_functions) def get_candidates(arg): if hasattr(arg, 'complete'): return complete2pattern(arg.complete, 'zsh', choice_type2fn, preambles) if arg.choices: first = next(iter(arg.choices)) if isinstance(first, Choice): return choice_type2fn[first.type] return "({})".format(" ".join(map(str, arg.choices))) def format_optional(opt, get_help): return (('{nargs}{options}"[{help}]"' if (isinstance(opt, FLAG_OPTION) or opt.nargs == 0) else '{nargs}{options}"[{help}]:{dest}:{pattern}"').format( nargs=('"(- : *)"' if (isinstance(opt, OPTION_END) or opt.nargs == REMAINDER) else '"*"' if isinstance(opt, OPTION_MULTI) else ""), options=("{{{}}}".format(",".join(opt.option_strings)) if len( opt.option_strings) > 1 else '"{}"'.format("".join(opt.option_strings))), help=escape_zsh(get_help(opt) if opt.help else opt.metavar or opt.dest), dest=opt.metavar or opt.dest, pattern=get_candidates(opt) or "").replace('""', '')) def format_positional(opt, get_help): return '"{nargs}:{help}:{pattern}"'.format( nargs={ONE_OR_MORE: "(*)", ZERO_OR_MORE: "(*):", REMAINDER: "(-)*:"}.get(opt.nargs, ""), help=escape_zsh(get_help(opt) if opt.help else opt.metavar or opt.dest), pattern=get_candidates(opt) or "") # {cmd: {"help": help, "arguments": [arguments]}} with get_formatter(parser) as get_help: all_commands = { root_prefix: { "cmd": prog, "arguments": [ format_optional(opt, get_help) for opt in parser._get_optional_actions() if opt.help != SUPPRESS] + [ format_positional(opt, get_help) for opt in parser._get_positional_actions() if opt.help != SUPPRESS and not is_subparser(opt)], "help": head(parser.description), "commands": [], "paths": []}} def recurse(parser, prefix, paths=None): paths = paths or [] subcmds = [] for sub in parser._get_positional_actions(): if sub.help == SUPPRESS or not is_subparser(sub): continue log.debug(f"subparser:choices:{prefix}:{sorted(sub.choices)}") public_cmds = get_public_subcommands(sub) for cmd, subparser in sub.choices.items(): if cmd not in public_cmds: log.debug("skip:subcommand:%s", cmd) continue log.debug("subcommand:%s", cmd) with get_formatter(subparser) as get_help: # optionals arguments = [ format_optional(opt, get_help) for opt in subparser._get_optional_actions() if opt.help != SUPPRESS] # positionals arguments.extend( format_positional(opt, get_help) for opt in subparser._get_positional_actions() if opt.help != SUPPRESS and not is_subparser(opt)) # help text desc = get_help(subparser.description or public_cmds[cmd]) new_pref = f"{prefix}_{wordify(cmd)}" options = all_commands[new_pref] = { "cmd": cmd, "help": desc, "arguments": arguments, "paths": [*paths, cmd]} new_subcmds = recurse(subparser, new_pref, [*paths, cmd]) options["commands"] = { all_commands[pref]["cmd"]: all_commands[pref] for pref in new_subcmds if pref in all_commands} subcmds.extend([*new_subcmds, new_pref]) log.debug("subcommands:%s:%s", cmd, options) return subcmds recurse(parser, root_prefix) all_commands[root_prefix]["commands"] = { options["cmd"]: options for prefix, options in sorted(all_commands.items()) if len(options.get("paths", [])) < 2 and prefix != root_prefix} subcommands = { prefix: options for prefix, options in all_commands.items() if options.get("commands")} subcommands.setdefault(root_prefix, all_commands[root_prefix]) log.debug("subcommands:%s:%s", root_prefix, sorted(all_commands)) def command_case(prefix, options): name = options["cmd"] commands = options["commands"] case_fmt_on_no_sub = """{name}) _arguments -C -s ${prefix}_{name_wordify}_options ;;""" case_fmt_on_sub = """{name}) {prefix}_{name_wordify} ;;""" cases = [] for _, options in sorted(commands.items()): fmt = case_fmt_on_sub if options.get("commands") else case_fmt_on_no_sub cases.append( fmt.format(name=options["cmd"], name_wordify=wordify(options["cmd"]), prefix=prefix)) cases = "\n\t".expandtabs(8).join(cases) return Template("""\ ${prefix}() { local context state line \ curcontext="$curcontext" one_or_more='(*)' remainder='(-)*:' default='*::: :->${name}' # Add default positional/remainder specs only if none exist, and only once per session if (( ! ${prefix}_defaults_added )); then if (( ${${prefix}_options[(I)${(q)one_or_more}*]} +\ ${${prefix}_options[(I)${(q)remainder}*]} +\ ${${prefix}_options[(I)${(q)default}]} == 0 )); then ${prefix}_options+=(': :${prefix}_commands' '*::: :->${name}') fi ${prefix}_defaults_added=1 fi _arguments -C -s $$${prefix}_options case $state in ${name}) words=($line[1] "${words[@]}") (( CURRENT += 1 )) curcontext="${curcontext%:*:*}:${prefix}-$line[1]:" case $line[1] in ${cases} esac esac } """).safe_substitute(name=name, prefix=prefix, cases=cases) def command_option(prefix, options): arguments = "\n ".join(options["arguments"]) return f"""\ {prefix}_options=( {arguments} ) # guard to ensure default positional specs are added only once per session {prefix}_defaults_added=0 """ def command_list(prefix, options): name = " ".join([prog, *options["paths"]]) commands = "\n ".join(f'{quote(cmd)}:{quote(opt["help"])}' for cmd, opt in sorted(options["commands"].items())) return f""" {prefix}_commands() {{ local _commands=( {commands} ) _describe '{name} commands' _commands }}""" preamble = "\n".join(list(dict.fromkeys(preambles))) # References: # - https://github.com/zsh-users/zsh-completions # - http://zsh.sourceforge.net/Doc/Release/Completion-System.html # - https://mads-hartmann.com/2017/08/06/writing-zsh-completion-scripts.html # - http://www.linux-mag.com/id/1106/ return Template("""\ #compdef ${prog} # AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab # Usage: # 1) Copy this to a file named _${prog} (e.g. ~/.local/share/zsh_completion/_${prog}). # 2) Add the following line to your .zshrc: # fpath=(~/.local/share/zsh_completion $fpath) # See also: https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org ${command_commands} ${command_options} ${command_cases} ${preamble} typeset -A opt_args if [[ $zsh_eval_context[-1] == eval ]]; then # eval/source/. command, register function for later compdef ${root_prefix} -N ${prog} else # autoload from fpath, call function directly ${root_prefix} "$@\" fi """).safe_substitute( prog=prog, root_prefix=root_prefix, command_cases="\n".join(starmap(command_case, sorted(subcommands.items()))), command_commands="\n".join(starmap(command_list, sorted(subcommands.items()))), command_options="\n".join(starmap(command_option, sorted(all_commands.items()))), preamble=f"""# Custom Preamble\n{preamble}\n# End Custom Preamble\n""" if preamble else "", ) @mark_completer("tcsh") def complete_tcsh(parser, root_prefix=None, preamble="", choice_functions=None): """ Return tcsh syntax autocompletion script. root_prefix: ignored (tcsh has no support for functions) See `complete` for other arguments. """ optionals_single = set() optionals_double = set() specials = [] # `--opt=` rules, emitted before the generic `c/--/` one which would shadow them eq_specials = [] index_choices = defaultdict(dict) preambles = [preamble] if preamble else [] choice_type2fn = {k: v["tcsh"] for k, v in CHOICE_FUNCTIONS.items()} if choice_functions: choice_type2fn.update(choice_functions) def get_specials(arg, arg_type, arg_sel, check_subparser=False): if hasattr(arg, 'complete'): complete_fn = complete2pattern(arg.complete, 'tcsh', choice_type2fn, preambles) if complete_fn: yield f"'{arg_type}/{arg_sel}/{complete_fn}/'" elif arg.choices: if check_subparser and is_subparser(arg): choice_strs = ' '.join(get_public_subcommands(arg)) else: choice_strs = ' '.join(map(str, arg.choices)) yield f"'{arg_type}/{arg_sel}/({choice_strs})/'" def recurse_parser(cparser, positional_idx, requirements=None): log_prefix = "| " * positional_idx log.debug("%sParser @ %d", log_prefix, positional_idx) if requirements: log.debug("%s- Requires: %s", log_prefix, " ".join(requirements)) else: requirements = [] for optional in cparser._get_optional_actions(): if optional.help == SUPPRESS: continue log.debug("%s| Optional: %s", log_prefix, optional.dest) # Mingle all optional arguments for all subparsers for optional_str in optional.option_strings: log.debug("%s| | %s", log_prefix, optional_str) if optional_str.startswith('--'): optionals_double.add(optional_str[2:]) elif optional_str.startswith('-'): optionals_single.add(optional_str[1:]) specials.extend(get_specials(optional, 'n', optional_str)) if optional.nargs != 0: eq_specials.extend(get_specials(optional, 'c', optional_str + '=')) for positional in cparser._get_positional_actions(): positional_idx += 1 if positional.help == SUPPRESS: continue log.debug("%s| Positional #%d: %s", log_prefix, positional_idx, positional.dest) index_choices[positional_idx][tuple(requirements)] = positional if is_subparser(positional): public_cmds = get_public_subcommands(positional) for subcmd, subparser in positional.choices.items(): if subcmd in public_cmds: log.debug("%s| | SubParser: %s", log_prefix, subcmd) recurse_parser(subparser, positional_idx, requirements + [subcmd]) else: log.debug("%s| | SubParser skip: %s", log_prefix, subcmd) recurse_parser(parser, 0) for idx, ndict in index_choices.items(): if len(ndict) == 1: # Single choice, no requirements arg = next(iter(ndict.values())) specials.extend(get_specials(arg, 'p', str(idx), True)) else: # Multiple requirements nlist = [] for nn, arg in ndict.items(): if nn and idx == len(nn) + 1: # lookup preceding (sub)command name for completions specials.extend(get_specials(arg, 'n', nn[-1])) continue max_idx = len(nn) + 1 checks = [f'("$cmd[{iidx}]" == "{n}")' for iidx, n in enumerate(nn, start=2)] condition = f"$#cmd >= {max_idx} && " + " && ".join(checks) if hasattr(arg, 'complete'): complete_fn = complete2pattern(arg.complete, 'tcsh', choice_type2fn, preambles) if complete_fn: if complete_fn.startswith('`') and complete_fn.endswith('`'): # nested backticks crash tcsh's parser, use `eval` instead nlist.append(f"if ( {condition} ) eval {complete_fn.strip('`')}") else: log.debug("warning: tcsh cannot express completion patterns" " (`f:*.txt`, `d`, ...) as commands") elif arg.choices: nlist.append(f"if ( {condition} ) echo {join(map(str, arg.choices))}") if nlist: nlist_str = '; '.join(nlist) # pad $cmd so indexing it never runs out of range. # $COMMAND_LINE must stay unquoted to allow csh word splitting. padding = ' '.join(['""'] * 9) specials.append( f"'p@{str(idx)}@`set cmd=($COMMAND_LINE {padding}); {nlist_str}`@'") if optionals_double: if optionals_single: optionals_single.add('-') else: # Don't add a space after completing "--" from "-" optionals_single = ('-', '-') specials = list(dict.fromkeys(specials)) eq_specials = list(dict.fromkeys(eq_specials)) preamble = "\n".join(list(dict.fromkeys(preambles))) return Template("""\ # AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab # Usage: # 1) Copy this to somewhere (e.g. ~/.local/share/tcsh_completion/${prog}). # 2) Add the following line to your .cshrc or .tcshrc: # source ~/.local/share/tcsh_completion/${prog} # See also: https://github.com/tcsh-org/tcsh/blob/master/complete.tcsh ${preamble} complete ${prog} \\ ${optionals_eq}'c/--/(${optionals_double})/' \\ 'c/-/(${optionals_single})/' \\ ${optionals_special} \\ 'p/*/()/'""").safe_substitute( preamble=f"\n# Custom Preamble\n{preamble}\n# End Custom Preamble\n" if preamble else "", prog=parser.prog, optionals_double=' '.join(sorted(optionals_double)), optionals_single=' '.join(sorted(optionals_single)), optionals_eq=''.join(f'{eq} \\\n ' for eq in eq_specials), optionals_special=' \\\n '.join(specials)) @mark_completer("fish") def complete_fish(parser, root_prefix=None, preamble="", choice_functions=None): """ Return fish syntax autocompletion script. See `complete` for arguments. """ prog = parser.prog prefix = wordify(f"_shtab_{root_prefix or prog}") completions = [] commands = [] # all (sub)command paths, e.g. ["sub", "sub subsub"] opts_with_value = set() # option strings which consume a following value token preambles = [preamble] if preamble else [] choice_type2fn = {k: v["fish"] for k, v in CHOICE_FUNCTIONS.items()} if choice_functions: choice_type2fn.update(choice_functions) def get_candidates(arg): if hasattr(arg, 'complete'): return complete2pattern(arg.complete, 'fish', choice_type2fn, preambles) if arg.choices: return join(map(str, arg.choices)) def pos_condition(index, width, open_ended): """Condition suffix restricting a completion to the given positional slot(s).""" npos = f"${prefix}_npos" if open_ended or width is None: return f"; and test {npos} -ge {index}" if width == 1: return f"; and test {npos} -eq {index}" return f"; and test {npos} -ge {index}; and test {npos} -le {index + width - 1}" def start_output(path, pos_test=""): """`complete` command start, with a condition matching the (sub)command `path`.""" cond = " ".join([f"{prefix}_using"] + [quote(cmd) for cmd in path]) + pos_test return ["complete", "-c", prog, f"-n {quote(cond)}"] def recurse_parser(cparser: ArgumentParser, path: list[str]): """ path: the list of subcommands that led to current """ log_prefix = "| " * len(path) log.debug("%sParser @ %d", log_prefix, len(path)) for optional in cparser._get_optional_actions(): log.debug("%s| Optional: %s", log_prefix, optional.dest) if optional.help == SUPPRESS: continue output = start_output(path) for optional_str in optional.option_strings: log.debug("%s| | %s", log_prefix, optional_str) if optional_str.startswith("--"): output.append(f"-l {optional_str[2:]}") elif optional_str.startswith("-"): output.append(f"-s {optional_str[1:]}") if not (isinstance(optional, FLAG_OPTION) or optional.nargs == 0): opts_with_value.update(optional.option_strings) candidates = get_candidates(optional) output.append(f'-xka "{candidates}"' if candidates else "-x") with get_formatter(cparser) as get_help: if desc := head( get_help(optional) if optional.help else optional.metavar or optional.dest ): output.append(f'-d {quote(desc)}') completions.append(' '.join(output)) index = 0 # the next positional slot (number of preceding positional arguments) open_ended = False # an earlier positional consumes any number of tokens for positional in cparser._get_positional_actions(): if positional.help == SUPPRESS: continue log.debug("%s| Positional #%d: %s", log_prefix, index, positional.dest) if is_subparser(positional): public_cmds = get_public_subcommands(positional) pos_test = pos_condition(index, 1, open_ended) for subcmd, subparser in positional.choices.items(): # type: ignore[union-attr] if subcmd not in public_cmds: continue log.debug("%s| | SubParser: %s", log_prefix, subcmd) commands.append(" ".join(path + [subcmd])) output = start_output(path, pos_test) output.append(f"-a {quote(subcmd)}") with get_formatter(subparser) as get_help: if desc := get_help(subparser.description or public_cmds[subcmd]): output.append(f'-d {quote(desc)}') completions.append(' '.join(output)) recurse_parser(subparser, path + [subcmd]) index += 1 else: # simple argument (file, name...) width = (positional.nargs if isinstance(positional.nargs, int) else 1 if positional.nargs in (None, "?") else None) candidates = get_candidates(positional) if candidates: output = start_output(path, pos_condition(index, width, open_ended)) output.append(f'-ka "{candidates}"') with get_formatter(cparser) as get_help: if desc := head( get_help(positional) if positional. help else positional.metavar or positional.dest): output.append(f'-d {quote(desc)}') completions.append(' '.join(output)) if width is None: open_ended = True else: index += width recurse_parser(parser, []) preamble = "\n".join(list(dict.fromkeys(preambles))) return Template("""\ # AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab # Usage: # 1) Copy this to ~/.config/fish/completions/${prog}.fish # 2) Ensure a binary named ${prog} is in your PATH # See also: https://fishshell.com/docs/current/completions.html#where-to-put-completions ${preamble} # Parse current commandline: # - ${prefix}_cmdpath=(sub)command path seen so far # - ${prefix}_npos=number of positional arguments given after it # - options are skipped based on ${prefix}_opts_with_value & ${prefix}_commands lists function ${prefix}_scan set -g ${prefix}_cmdpath '' set -g ${prefix}_npos 0 set -l tokens (commandline -opc) set -e tokens[1] set -l expect_value 0 for t in $tokens if test $expect_value -eq 1 set expect_value 0 continue end switch "$t" case '--*=*' continue case '-*' if contains -- $t $$${prefix}_opts_with_value set expect_value 1 end continue case '*' if test $$${prefix}_npos -eq 0 set -l candidate $t if test -n "$$${prefix}_cmdpath" set candidate "$$${prefix}_cmdpath $t" end if contains -- $candidate $$${prefix}_commands set -g ${prefix}_cmdpath $candidate continue end end set -g ${prefix}_npos (math $$${prefix}_npos + 1) end end end # Condition helper: true if the current (sub)command path equals the given one. function ${prefix}_using ${prefix}_scan test "$$${prefix}_cmdpath" = "$argv" end set -g ${prefix}_commands ${commands} set -g ${prefix}_opts_with_value ${opts_with_value} complete -c ${prog} -e complete -c ${prog} -f ${completions} """).safe_substitute( preamble=f"# Custom Preamble\n{preamble}\n# End Custom Preamble\n" if preamble else "", prog=parser.prog, prefix=prefix, commands=' '.join(quote(cmd) for cmd in commands), opts_with_value=' '.join(quote(opt) for opt in sorted(opts_with_value)), completions='\n'.join(completions), ) def _powershell_escape(string: str) -> str: """ Similar to `shlex.quote`, see: - https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/ about/about_quoting_rules - https://learn.microsoft.com/en-us/powershell/scripting/lang-spec/ chapter-02#2352-string-literals """ s = str(string) for ch in "'\u2018\u2019\u201a\u201b": s = s.replace(ch, ch * 2) return f"'{s}'" def _powershell_list(items): """Serialize a list of strings to a PowerShell array literal.""" escaped = ", ".join(_powershell_escape(i) for i in items) return f"@({escaped})" def _powershell_hashtable(d): """Serialize a dict[str, list[str]] to PowerShell @{} syntax.""" if not d: return "@{}" entries = [ f" {_powershell_escape(key)} = {_powershell_list(values)}" for key, values in sorted(d.items())] return "@{\n" + "\n".join(entries) + "\n}" def _powershell_flat_hashtable(d): """Serialize a dict[str, str] to PowerShell @{} syntax.""" if not d: return "@{}" entries = [ f" {_powershell_escape(key)} = {_powershell_escape(value)}" for key, value in sorted(d.items())] return "@{\n" + "\n".join(entries) + "\n}" def get_powershell_commands(root_parser, root_prefix, choice_functions=None): """ Recursive subcommand parser traversal, returning dicts of information on commands (formatted for output to the PowerShell completions script). Returns: subparsers : dict mapping prefix -> list of subparser names option_strings : dict mapping prefix -> list of option strings compgens : dict mapping action key -> completer function name choices : dict mapping action key -> list of choice strings nargs : dict mapping action key -> nargs value (string) preambles : list of preamble functions """ choice_type2fn = {k: v["powershell"] for k, v in CHOICE_FUNCTIONS.items()} if choice_functions: choice_type2fn.update(choice_functions) subparsers = {} option_strings = {} compgens = {} choices = {} nargs = {} help_text = {} preambles = [] def recurse(parser, prefix): get_help = parser._get_formatter()._expand_help discovered_subparsers = [] for i, positional in enumerate(parser._get_positional_actions()): action_key = f"{prefix}_pos_{i}" if hasattr(positional, 'complete'): comp_pattern = complete2pattern(positional.complete, 'powershell', choice_type2fn, preambles) if comp_pattern: compgens[action_key] = comp_pattern elif positional.choices: log.debug(f"choices:{prefix}:{sorted(positional.choices)}") this_positional_choices = [] for choice in positional.choices: if isinstance(positional.choices, dict): log.debug("subcommand:%s", choice) public_cmds = get_public_subcommands(positional) if choice in public_cmds: discovered_subparsers.append(str(choice)) this_positional_choices.append(str(choice)) subparser = positional.choices[choice] subcmd_help = next( (sub.help for sub in positional._get_subactions() if sub.dest == choice and sub.help not in (None, SUPPRESS)), None) desc = (subparser.description or subcmd_help or "").strip() if desc: help_text[f"{prefix}_{wordify(choice)}"] = desc.split("\n")[0] recurse(subparser, f"{prefix}_{wordify(choice)}") else: log.debug("skip:subcommand:%s", choice) else: this_positional_choices.append(str(choice)) if this_positional_choices: choices[action_key] = this_positional_choices if positional.help not in (None, SUPPRESS): help_text[action_key] = get_help(positional) if positional.nargs not in (None, "1", "?"): nargs[action_key] = str(positional.nargs) if discovered_subparsers: subparsers[prefix] = discovered_subparsers log.debug(f"subcommands:{prefix}:{discovered_subparsers}") option_strings[prefix] = sum( (opt.option_strings for opt in parser._get_optional_actions() if opt.help != SUPPRESS), []) for optional in parser._get_optional_actions(): if optional == SUPPRESS: continue for option_string in optional.option_strings: opt_key = f"{prefix}_{wordify(option_string)}" if hasattr(optional, 'complete'): comp_pattern = complete2pattern(optional.complete, 'powershell', choice_type2fn, preambles) if comp_pattern: compgens[opt_key] = comp_pattern if optional.choices: choices[opt_key] = list(map(str, optional.choices)) if optional.help not in (None, SUPPRESS): help_text[opt_key] = get_help(optional) if optional.nargs is not None and optional.nargs != 1: nargs[opt_key] = str(optional.nargs) recurse(root_parser, root_prefix) return subparsers, option_strings, compgens, choices, nargs, help_text, preambles @mark_completer("powershell") def complete_powershell(parser, root_prefix=None, preamble="", choice_functions=None): """ Returns PowerShell syntax autocompletion script. See `complete` for arguments. """ root_prefix = wordify(f"_shtab_{root_prefix or parser.prog}") (subparsers, option_strings, compgens, choices, nargs, help_text, extra_preambles) = get_powershell_commands(parser, root_prefix, choice_functions=choice_functions) # References: # - https://learn.microsoft.com/en-us/powershell/module/ # microsoft.powershell.core/register-argumentcompleter # - https://learn.microsoft.com/en-us/powershell/scripting/ # learn/shell/tab-completion preamble = "\n".join(list(dict.fromkeys(([preamble] if preamble else []) + extra_preambles))) return Template(r"""# AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab # Usage: # 1) Copy this to somewhere (e.g. ~\.config\powershell\completions\${prog}.ps1 # 2) Add the following line to your $PROFILE: # . ~\.config\powershell\completions\${prog}.ps1 # See also: https://learn.microsoft.com/en-us/powershell/scripting/learn/shell⁠ /creating-profiles#adding-customizations-to-your-profile ${preamble} # --- Completion data --- $$${root_prefix}_subparsers = ${subparsers_ht} $$${root_prefix}_option_strings = ${option_strings_ht} $$${root_prefix}_compgens = ${compgens_ht} $$${root_prefix}_choices = ${choices_ht} $$${root_prefix}_nargs = ${nargs_ht} $$${root_prefix}_help = ${help_ht} # --- Helper functions --- function _shtab_powershell_compgen_files { param([string]$WordToComplete) $dir = '' if ($WordToComplete -match '^(.*[\\/])') { $dir = $Matches[1] } Get-ChildItem -Path "$WordToComplete*" -File -ErrorAction SilentlyContinue | ForEach-Object { $dir + $_.Name } Get-ChildItem -Path "$WordToComplete*" -Directory -ErrorAction SilentlyContinue | ForEach-Object { $dir + $_.Name + [System.IO.Path]::DirectorySeparatorChar } } function _shtab_powershell_compgen_dirs { param([string]$WordToComplete) $dir = '' if ($WordToComplete -match '^(.*[\\/])') { $dir = $Matches[1] } Get-ChildItem -Path "$WordToComplete*" -Directory -ErrorAction SilentlyContinue | ForEach-Object { $dir + $_.Name + [System.IO.Path]::DirectorySeparatorChar } } function _shtab_powershell_replace_nonword { param([string]$Text) $Text -replace '[^\w]', '_' } # --- Main completer --- Register-ArgumentCompleter -Native -CommandName ${prog} -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) # Tokenize the command line (skip program name) $allTokens = @() if ($commandAst.CommandElements.Count -gt 1) { $allTokens = @($commandAst.CommandElements[1..($commandAst.CommandElements.Count - 1)] | ForEach-Object { $_.ToString() }) } # Determine which tokens are "completed" (before the word being typed) # The last token is the one currently being completed if it matches wordToComplete $tokens = @() if ($allTokens.Count -gt 0) { if ($wordToComplete -and $allTokens[-1] -eq $wordToComplete) { if ($allTokens.Count -gt 1) { $tokens = $allTokens[0..($allTokens.Count - 2)] } } else { $tokens = $allTokens } } # State tracking $prefix = '${root_prefix}' $completedPositionals = 0 $currentActionKey = "${prefix}_pos_0" $currentActionNargs = 1 $currentActionArgsConsumed = 0 $currentActionIsPositional = $true $posOnly = $false # Helper: look up nargs for a given action key (default 1) function Get-ActionNargs($actionKey) { $n = $$${root_prefix}_nargs[$actionKey] if ($n) { return $n } else { return '1' } } # Helper: look up help text for a given action key (empty if none) function Get-ActionHelp($actionKey) { return $$${root_prefix}_help[$actionKey] } # Walk completed tokens to determine current parser state foreach ($token in $tokens) { if ($posOnly -or $token -ne '--') { # Check for subparser match $currentSubparsers = $$${root_prefix}_subparsers[$prefix] if ($currentSubparsers -and $currentSubparsers -contains $token) { $prefix = $prefix + '_' + (_shtab_powershell_replace_nonword $token) $completedPositionals = 0 $currentActionKey = "${prefix}_pos_0" $currentActionNargs = Get-ActionNargs $currentActionKey $currentActionArgsConsumed = 0 $currentActionIsPositional = $true continue } # Check for option string match $currentOptions = $$${root_prefix}_option_strings[$prefix] if ($currentOptions -and $currentOptions -contains $token) { $currentActionKey = $prefix + '_' + (_shtab_powershell_replace_nonword $token) $currentActionNargs = Get-ActionNargs $currentActionKey $currentActionArgsConsumed = 0 $currentActionIsPositional = $false continue } # Consume argument for current action $currentActionArgsConsumed++ if ($currentActionNargs -ne '*' -and $currentActionNargs -ne '+' -and $currentActionNargs -ne '?' -and $currentActionNargs -notlike '*...*') { if ($currentActionArgsConsumed -ge [int]$currentActionNargs) { if ($currentActionIsPositional) { $completedPositionals++ } $currentActionKey = "${prefix}_pos_${completedPositionals}" $currentActionNargs = Get-ActionNargs $currentActionKey $currentActionArgsConsumed = 0 $currentActionIsPositional = $true } } } else { $posOnly = $true } } # --- Generate completions --- if ($env:SHTAB_DEBUG -eq 'true') { Write-Host "shtab: wordToComplete='$wordToComplete' prefix='$prefix' ` actionKey='$currentActionKey' isPositional='$currentActionIsPositional' posOnly='$posOnly'" } $completions = @() if (-not $posOnly -and $wordToComplete -like '-*') { # Complete option strings, tooltipped with each option's own help text $opts = $$${root_prefix}_option_strings[$prefix] if ($opts) { foreach ($opt in $opts) { if ($opt -like "$wordToComplete*") { $optKey = $prefix + '_' + (_shtab_powershell_replace_nonword $opt) $completions += , @{Text = $opt; Tooltip = Get-ActionHelp $optKey} } } } } else { # Complete subparsers (only when current action is positional), # tooltipped with each subcommand's own help text if ($currentActionIsPositional) { $subs = $$${root_prefix}_subparsers[$prefix] if ($subs) { foreach ($sub in $subs) { if ($sub -like "$wordToComplete*") { $subKey = $prefix + '_' + (_shtab_powershell_replace_nonword $sub) $completions += , @{Text = $sub; Tooltip = Get-ActionHelp $subKey} } } } } $actionHelp = Get-ActionHelp $currentActionKey # Complete choices for current action (positional or option) $actionChoices = $$${root_prefix}_choices[$currentActionKey] if ($actionChoices) { foreach ($choice in $actionChoices) { if ($choice -like "$wordToComplete*") { $completions += , @{Text = $choice; Tooltip = $actionHelp} } } } # Complete using compgen function for current action $actionCompgen = $$${root_prefix}_compgens[$currentActionKey] if ($actionCompgen) { foreach ($item in @(& $actionCompgen $wordToComplete)) { $completions += , @{Text = $item; Tooltip = $actionHelp} } } } # Deduplicate (by text) and emit CompletionResult objects $seen = New-Object System.Collections.Generic.HashSet[string] $results = @() foreach ($c in $completions) { if ($seen.Add($c.Text)) { $tooltip = if ($c.Tooltip) { $c.Tooltip } else { $c.Text } $results += [System.Management.Automation.CompletionResult]::new( $c.Text, # completionText $c.Text, # listItemText 'ParameterValue', # resultType $tooltip # toolTip ) } } # Prevent fallback file-path completion (PowerShell/PowerShell#19628) if ($results.Count -eq 0) { return $null } $results } """.replace("⁠\n", "")).safe_substitute( subparsers_ht=_powershell_hashtable(subparsers), option_strings_ht=_powershell_hashtable(option_strings), compgens_ht=_powershell_flat_hashtable(compgens), choices_ht=_powershell_hashtable(choices), nargs_ht=_powershell_flat_hashtable(nargs), help_ht=_powershell_flat_hashtable(help_text), preamble=f"\n# Custom Preamble\n{preamble}\n# End Custom Preamble\n" if preamble else "", root_prefix=root_prefix, prog=parser.prog, ) def complete(parser: ArgumentParser, shell: str = "bash", root_prefix: Opt[str] = None, preamble: str = "", choice_functions: Opt[Any] = None) -> str: """ shell: bash/zsh/tcsh/fish/powershell root_prefix: prefix for shell functions to avoid clashes (default: "_{parser.prog}") preamble: text to prepend to generated script (e.g. `"_myprog_custom_function(){ echo hello }"`). Consider using `parser.add_argument().complete = shtab.cmd("echo hello")` instead. choice_functions: *deprecated* NOTE: `parser.add_argument().complete = ...` can be used to define custom completions (e.g. filenames). See <../examples/pathcomplete.py>. """ if isinstance(preamble, dict): # warn("replace `complete(preamble={...})` with `.complete = {'preamble': {...}}`", # DeprecationWarning, stacklevel=2) preamble = preamble.get(shell, "") completer = get_completer(shell) return completer( parser, root_prefix=root_prefix, preamble=preamble, choice_functions=choice_functions, ) def completion_action(parent: Opt[ArgumentParser] = None, preamble: Union[str, dict[str, str]] = ""): class PrintCompletionAction(_ShtabPrintCompletionAction): def __call__(self, parser, namespace, values, option_string=None): print(complete(parent or parser, values, preamble=preamble)) parser.exit(0) return PrintCompletionAction def add_argument_to( parser: _ActionsContainerT, option_string: Union[str, list[str]] = "--print-completion", help: str = "print shell completion script", # pylint: disable=W0622 parent: Opt[ArgumentParser] = None, preamble: Union[str, dict[str, str]] = "", ) -> _ActionsContainerT: """ option_string: iff positional (no `-` prefix) then `parser` is assumed to actually be a subparser (subcommand mode) parent: required in subcommand mode preamble: see `complete` for details """ if isinstance(option_string, str): option_string = [option_string] kwargs = { "choices": SUPPORTED_SHELLS, "default": None, "help": help, "action": completion_action(parent, preamble)} if option_string[0][0] != "-": # subparser mode kwargs.update(default=SUPPORTED_SHELLS[0], nargs="?") if parent is None: raise ValueError("subcommand mode: parent required") parser.add_argument(*option_string, **kwargs) return parser ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9327204 shtab-1.12.1/shtab/__main__.py0000644000175100017510000000014515245646712015552 0ustar00runnerrunnerimport sys from .main import main if __name__ == "__main__": sys.exit(main(sys.argv[1:]) or 0) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9327204 shtab-1.12.1/shtab/click.py0000644000175100017510000001142215245646712015117 0ustar00runnerrunnerimport argparse import logging from functools import wraps import click from . import DIRECTORY, FILE, SUPPORTED_SHELLS, complete __all__ = ['add_command_to', 'option'] log = logging.getLogger(__name__) @click.argument('shell', default="bash", type=click.Choice(SUPPORTED_SHELLS)) @click.option('--prefix', default=None, help="Prepended to generated functions to avoid clashes.") @click.option('--preamble', default="", help="Prepended to generated script.") @click.pass_context def completion(ctx, shell, prefix, preamble): """Print shell completion script.""" root = ctx.find_root() parser = click2argparse(root.command) print(complete(parser, shell=shell, root_prefix=prefix, preamble=preamble)) def add_command_to(group: click.Group, name='completion', help=completion.__doc__): # pylint: disable=redefined-builtin, yapf: disable """ Add `completion` command to 'myapp' click group: >>> import click, shtab.click >>> @click.group('myapp') ... def main(): ... ... >>> shtab.click.add_command_to(main) # magic! """ return group.command(name=name, help=help)(completion) def option(help="Print shell completion script.", # pylint: disable=redefined-builtin **attrs): # yapf: disable """ Attaches a `--print-completion=SHELL` option to 'myapp' command (to print a shell completion script and exit): >>> import click, shtab.click >>> @click.command('myapp') ... @shtab.click.option() # magic! ... @click.argument(...) ... @click.option(...) ... def main(...): """ def inner(func): @click.option('--print-completion', default=None, required=False, type=click.Choice(SUPPORTED_SHELLS), help=help, **attrs) @wraps(func) @click.pass_context def wrapper(ctx, print_completion, *args, **kwargs): if print_completion: root = ctx.find_root() parser = click2argparse(root.command) print(complete(parser, shell=print_completion)) return return func(*args, **kwargs) return wrapper return inner def _complete_paths(arg, param): if isinstance(param.type, click.File): # for safety, don't complete files in write-only (creation) mode arg.complete = FILE if set("ra+") & set(param.type.mode.lower()) else DIRECTORY elif isinstance(param.type, click.Path): if param.type.file_okay: arg.complete = FILE elif param.type.dir_okay: arg.complete = DIRECTORY def click2argparse(command: click.Command, parser=None): if parser is None: log.debug("parser:%s", command.name) parser = argparse.ArgumentParser(prog=command.name, description=command.help) for param in command.params: if getattr(param, 'deprecated', False): # click>=8.2 continue if getattr(param, 'hidden', False): # pallets/click#3788 continue spec = {'help': getattr(param, 'help', None)} # click>=8.5 if param.param_type_name == 'option': log.debug("optional:%s", param.opts) if param.is_flag: spec['action'] = 'store_true' else: spec['metavar'] = param.metavar spec['nargs'] = param.nargs spec['required'] = param.required if isinstance(param.type, click.Choice): spec['choices'] = param.type.choices log.debug("spec:%s", spec) arg = parser.add_argument(*param.opts, **spec) _complete_paths(arg, param) elif param.param_type_name == 'argument': log.debug("positional:%s", param.name) if param.nargs == -1: spec['nargs'] = '+' if param.required else '*' else: spec['nargs'] = param.nargs if isinstance(param.type, click.Choice): spec['choices'] = param.type.choices log.debug("spec:%s", spec) arg = parser.add_argument(param.name, **spec) _complete_paths(arg, param) if hasattr(command, 'commands') and len(command.commands) > 0: log.debug("subcommands:%s", command.commands) subparsers = parser.add_subparsers(dest=command.name, help=command.short_help, description=command.help, required=True) for name, subcmd in command.commands.items(): if subcmd.hidden or getattr(subcmd, 'deprecated', False): # click>=8.2 continue log.debug("subcommand:%s", name) subparser = subparsers.add_parser(name, help=subcmd.short_help, description=subcmd.help) click2argparse(subcmd, subparser) return parser ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9327204 shtab-1.12.1/shtab/main.py0000644000175100017510000000552315245646712014763 0ustar00runnerrunnerimport argparse import logging import os import sys from contextlib import contextmanager from importlib import import_module from pathlib import Path from . import SUPPORTED_SHELLS, __version__, add_argument_to, complete log = logging.getLogger(__name__) def get_main_parser(): parser = argparse.ArgumentParser(prog="shtab") parser.add_argument("parser", help="importable parser (or function returning parser) or click command") parser.add_argument("--version", action="version", version="%(prog)s " + __version__) parser.add_argument("-s", "--shell", default=SUPPORTED_SHELLS[0], choices=SUPPORTED_SHELLS) parser.add_argument("-o", "--output", default='-', help="output file (- for stdout)", type=Path) parser.add_argument("--prefix", help="prepended to generated functions to avoid clashes") parser.add_argument("--preamble", help="prepended to generated script") parser.add_argument("--prog", help="custom program name (overrides `parser.prog`)") parser.add_argument( "-u", "--error-unimportable", default=False, action="store_true", help="raise errors if `parser` is not found in $PYTHONPATH", ) parser.add_argument("--verbose", dest="loglevel", action="store_const", default=logging.INFO, const=logging.DEBUG, help="Log debug information") add_argument_to(parser, "--print-own-completion", help="print shtab's own completion") return parser def main(argv=None): parser = get_main_parser() args = parser.parse_args(argv) logging.basicConfig(level=args.loglevel) log.debug(args) module, other_parser = args.parser.rsplit(".", 1) if sys.path and sys.path[0]: # not blank so not searching curdir sys.path.insert(1, os.curdir) try: module = import_module(module) except ImportError as err: if args.error_unimportable: raise log.debug(str(err)) return other_parser = getattr(module, other_parser) if callable(other_parser): try: from .click import click, click2argparse except ImportError: other_parser = other_parser() else: if isinstance(other_parser, click.Command): other_parser = click2argparse(other_parser) else: other_parser = other_parser() if args.prog: other_parser.prog = args.prog @contextmanager def _open(out_path): if str(out_path) in ("-", "stdout"): yield sys.stdout else: with out_path.open('w') as fd: yield fd with _open(args.output) as fd: print( complete(other_parser, shell=args.shell, root_prefix=args.prefix or args.parser.split(".", 1)[0], preamble=args.preamble), file=fd) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9327204 shtab-1.12.1/shtab/py.typed0000644000175100017510000000014315245646712015155 0ustar00runnerrunnerThis file exists solely to signal that the `shtab` package carries inline types. Do not delete it. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2406077 shtab-1.12.1/shtab.egg-info/0000755000175100017510000000000015245646720015151 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2171907 shtab-1.12.1/shtab.egg-info/PKG-INFO0000644000175100017510000001663515245646720016261 0ustar00runnerrunnerMetadata-Version: 2.4 Name: shtab Version: 1.12.1 Summary: Automagic shell tab completion for Python CLI applications Author-email: Casper da Costa-Luis Maintainer-email: tqdm developers License: MPL-2.0 Project-URL: documentation, https://tqdm.github.io/shtab Project-URL: repository, https://github.com/tqdm/shtab Project-URL: changelog, https://github.com/tqdm/shtab/releases Keywords: tab,completion,argparse,shell,bash,zsh,tcsh,fish,powershell,pwsh Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: MacOS X Classifier: Environment :: Other Environment Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Other Audience Classifier: Intended Audience :: System Administrators Classifier: Operating System :: MacOS Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: BSD Classifier: Operating System :: POSIX :: BSD :: FreeBSD Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: SunOS/Solaris Classifier: Operating System :: Unix Classifier: Programming Language :: Other Scripting Engines Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Unix Shell Classifier: Topic :: Desktop Environment Classifier: Topic :: Education :: Computer Aided Instruction (CAI) Classifier: Topic :: Education :: Testing Classifier: Topic :: Office/Business Classifier: Topic :: Other/Nonlisted Topic Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Pre-processors Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: System Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Shells Classifier: Topic :: System :: System Shells Classifier: Topic :: Terminals Classifier: Topic :: Utilities Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-File: LICENCE Provides-Extra: dev Requires-Dist: pytest>=6; extra == "dev" Requires-Dist: pytest-cov; extra == "dev" Requires-Dist: click; extra == "dev" Dynamic: license-file |Logo| shtab ===== |Tests| |Coverage| |Quality| |PyPI| |Conda| |PyPI-Downloads| |Licence| - What: Automatically generate shell tab completion scripts for Python CLI apps - Why: Speed & correctness. Alternatives like `argcomplete `_ and `pyzshcomplete `_ are slow and have side-effects - How: ``shtab`` processes an ``argparse.ArgumentParser`` object to generate a tab completion script for your shell Features -------- - Outputs tab completion scripts for multiple shells - ``bash``, ``zsh``, ``fish``, ``tcsh``, ``powershell`` - Supports - `argparse `_ - `docopt `_ (via `argopt `_) - `click `_ - ````, ``--options`` and ``sub commands`` - Choices (``--say={hello,goodbye}``) - Paths (``--file={*.y*ml,*.toml}``, ``--dir=*/``) - Dynamic shell commands (``--branch=$(git branch)``) ------------------------------------------ .. contents:: Table of Contents :backlinks: top Installation ------------ Choose one of: - ``pip install shtab``, or - ``conda install -c conda-forge shtab`` See `operating system-specific instructions in the docs `_. Usage ----- There are two ways of using ``shtab``: - `CLI Usage `_: ``shtab``'s own CLI interface for external applications - may not require any code modifications whatsoever - end-users execute ``shtab your_cli_app.your_parser_object`` - `Library Usage `_: as a library integrated into your CLI application - adds a couple of lines to your application - argument mode: end-users execute ``your_cli_app --print-completion {bash,zsh,tcsh,fish,powershell}`` - subparser mode: end-users execute ``your_cli_app completion {bash,zsh,tcsh,fish,powershell}`` Examples -------- See `the docs for usage examples `_. FAQs ---- Not working? Check out `frequently asked questions `_. Alternatives ------------ All these execute the underlying script *every* time ```` is pressed (slow and have side-effects): - `argcomplete `_ - `pyzshcomplete `_ (only provides ``zsh`` completion) - `click `_ (don't want to migrate away from ``click``? Use ``shtab``'s one-liner click integration `in the CLI `_ or `in a library `_) Contributions ------------- Please do open `issues `_ & `pull requests `_! Some ideas: - support ``python -m`` prefix (`#55 `_) See `CONTRIBUTING.md `_ for more guidance. |git-fame| |Hits| .. |Logo| image:: https://github.com/tqdm/shtab/raw/main/meta/logo.png .. |Tests| image:: https://img.shields.io/github/actions/workflow/status/tqdm/shtab/test.yml?logo=github&label=tests :target: https://github.com/tqdm/shtab/actions :alt: Tests .. |Coverage| image:: https://codecov.io/gh/tqdm/shtab/branch/main/graph/badge.svg :target: https://codecov.io/gh/tqdm/shtab :alt: Coverage .. |Quality| image:: https://app.codacy.com/project/badge/Grade/0ce50da5c6e04236a891b092c7012753 :target: https://app.codacy.com/gh/tqdm/shtab/dashboard :alt: Quality .. |Conda| image:: https://img.shields.io/conda/v/conda-forge/shtab.svg?label=conda&logo=conda-forge :target: https://anaconda.org/conda-forge/shtab :alt: conda-forge .. |PyPI| image:: https://img.shields.io/pypi/v/shtab.svg?label=pip&logo=PyPI&logoColor=white :target: https://pypi.org/project/shtab :alt: PyPI .. |PyPI-Downloads| image:: https://static.pepy.tech/personalized-badge/shtab?left_text=downloads%2Fmonth :target: https://pepy.tech/project/shtab :alt: Downloads .. |git-fame| image:: https://git-fame.cdcl.ml/gh/tqdm/shtab?min=1&w=1&M=1&C=1&enum=1 :alt: git-fame :target: https://git-fame.cdcl.ml/gh/tqdm/shtab?w=1&M=1&C=1&enum=1 .. |Hits| image:: https://cgi.cdcl.ml/hits?q=shtab&style=social&r=https://github.com/tqdm/shtab&a=hidden :target: https://cgi.cdcl.ml/hits?q=shtab&a=plot&r=https://github.com/tqdm/shtab&style=social :alt: Hits .. |LICENCE| image:: https://img.shields.io/pypi/l/shtab.svg :target: https://raw.githubusercontent.com/tqdm/shtab/main/LICENCE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2282374 shtab-1.12.1/shtab.egg-info/SOURCES.txt0000644000175100017510000000156315245646720017042 0ustar00runnerrunner.gitignore .mailmap .pre-commit-config.yaml .prospector.yml CONTRIBUTING.md LICENCE README.rst pyproject.toml .github/FUNDING.yml .github/ISSUE_TEMPLATE/config.yml .github/workflows/test.yml docs/index.md docs/pydoc-markdown.yml docs/pydoc_markdown_shtab.py docs/requirements.txt docs/use.md docs/custom_theme/main.html docs/custom_theme/partials/toc.html examples/click_process.py examples/click_subcommand.py examples/customcomplete.py examples/docopt_greeter.py examples/pathcomplete.py meta/logo.png shtab/__init__.py shtab/__main__.py shtab/click.py shtab/main.py shtab/py.typed shtab.egg-info/PKG-INFO shtab.egg-info/SOURCES.txt shtab.egg-info/dependency_links.txt shtab.egg-info/entry_points.txt shtab.egg-info/requires.txt shtab.egg-info/scm_file_list.json shtab.egg-info/scm_version.json shtab.egg-info/top_level.txt tests/__init__.py tests/test_click.py tests/test_shtab.py././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1788300752.217442 shtab-1.12.1/shtab.egg-info/dependency_links.txt0000644000175100017510000000000115245646720021217 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1788300752.217817 shtab-1.12.1/shtab.egg-info/entry_points.txt0000644000175100017510000000005215245646720020444 0ustar00runnerrunner[console_scripts] shtab = shtab.main:main ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2181964 shtab-1.12.1/shtab.egg-info/requires.txt0000644000175100017510000000004215245646720017545 0ustar00runnerrunner [dev] pytest>=6 pytest-cov click ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2149718 shtab-1.12.1/shtab.egg-info/scm_file_list.json0000644000175100017510000000156615245646720020670 0ustar00runnerrunner{ "files": [ ".github/FUNDING.yml", ".github/ISSUE_TEMPLATE/config.yml", ".github/workflows/test.yml", ".gitignore", ".mailmap", ".pre-commit-config.yaml", ".prospector.yml", "CONTRIBUTING.md", "LICENCE", "README.rst", "docs/custom_theme/main.html", "docs/custom_theme/partials/toc.html", "docs/index.md", "docs/pydoc-markdown.yml", "docs/pydoc_markdown_shtab.py", "docs/requirements.txt", "docs/use.md", "examples/click_process.py", "examples/click_subcommand.py", "examples/customcomplete.py", "examples/docopt_greeter.py", "examples/pathcomplete.py", "meta/logo.png", "pyproject.toml", "shtab/__init__.py", "shtab/__main__.py", "shtab/click.py", "shtab/main.py", "shtab/py.typed", "tests/__init__.py", "tests/test_click.py", "tests/test_shtab.py" ] } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2081409 shtab-1.12.1/shtab.egg-info/scm_version.json0000644000175100017510000000024115245646720020370 0ustar00runnerrunner{ "tag": "1.12.1", "distance": 0, "node": "ge1ab40616e77298204a61e1aad50d6f6e90e9217", "dirty": false, "branch": "HEAD", "node_date": "2026-09-01" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2183402 shtab-1.12.1/shtab.egg-info/top_level.txt0000644000175100017510000000000615245646720017677 0ustar00runnerrunnershtab ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300752.2410746 shtab-1.12.1/tests/0000755000175100017510000000000015245646720013520 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9327204 shtab-1.12.1/tests/__init__.py0000644000175100017510000000000015245646712015620 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9329576 shtab-1.12.1/tests/test_click.py0000644000175100017510000000273115245646712016222 0ustar00runnerrunnerfrom importlib.metadata import version import pytest from examples import click_process, click_subcommand from shtab.main import main from .test_shtab import no_log_info # noqa: F401, pylint: disable=unused-import, yapf: disable from .test_shtab import fix_shell ARG_HELP = version('click') >= '8.5' @pytest.fixture def runner(): pytest.importorskip('click') from click import testing return testing.CliRunner() @fix_shell def test_main(shell, capsys): main(['-us', shell, 'examples.click_subcommand.main']) captured = capsys.readouterr() assert "# AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab" in captured.out assert not captured.err @fix_shell def test_plain(shell, runner): result = runner.invoke(click_process.process, ['.']) assert result.output == "Reading from None and writing to .\n" assert result.exit_code == 0 result = runner.invoke(click_process.process, ['--print-completion', shell]) assert "# AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab" in result.output assert result.exit_code == 0 @fix_shell def test_rootcmd(shell, runner): result = runner.invoke(click_subcommand.main, ['process', '.']) assert result.output == "Reading from None and writing to .\n" assert result.exit_code == 0 result = runner.invoke(click_subcommand.main, ['completion', shell]) assert "# AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab" in result.output assert result.exit_code == 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788300745.9329576 shtab-1.12.1/tests/test_shtab.py0000644000175100017510000010531715245646712016242 0ustar00runnerrunner"""Tests for `shtab`.""" import logging import os import re import select import shutil import signal import subprocess import time from argparse import SUPPRESS, Action, ArgumentParser import pytest import shtab from shtab.main import get_main_parser, main fix_shell = pytest.mark.parametrize("shell", shtab.SUPPORTED_SHELLS) def complete(parser, shell, **kwargs): """convenience function to print for debugging in case of failure""" completion = shtab.complete(parser, shell, **kwargs) print(completion) return completion class Bash: def __init__(self, init_script=""): self.init = init_script def test(self, cmd="1", failure_message=""): """Equivalent to `bash -c '{init}; [[ {cmd} ]]'`.""" init = self.init + "\n" if self.init else "" proc = subprocess.Popen(['bash', '-o', 'pipefail', '-euc', f"{init}[[ {cmd} ]]"], text=True) stdout, stderr = proc.communicate() assert (0 == proc.wait() and not stdout and not stderr), f"""\ {failure_message} {cmd} === stdout === {stdout or ""}=== stderr === {stderr or ""}""" def compgen(self, compgen_cmd, word, expected_completions, failure_message=""): self.test( f'"$(echo $(compgen {compgen_cmd} -- "{word}"))" = "{expected_completions}"', failure_message, ) @pytest.mark.parametrize("init,test", [("export FOO=1", '"$FOO" -eq 1'), ("", '-z "${FOO-}"')]) def test_bash(init, test): shell = Bash(init) shell.test(test) def test_bash_compgen(): shell = Bash() shell.compgen('-W "foo bar foobar"', "f", "foo foobar") def test_choices(): assert "x" in shtab.Optional.FILE assert "" in shtab.Optional.FILE assert "x" in shtab.Required.FILE assert "" not in shtab.Required.FILE @pytest.fixture(autouse=True) def no_log_info(caplog): with caplog.at_level(logging.INFO, logger="shtab"): yield assert not caplog.get_records('call') @fix_shell def test_main(shell): main(['-us', shell, 'shtab.main.get_main_parser']) @fix_shell def test_main_self_completion(shell, capsys): with pytest.raises(SystemExit) as exc: main(["--print-own-completion", shell]) assert exc.type is SystemExit and exc.value.code == 0 captured = capsys.readouterr() assert not captured.err expected = { 'bash': "complete -F _shtab_shtab shtab", 'zsh': "_shtab_shtab_commands()", 'tcsh': "complete shtab", 'fish': "complete -c shtab", 'powershell': "Register-ArgumentCompleter -Native -CommandName shtab"} assert expected[shell] in captured.out @pytest.mark.parametrize('output', ["-", "stdout", "test.txt"]) @fix_shell def test_main_output_path(shell, capsys, change_dir, output): assert not capsys.readouterr().out main(["-s", shell, "shtab.main.get_main_parser", "-o", output]) captured = capsys.readouterr() assert not captured.err expected = { 'bash': "complete -F _shtab_shtab shtab", 'zsh': "_shtab_shtab_commands()", 'tcsh': "complete shtab", 'fish': "complete -c shtab", 'powershell': "Register-ArgumentCompleter -Native -CommandName shtab"} if output in ("-", "stdout"): assert expected[shell] in captured.out else: assert not captured.out assert expected[shell] in (change_dir / output).read_text() @fix_shell def test_prog_override(shell, capsys): main(["-s", shell, "--prog", "foo", "shtab.main.get_main_parser"]) captured = capsys.readouterr() assert not captured.err if shell == 'bash': assert "complete -F _shtab_shtab foo" in captured.out else: pytest.skip("WiP") @fix_shell def test_prog_scripts(shell, capsys): main(["-s", shell, "--prog", "script.py", "shtab.main.get_main_parser"]) captured = capsys.readouterr() assert not captured.err script_py = [i.strip() for i in captured.out.splitlines() if "script.py" in i and i[0] != '#'] if shell == 'bash': assert script_py == ["complete -F _shtab_shtab script.py"] elif shell == 'zsh': assert captured.out.startswith("#compdef script.py") assert script_py == [ "_describe 'script.py commands' _commands", 'local context state line curcontext="$curcontext" ' "one_or_more='(*)' remainder='(-)*:' default='*::: :->script.py'", "_shtab_shtab_options+=(': :_shtab_shtab_commands' '*::: :->script.py')", "script.py)", "compdef _shtab_shtab -N script.py"] elif shell == 'tcsh': assert script_py == ["complete script.py \\"] elif shell == 'fish': start = 'complete -c script.py -n _shtab_shtab_using' assert script_py == [ 'complete -c script.py -e', 'complete -c script.py -f', f"{start} -s h -l help -d 'show this help message and exit'", f"{start} -l version -d 'show program'\"'\"'s version number and exit'", f'{start} -s s -l shell -xka "bash zsh tcsh fish powershell" -d shell', f"{start} -s o -l output -x -d 'output file (- for stdout)'", f"{start} -l prefix -x -d 'prepended to generated functions to avoid clashes'", f"{start} -l preamble -x -d 'prepended to generated script'", f"{start} -l prog -x -d 'custom program name (overrides `parser.prog`)'", f"{start} -s u -l error-unimportable -d" " 'raise errors if `parser` is not found in $PYTHONPATH'", f"{start} -l verbose -d 'Log debug information'", f'{start} -l print-own-completion -xka "bash zsh tcsh fish powershell" -d' " 'print shtab'\"'\"'s own completion'"] elif shell == 'powershell': assert script_py == [ "Register-ArgumentCompleter -Native -CommandName script.py -ScriptBlock {"] else: raise NotImplementedError(shell) @fix_shell def test_prefix_override(shell, capsys): main(["-s", shell, "--prefix", "foo", "shtab.main.get_main_parser"]) captured = capsys.readouterr() assert not captured.err if shell == 'bash': shell = Bash(captured.out) shell.compgen('-W "${_shtab_foo_option_strings[*]}"', "--h", "--help") else: pytest.skip("WiP") @fix_shell def test_complete(shell): parser = get_main_parser() completion = complete(parser, shell) if shell == 'bash': shell = Bash(completion) shell.compgen('-W "${_shtab_shtab_option_strings[*]}"', "--h", "--help") else: pytest.skip("WiP") @fix_shell def test_positional_choices(shell): parser = ArgumentParser(prog="test") parser.add_argument("posA", choices=["one", "two"]) parser.add_argument("posB", choices=["BAA"]).complete = shtab.cmd("echo BZZ") completion = complete(parser, shell) assert "BAA" not in completion and "BZZ" in completion, ".complete should override choices" if shell == 'bash': shell = Bash(completion) shell.compgen('-W "$_shtab_test_pos_0_choices"', "o", "one") else: pytest.skip("WiP") def test_zsh_optional_positionals_keep_parser_order(): parser = ArgumentParser(prog="keyring", add_help=False) parser.add_argument("operation", choices=["get", "set", "del"], nargs="?") parser.add_argument("service", nargs="?") completion = complete(parser, "zsh") operation = '":operation:(get set del)"' service = '":service:"' assert completion.count(operation) == 1 assert completion.count(service) == 1 assert completion.index(operation) < completion.index(service) def test_zsh_subparser_positionals_are_not_duplicated(): parser = ArgumentParser(prog="test", add_help=False) subparsers = parser.add_subparsers() subparser = subparsers.add_parser("run", help="run", add_help=False) subparser.add_argument("mode", choices=["fast", "slow"], nargs="?") subparser.add_argument("path", nargs="?") completion = complete(parser, "zsh") mode = '":mode:(fast slow)"' path = '":path:"' assert completion.count(mode) == 1 assert completion.count(path) == 1 assert completion.index(mode) < completion.index(path) @fix_shell def test_custom_complete(shell): parser = ArgumentParser(prog="test") _complete = parser.add_argument("posA").complete = { 'bash': "_shtab_test_some_func", 'fish': "(_shtab_test_some_func)"} preamble = { 'bash': "_shtab_test_some_func() { compgen -W 'one two' -- $1 ;}", 'fish': "function _shtab_test_some_func\n printf '%s\\n' one two\nend"} # with pytest.warns(DeprecationWarning): completion_deprecated = complete(parser, shell, preamble=preamble) _complete['preamble'] = preamble completion = complete(parser, shell) assert completion == completion_deprecated if shell == 'bash': shell = Bash(completion) shell.test('"$($_shtab_test_pos_0_COMPGEN o)" = "one"') elif shell == 'fish': assert fish_candidates(completion, "test o") == ["one"] else: pytest.skip("WiP") def zsh_specs(completion, name, tmp_path): """`zsh -n` the completion, then return the values zsh assigns to array `name`.""" if not shutil.which('zsh'): pytest.skip("zsh not available") subprocess.check_call(['zsh', '-nc', completion]) script = tmp_path / "completion.zsh" script.write_text(completion) # `eval` so the script registers itself rather than running `compdef` (unavailable here) values = subprocess.check_output( ['zsh', '-f', '-c', f'eval "$(<{script})" 2>/dev/null; print -rl -- "${{(@){name}}}"'], text=True) return values.splitlines() @pytest.mark.parametrize("help_text", [ "plain help", "don't do this", "e.g. '>size_added,path'", 'a "quoted" value', "cost: $5 (100%%) `tick`"]) def test_zsh_help_quoting(help_text, tmp_path): """Help must not gain stray quotes (#224)""" parser = ArgumentParser(prog="test", add_help=False) parser.add_argument("--opt", help=help_text) completion = complete(parser, 'zsh') assert "'\"'\"'" not in completion, ("`shlex.quote`'s `'\"'\"'` idiom is invalid" " inside the double-quoted specs") specs = zsh_specs(completion, "_shtab_test_options", tmp_path) assert len(specs) == 1, f"quoting split the spec into {len(specs)} words: {specs}" # `_arguments` strips the backslashes; what must not appear is *extra* quotes assert specs[0].count("'") == help_text.count("'") assert specs[0].count('"') == help_text.count('"') @fix_shell def test_non_sequence_choices(shell, tmp_path): parser = ArgumentParser(prog="myprog", add_help=False) parser.add_argument("--mapping", choices={"one": 1, "two": 2}) parser.add_argument("posA", choices={"three"}) completion = complete(parser, shell) if shell == 'bash': shell = Bash(completion) shell.compgen('-W "${_shtab_myprog_pos_0_choices[*]}"', "t", "three") elif shell == 'zsh': specs = zsh_specs(completion, "_shtab_myprog_options", tmp_path) assert specs == ["--mapping[mapping]:mapping:(one two)", ":posA:(three)"] elif shell == 'fish': assert fish_candidates(completion, "myprog t") == ["three"] assert fish_candidates(completion, "myprog --mapping t") == ["two"] else: pytest.skip("WiP") def test_zsh_remainder_custom_complete_has_optional_message_colon(): parser = ArgumentParser(prog="test") parser.add_argument("command", nargs=1).complete = {'zsh': "{_command_names -e}"} parser.add_argument("args", nargs="...").complete = {'zsh': "_normal"} completion = complete(parser, 'zsh') assert '"(-)*::args:_normal"' in completion assert '"(-)*:args:_normal"' not in completion def test_zsh_custom_action_nargs_zero_takes_no_argument(): class CustomFlagAction(Action): def __call__(self, parser, namespace, values, option_string=None): pass parser = ArgumentParser(prog="test", add_help=False) parser.add_argument("--help", "-h", action=CustomFlagAction, help="Helpy", nargs=0, default=SUPPRESS) completion = complete(parser, 'zsh') assert '{--help,-h}"[Helpy]"' in completion assert '{--help,-h}"[Helpy]:help:"' not in completion def fish_candidates(completion, cmdline): """Source `completion` in fish, then return the completion candidates for `cmdline`.""" if not shutil.which('fish'): pytest.skip("fish not available") quoted = "'" + cmdline.replace("\\", "\\\\").replace("'", "\\'") + "'" proc = subprocess.check_output(['fish', '-c', f"{completion}\ncomplete -C{quoted}"], text=True) # each output line is "candidatedescription" (or just "candidate") return [line.split("\t")[0] for line in proc.splitlines() if line.strip()] def powershell_candidates(completion, cmdline): """Source `completion` in PowerShell, then return the completion candidates for `cmdline`.""" if not shutil.which('pwsh'): pytest.skip("pwsh not available") proc = subprocess.check_output([ 'pwsh', '-NoProfile', '-NonInteractive', '-Command', f""" {completion} $line = '{cmdline}' $results = TabExpansion2 -inputScript $line -cursorColumn $line.Length $results.CompletionMatches | Where-Object {{ $_.ResultType -eq 'ParameterValue' }} | ForEach-Object {{ $_.CompletionText }} """], text=True) return [line for line in proc.splitlines() if line.strip()] def tcsh_candidates(completion, cmdlines, cwd): """ Return pty-driven completion candidates for each of `cmdlines`. Reason: tcsh has no `complete -C` equivalent. """ pty = pytest.importorskip('pty') if not shutil.which('tcsh'): pytest.skip("tcsh not available") script = cwd / "completion.tcsh" script.write_text(completion) prompt = "|candidates|" pid, fd = pty.fork() if pid == 0: # child os.chdir(cwd) os.environ['TERM'] = 'xterm' os.environ['COLUMNS'] = '999' os.execvp('tcsh', ['tcsh', '-f', '-i']) output = "" def clean(chunk): return re.sub(r"\x1b\[[0-9;]*[A-Za-z]|[\a\r\b]", "", chunk.decode('utf-8', 'replace')) def read(num_prompts, timeout=10.0): """read until `timeout` elapses or `num_prompts` seen, then drain""" nonlocal output deadline = time.time() + timeout while output.count(prompt) < num_prompts and time.time() < deadline: if select.select([fd], [], [], 0.1)[0]: try: chunk = os.read(fd, 1 << 16) except OSError: break if not chunk: break output += clean(chunk) # a command just finished, give tcsh a moment to enable its line editor again while select.select([fd], [], [], 0.2)[0]: output += clean(os.read(fd, 1 << 16)) return output candidates = [] try: # tcsh echoes what is typed, so the command setting the prompt must not contain it verbatim os.write( fd, f'set autolist\nsource {script}\nset prompt="{prompt[:1]}""{prompt[1:]}"\n'.encode()) read(1) for cmdline in cmdlines: seen = len(output) # `autolist` lists the candidates, ^U then discards the line, \n asks for a new prompt os.write(fd, cmdline.encode() + b"\t\x15\n") chunk = read(output.count(prompt) + 1)[seen:].replace(prompt, "\n") # drop the echoed command line & the re-drawn prompt, leaving the listed candidates candidates.append([ word for line in chunk.splitlines() if line.strip() and cmdline.strip() not in line for word in line.split()]) finally: os.close(fd) os.kill(pid, signal.SIGKILL) os.waitpid(pid, 0) return candidates @pytest.fixture(scope="module") def test_parser(): # NOTE: `prog="test"` fails on fish<4 due to autoloading of builtin `complete -c test -e` parser = ArgumentParser(prog="myprog", description="%(prog)s (root) description") parser.add_argument("--repo", "-r", help="repository to use") parser.add_argument("--hidden-opt", help=SUPPRESS) subparsers = parser.add_subparsers(dest="cmd") create = subparsers.add_parser("create", help="create something") create.add_argument("--exclude-from", help="exclude patterns file", metavar="excl").complete = shtab.FILE create.add_argument("name", choices=["alpha", "beta"], help="name of the thing to create") create.add_argument("paths", nargs="*", help="paths to add").complete = shtab.FILE delete = subparsers.add_parser("delete", help="delete something", description="%(prog)s desc") delete.add_argument("--force", action="store_true", help="force deletion") delete.add_argument("name", help="name of the thing to delete") subparsers.add_parser("list", help="%(prog)s") # forward-compat with python/cpython#67037 hidden = subparsers.add_parser("hidden", help=SUPPRESS) hidden.add_argument("--hidden", help="should be hidden") return parser @fix_shell def test_file_completion(shell, change_dir, test_parser): (change_dir / "test_file.txt").touch() completion = complete(test_parser, shell) if shell == 'bash': shell = Bash(completion) shell.test('"$($_shtab_myprog_create_pos_1_COMPGEN test_)" = test_file.txt') elif shell == 'zsh': specs = zsh_specs(completion, "_shtab_myprog_create_options", change_dir) assert "(*)::paths to add:_files" in specs elif shell == 'tcsh': command_lines = ["myprog create --exclude-from ", "myprog --repo test_"] candidates = tcsh_candidates(completion, command_lines, change_dir) assert candidates == [["completion.tcsh", "test_file.txt"], []] elif shell == 'fish': assert fish_candidates(completion, "myprog create alpha test_") == ["test_file.txt"] assert fish_candidates(completion, "myprog create --exclude-from ") == ["test_file.txt"] assert not fish_candidates(completion, "myprog delete test_") assert not fish_candidates(completion, "myprog --repo test_") (change_dir / "subdir").mkdir() (change_dir / "subdir" / "nested.txt").touch() assert fish_candidates(completion, "myprog create alpha subdir/nes") == ["subdir/nested.txt"] absolute = change_dir / "subdir" / "nes" candidates = fish_candidates(completion, f"myprog create --exclude-from {absolute}") assert candidates[0].endswith("subdir/nested.txt") assert len(candidates) == 1 elif shell == 'powershell': assert powershell_candidates(completion, "myprog create alpha test_") == ["test_file.txt"] assert powershell_candidates(completion, "myprog create --exclude-from ") == ["test_file.txt"] assert not powershell_candidates(completion, "myprog delete test_") assert not powershell_candidates(completion, "myprog --repo test_") (change_dir / "subdir").mkdir() (change_dir / "subdir" / "nested.txt").touch() assert powershell_candidates(completion, "myprog create alpha subdir/nes") == ["subdir/nested.txt"] assert powershell_candidates(completion, "myprog create alpha subdir/") == ["subdir/nested.txt"] absolute = change_dir / "subdir" / "nes" candidates = powershell_candidates(completion, f"myprog create --exclude-from {absolute}") assert candidates[0].endswith(f"subdir{os.sep}nested.txt") assert len(candidates) == 1 else: raise NotImplementedError(completion) def bash_candidates(completion, cmdlines, cwd): """ Return pty-driven completion candidates for each of `cmdlines`. Reason: readline's post-processing (e.g. `-o filenames` appending `/` to dirs) happens after `COMPREPLY` and is thus invisible to `compgen`-based tests. """ pty = pytest.importorskip('pty') if not shutil.which('bash'): pytest.skip("bash not available") script = cwd / "completion.bash" script.write_text(completion) prompt = "|candidates|" pid, fd = pty.fork() if pid == 0: # child os.chdir(cwd) os.environ['TERM'] = 'dumb' os.environ['COLUMNS'] = '999' os.execvp('bash', ['bash', '--norc', '--noprofile', '-i']) output = "" def clean(chunk): return re.sub(r"\x1b\[[0-9;?]*[A-Za-z]|[\a\r\b]", "", chunk.decode('utf-8', 'replace')) def read(condition, timeout=10.0): """read until `timeout` elapses or `condition()`, then drain""" nonlocal output deadline = time.time() + timeout while not condition() and time.time() < deadline: if select.select([fd], [], [], 0.1)[0]: try: chunk = os.read(fd, 1 << 16) except OSError: break if not chunk: break output += clean(chunk) while select.select([fd], [], [], 0.3)[0]: try: chunk = os.read(fd, 1 << 16) except OSError: break if not chunk: break output += clean(chunk) candidates = [] try: os.write(fd, f"source {script}; PS1='{prompt}'\n".encode()) read(lambda: output.count(prompt) >= 1) for cmdline in cmdlines: seen = len(output) os.write(fd, cmdline.encode() + b"\t") # the typed line is echoed back, followed by whatever readline inserted read(lambda cmdline=cmdline, seen=seen: cmdline in output[seen:]) candidates.append(output[seen:]) os.write(fd, b"\x15exit\n") # discard the line & quit read(lambda: True) finally: os.close(fd) os.kill(pid, signal.SIGKILL) os.waitpid(pid, 0) return candidates def test_bash_dir_collision(change_dir, test_parser): """Subcommands matching a dir name must not gain a trailing slash""" try: subprocess.check_call(['bash', '-c', 'type compopt']) except subprocess.CalledProcessError: pytest.skip("bash without compopt") (change_dir / "create").mkdir() (change_dir / "subdir").mkdir() completion = complete(test_parser, 'bash') assert "complete -F _shtab_myprog myprog" in completion, \ "`-o filenames` must not apply readline filename post-processing globally" lines = bash_candidates(completion, ["myprog cre", "myprog create alpha sub"], change_dir) assert lines == [ "myprog create ", # choices should take priority over dirs "myprog create alpha subdir/", # dirs should still have trailing `/` ] def test_powershell_single_token(test_parser, change_dir): completion = complete(test_parser, 'powershell') assert powershell_candidates(completion, "myprog --") == ["--help", "--repo"] assert powershell_candidates(completion, "myprog c") == ["create"] def test_fish_global_option_value(test_parser): """Subcommands complete after `--global-opt value` (#228)""" completion = complete(test_parser, 'fish') candidates = fish_candidates(completion, "myprog --repo x ") assert {"create", "delete", "list"} <= set(candidates) def test_fish_value_equal_to_command_name(test_parser): """Values matching command names must not confuse (#229)""" completion = complete(test_parser, 'fish') # `list` is the value of `delete`'s `name` positional, not the `list` subcommand candidates = fish_candidates(completion, "myprog delete list --") assert "--force" in candidates assert "--short" not in candidates def test_fish_positional_order(test_parser): """Positionals are completed at the right slot (#230)""" completion = complete(test_parser, 'fish') # first slot offers the `name` choices assert {"alpha", "beta"} <= set(fish_candidates(completion, "myprog create ")) # later slots (the `paths` positional) must not re-offer them assert "alpha" not in fish_candidates(completion, "myprog create alpha alp") @fix_shell def test_placeholder_help_expansion(shell): if shell in ('bash', 'tcsh'): pytest.skip("WiP") parser = ArgumentParser(prog="test") parser.add_argument("--retries", type=int, default=3, help="retries (default %(default)s)") parser.add_argument("posA", choices=["one", "two"], help="also %(prog)s thing") completion = complete(parser, shell) assert "retries (default 3)" in completion assert "also test thing" in completion def test_fish_subcommand_description(): """add_parser(help=...) is used when there is no description (#231)""" parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() subparsers.add_parser("subA", help="help message") subparsers.add_parser("subB", description="description wins\nsecond line", help="unused") completion = complete(parser, 'fish') assert "-a subA -d 'help message'" in completion assert "-a subB -d 'description wins second line'" in completion def test_fish_choice_flags(): parser = ArgumentParser(prog="test") parser.add_argument("--cust", help="custom").complete = {'bash': "_some_func"} parser.add_argument("--fmt", choices=["json", "csv"], help="format") parser.add_argument("posB", choices=["one", "two"], help="a word") completion = complete(parser, 'fish') assert "-l cust -x -d custom" in completion assert '-l fmt -xka "json csv" -d format' in completion assert '-ka "one two" -d \'a word\'' in completion @pytest.fixture def tcsh_pattern_parser(): """Two subcommands sharing positional slot 2, one of them `.complete`-ing a pattern.""" parser = ArgumentParser(prog="myprog") parser.add_argument("--conf", help="config file") subparsers = parser.add_subparsers() build = subparsers.add_parser("build", help="build") build.add_argument("cfg", help="config").complete = shtab.glob("*.yml", "*.yaml") build.add_argument("stage", choices=["dev", "prod"], help="stage") run = subparsers.add_parser("run", help="run") run.add_argument("mode", choices=["fast", "slow"], help="mode") run.add_argument("target", choices=["all", "one"], help="target") return parser def test_tcsh_slot_after_subcommand(tcsh_pattern_parser): """A slot directly following a (sub)command is keyed off that word (#236)""" completion = complete(tcsh_pattern_parser, 'tcsh') assert "'n/build/f:{*.yml,*.yaml}/'" in completion assert "f:{*.yml,*.yaml}`" not in completion and ") f:" not in completion assert "'n/run/(fast slow)/'" in completion # a slot further away from its (sub)command can only be keyed off its index assert '("$cmd[2]" == "run") ) echo all one' in completion assert '("$cmd[2]" == "build") ) echo dev prod' in completion def test_tcsh_pattern_completion(tcsh_pattern_parser, change_dir): """The generated pattern rule works in tcsh itself (#236)""" for name in ("app.yml", "conf.yaml", "notes.md"): (change_dir / name).touch() completion = complete(tcsh_pattern_parser, 'tcsh') cmdlines = [ "myprog build ", "myprog run ", "myprog ", "myprog --conf c.yml build ", "myprog --conf c.yml run "] yml, fast_slow = ["app.yml", "conf.yaml"], ["fast", "slow"] assert tcsh_candidates(completion, cmdlines, change_dir) == [yml, fast_slow, ["build", "run"], yml, fast_slow] @fix_shell def test_subparser_custom_complete(shell): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() sub = subparsers.add_parser("sub", help="help message") sub.add_argument("posA").complete = { 'bash': "_shtab_test_some_func", 'fish': "(_shtab_test_some_func)", 'preamble': { 'bash': "_shtab_test_some_func() { compgen -W 'one two' -- $1 ;}", 'fish': "function _shtab_test_some_func\n printf '%s\\n' one two\nend"}} completion = complete(parser, shell) if shell == 'bash': shell = Bash(completion) shell.compgen('-W "${_shtab_test_subparsers[*]}"', "s", "sub") shell.compgen('-W "$_shtab_test_pos_0_choices"', "s", "sub") shell.test('"$($_shtab_test_sub_pos_0_COMPGEN o)" = "one"') shell.test('-z "${_shtab_test_COMPGEN-}"') elif shell == 'fish': assert fish_candidates(completion, "test sub o") == ["one"] else: pytest.skip("WiP") @fix_shell def test_subparser_aliases(shell): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() sub = subparsers.add_parser("sub", aliases=["xsub", "ysub"], help="help message") sub.add_argument("posA").complete = { 'bash': "_shtab_test_some_func", 'preamble': {'bash': "_shtab_test_some_func() { compgen -W 'one two' -- $1 ;}"}} completion = complete(parser, shell) if shell == 'bash': shell = Bash(completion) shell.compgen('-W "${_shtab_test_subparsers[*]}"', "s", "sub") shell.compgen('-W "${_shtab_test_pos_0_choices[*]}"', "s", "sub") shell.compgen('-W "${_shtab_test_subparsers[*]}"', "x", "xsub") shell.compgen('-W "${_shtab_test_pos_0_choices[*]}"', "x", "xsub") shell.compgen('-W "${_shtab_test_subparsers[*]}"', "y", "ysub") shell.compgen('-W "${_shtab_test_pos_0_choices[*]}"', "y", "ysub") shell.test('"$($_shtab_test_sub_pos_0_COMPGEN o)" = "one"') shell.test('-z "${_shtab_test_COMPGEN-}"') else: pytest.skip("WiP") @fix_shell def test_subparser_colons(shell): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() subparsers.add_parser("sub:cmd", help="help message") completion = complete(parser, shell) if shell == 'bash': shell = Bash(completion) shell.compgen('-W "${_shtab_test_subparsers[*]}"', "s", "sub:cmd") shell.compgen('-W "${_shtab_test_pos_0_choices[*]}"', "s", "sub:cmd") shell.test('-z "${_shtab_test_COMPGEN-}"') else: pytest.skip("WiP") @fix_shell def test_subparser_slashes(shell): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() subparsers.add_parser("sub/cmd", help="help message") completion = complete(parser, shell) if shell == 'bash': shell = Bash(completion) shell.compgen('-W "${_shtab_test_subparsers[*]}"', "s", "sub/cmd") shell.compgen('-W "${_shtab_test_pos_0_choices[*]}"', "s", "sub/cmd") shell.test('-z "${_shtab_test_COMPGEN-}"') elif shell == 'zsh': assert "_shtab_test_sub/cmd" not in completion assert "_shtab_test_sub_cmd" in completion else: pytest.skip("WiP") @fix_shell def test_add_argument_to_optional(shell): parser = ArgumentParser(prog="test") shtab.add_argument_to(parser, ["-s", "--shell"]) completion = complete(parser, shell) if shell == 'bash': shell = Bash(completion) shell.compgen('-W "${_shtab_test_option_strings[*]}"', "--s", "--shell") else: pytest.skip("WiP") def test_add_argument_to_group_typing(): parser = ArgumentParser(prog="test") group = parser.add_argument_group("completion") assert shtab.add_argument_to(group) is group, "should return same object" @fix_shell def test_add_argument_to_positional(shell, capsys): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() sub = subparsers.add_parser("completion", help="help message") shtab.add_argument_to(sub, "shell", parent=parser) from argparse import Namespace completion_manual = complete(parser, shell) assert completion_manual.rstrip() == capsys.readouterr().out.rstrip() with pytest.raises(SystemExit) as exc: sub._actions[-1](sub, Namespace(), shell) assert exc.type is SystemExit and exc.value.code == 0 completion, err = capsys.readouterr() assert completion_manual.rstrip() == completion.rstrip() assert not err if shell == 'bash': shell = Bash(completion) shell.compgen('-W "${_shtab_test_subparsers[*]}"', "c", "completion") shell.compgen('-W "${_shtab_test_pos_0_choices[*]}"', "c", "completion") shell.compgen('-W "${_shtab_test_completion_pos_0_choices[*]}"', "ba", "bash") shell.compgen('-W "${_shtab_test_completion_pos_0_choices[*]}"', "z", "zsh") else: pytest.skip("WiP") @fix_shell def test_get_completer(shell): shtab.get_completer(shell) @pytest.mark.parametrize("quote", ["'"] + list(map(chr, range(0x2018, 0x201c)))) def test_powershell_escape_quotes(quote): assert shtab._powershell_escape(f"a{quote}b") == f"'a{quote}{quote}b'" def test_get_completer_invalid(): try: shtab.get_completer("invalid") except NotImplementedError: pass else: raise NotImplementedError("invalid") @pytest.fixture def change_dir(tmp_path): original_cwd = os.getcwd() os.chdir(tmp_path) yield tmp_path os.chdir(original_cwd) @fix_shell def test_path_completion_after_redirection(change_dir, shell): if shell != 'bash': pytest.skip("WiP") parser = ArgumentParser(prog="test") shtab.add_argument_to(parser, ["-s", "--shell"]) completion = complete(parser, 'bash') (change_dir / "test_file.txt").touch() for redirection in [">", ">>", "1>", "1>>", "2>", "2>>"]: shell = Bash(completion + f"\nCOMP_WORDS=(test '{redirection}' tes); COMP_CWORD=2; _shtab_test;") shell.test('"${COMPREPLY[@]}" = "test_file.txt"', f"Redirection {redirection} failed") def test_bash_path_completion_in_middle_of_command(change_dir): """A path option still completes when the next option is already typed.""" parser = ArgumentParser(prog="test") parser.add_argument("-d", "--directory").complete = shtab.DIRECTORY parser.add_argument("-w", "--wdir") completion = shtab.complete(parser, shell="bash") (change_dir / "folder").mkdir() shell = Bash(completion + "\nCOMP_WORDS=(test -d -w); COMP_CWORD=2; _shtab_test;") shell.test('"${COMPREPLY[@]}" = "folder"') def test_bash_option_completion_after_positional_path(): parser = ArgumentParser(prog="test") parser.add_argument("path").complete = shtab.DIRECTORY parser.add_argument("-w", "--wdir") completion = shtab.complete(parser, shell="bash") shell = Bash(completion + "\nCOMP_WORDS=(test . --w); COMP_CWORD=2; _shtab_test;") shell.test('"${COMPREPLY[*]}" = "--wdir"') @fix_shell def test_fallbacks(shell, test_parser): completion = complete(test_parser, shell) assert "hidden" not in completion assert "delete something" not in completion, "expected description to override help" if shell == 'zsh': assert '"--exclude-from[exclude patterns file]:excl:_files"' in completion, ( "expected metavar: excl") assert "create:'create something'" in completion assert "delete:'myprog delete desc'" in completion assert "list:'myprog list'" in completion elif shell == 'fish': assert "-a create -d 'create something'" in completion assert "-a delete -d 'myprog delete desc'" in completion assert "-a list -d 'myprog list'" in completion