././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1709837457.0221002 shtab-1.7.1/0000755000175100001770000000000014572406221012231 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1709837457.0141 shtab-1.7.1/.github/0000755000175100001770000000000014572406221013571 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1709837457.0141 shtab-1.7.1/.github/workflows/0000755000175100001770000000000014572406221015626 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/.github/workflows/test.yml0000644000175100001770000000431714572406201017333 0ustar00runnerdockername: Test 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) name: Test py${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: python: [3.7, 3.11] steps: - uses: actions/checkout@v4 with: {fetch-depth: 0} - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Install run: pip install -U -e .[dev] - run: pytest - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} deploy: needs: test name: PyPI Deploy environment: pypi permissions: contents: write id-token: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: {fetch-depth: 0} - uses: actions/setup-python@v5 with: {python-version: '3.x'} - id: dist uses: casperdcl/deploy-pypi@v2 with: build: true upload: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') name: Release 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: ${{ github.token }} - name: 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: Olivaw[bot] email: 64868532+iterative-olivaw@users.noreply.github.com ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/.gitignore0000644000175100001770000000031114572406201014212 0ustar00runnerdocker*.py[cod] __pycache__/ # Packages /MANIFEST.in /*.egg*/ /shtab/_dist_ver.py /build/ /dist/ /docs/build/ /docs/contributing.md # Unit test / coverage reports /.coverage* /coverage.xml /.pytest_cache/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/.pre-commit-config.yaml0000644000175100001770000000251714572406201016515 0ustar00runnerdockerdefault_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.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|.github/workflows/test.yml)$ - repo: https://github.com/PyCQA/flake8 rev: 7.0.0 hooks: - id: flake8 args: [-j8] additional_dependencies: - flake8-broken-line - flake8-bugbear - flake8-comprehensions - flake8-debugger - flake8-isort - flake8-pyproject - flake8-string-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.8.0 hooks: - id: mypy additional_dependencies: [types-setuptools] - repo: https://github.com/google/yapf rev: v0.40.2 hooks: - id: yapf args: [-i] additional_dependencies: [toml] - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - id: isort ci: autoupdate_schedule: monthly skip: [flake8] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/CONTRIBUTING.md0000644000175100001770000000321114572406201014455 0ustar00runnerdocker# Contributing ## Tests When contributing pull requests, it's a good idea to run basic checks locally: ```bash # install development dependencies shtab (main)$ pip install pre-commit -e .[dev] shtab (main)$ pre-commit install # install pre-commit checks shtab (main)$ pytest # run all tests ``` ## 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()` - ... - `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 had, should be fast. Helper functions such as `replace_format` allows use of curly braces `{}` in string snippets without clashing 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/LICENCE0000644000175100001770000000107014572406201013212 0ustar00runnerdockerCopyright 2020-2023 Casper da Costa-Luis Licensed under the Apache Licence, Version 2.0 (the "Licence"); you may not use this project except in compliance with the Licence. You may obtain a copy of the Licence at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licence for the specific language governing permissions and limitations under the Licence. ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1709837457.0181 shtab-1.7.1/PKG-INFO0000644000175100001770000001622514572406221013334 0ustar00runnerdockerMetadata-Version: 2.1 Name: shtab Version: 1.7.1 Summary: Automagic shell tab completion for Python CLI applications Author-email: Casper da Costa-Luis Maintainer-email: Iterative License: Apache-2.0 Project-URL: documentation, https://docs.iterative.ai/shtab Project-URL: repository, https://github.com/iterative/shtab Project-URL: changelog, https://github.com/iterative/shtab/releases Keywords: tab,complete,completion,shell,bash,zsh,argparse 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: License :: OSI Approved :: Apache Software License 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.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation Classifier: Programming Language :: Python :: Implementation :: IronPython Classifier: Programming Language :: Python :: Implementation :: PyPy 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.7 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: pytest-timeout; extra == "dev" |Logo| shtab ===== |PyPI-Downloads| |Tests| |Coverage| |PyPI| |Conda| - 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 - ``bash`` - ``zsh`` - ``tcsh`` - Supports - `argparse `_ - `docopt `_ (via `argopt `_) - Supports arguments, options and subparsers - Supports choices (e.g. ``--say={hello,goodbye}``) - Supports file and directory path completion - Supports custom path completion (e.g. ``--file={*.txt}``) ------------------------------------------ .. 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}`` - subparser mode: end-users execute ``your_cli_app completion {bash,zsh,tcsh}`` Examples -------- See `the docs for usage examples `_. FAQs ---- Not working? Check out `frequently asked questions `_. Alternatives ------------ - `argcomplete `_ - executes the underlying script *every* time ```` is pressed (slow and has side-effects) - only provides ``bash`` completion - `pyzshcomplete `_ - executes the underlying script *every* time ```` is pressed (slow and has side-effects) - only provides ``zsh`` completion - `click `_ - different framework completely replacing the builtin ``argparse`` - solves multiple problems (rather than POSIX-style "do one thing well") Contributions ------------- Please do open `issues `_ & `pull requests `_! Some ideas: - support ``fish`` - support ``powershell`` See `CONTRIBUTING.md `_ for more guidance. |Hits| .. |Logo| image:: https://github.com/iterative/shtab/raw/main/meta/logo.png .. |Tests| image:: https://img.shields.io/github/actions/workflow/status/iterative/shtab/test.yml?logo=github&label=tests :target: https://github.com/iterative/shtab/actions :alt: Tests .. |Coverage| image:: https://codecov.io/gh/iterative/shtab/branch/main/graph/badge.svg :target: https://codecov.io/gh/iterative/shtab :alt: Coverage .. |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://img.shields.io/pypi/dm/shtab.svg?label=pypi%20downloads&logo=PyPI&logoColor=white :target: https://pepy.tech/project/shtab :alt: Downloads .. |Hits| image:: https://caspersci.uk.to/cgi-bin/hits.cgi?q=shtab&style=social&r=https://github.com/iterative/shtab&a=hidden :target: https://caspersci.uk.to/cgi-bin/hits.cgi?q=shtab&a=plot&r=https://github.com/iterative/shtab&style=social :alt: Hits ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/README.rst0000644000175100001770000001021714572406201013717 0ustar00runnerdocker|Logo| shtab ===== |PyPI-Downloads| |Tests| |Coverage| |PyPI| |Conda| - 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 - ``bash`` - ``zsh`` - ``tcsh`` - Supports - `argparse `_ - `docopt `_ (via `argopt `_) - Supports arguments, options and subparsers - Supports choices (e.g. ``--say={hello,goodbye}``) - Supports file and directory path completion - Supports custom path completion (e.g. ``--file={*.txt}``) ------------------------------------------ .. 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}`` - subparser mode: end-users execute ``your_cli_app completion {bash,zsh,tcsh}`` Examples -------- See `the docs for usage examples `_. FAQs ---- Not working? Check out `frequently asked questions `_. Alternatives ------------ - `argcomplete `_ - executes the underlying script *every* time ```` is pressed (slow and has side-effects) - only provides ``bash`` completion - `pyzshcomplete `_ - executes the underlying script *every* time ```` is pressed (slow and has side-effects) - only provides ``zsh`` completion - `click `_ - different framework completely replacing the builtin ``argparse`` - solves multiple problems (rather than POSIX-style "do one thing well") Contributions ------------- Please do open `issues `_ & `pull requests `_! Some ideas: - support ``fish`` - support ``powershell`` See `CONTRIBUTING.md `_ for more guidance. |Hits| .. |Logo| image:: https://github.com/iterative/shtab/raw/main/meta/logo.png .. |Tests| image:: https://img.shields.io/github/actions/workflow/status/iterative/shtab/test.yml?logo=github&label=tests :target: https://github.com/iterative/shtab/actions :alt: Tests .. |Coverage| image:: https://codecov.io/gh/iterative/shtab/branch/main/graph/badge.svg :target: https://codecov.io/gh/iterative/shtab :alt: Coverage .. |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://img.shields.io/pypi/dm/shtab.svg?label=pypi%20downloads&logo=PyPI&logoColor=white :target: https://pepy.tech/project/shtab :alt: Downloads .. |Hits| image:: https://caspersci.uk.to/cgi-bin/hits.cgi?q=shtab&style=social&r=https://github.com/iterative/shtab&a=hidden :target: https://caspersci.uk.to/cgi-bin/hits.cgi?q=shtab&a=plot&r=https://github.com/iterative/shtab&style=social :alt: Hits ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1709837457.0181 shtab-1.7.1/docs/0000755000175100001770000000000014572406221013161 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/docs/index.md0000644000175100001770000001237714572406201014622 0ustar00runnerdocker![shtab](https://static.iterative.ai/img/shtab/banner.png) [![Downloads](https://img.shields.io/pypi/dm/shtab.svg?label=pypi%20downloads&logo=PyPI&logoColor=white)](https://pepy.tech/project/shtab) [![Tests](https://img.shields.io/github/actions/workflow/status/iterative/shtab/test.yml?logo=github&label=tests)](https://github.com/iterative/shtab/actions) [![Coverage](https://codecov.io/gh/iterative/shtab/branch/main/graph/badge.svg)](https://codecov.io/gh/iterative/shtab) [![PyPI](https://img.shields.io/pypi/v/shtab.svg?label=pip&logo=PyPI&logoColor=white)](https://pypi.org/project/shtab) [![conda-forge](https://img.shields.io/conda/v/conda-forge/shtab.svg?label=conda&logo=conda-forge)](https://anaconda.org/conda-forge/shtab) - 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 - `bash` - `zsh` - `tcsh` - Supports - [argparse](https://docs.python.org/library/argparse) - [docopt](https://pypi.org/project/docopt) (via [argopt](https://pypi.org/project/argopt)) - Supports arguments, options and subparsers - Supports choices (e.g. `--say={hello,goodbye}`) - Supports file and directory path completion - Supports custom path completion (e.g. `--file={*.txt}`) ------------------------------------------------------------------------ ## Installation === "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 Not working? - Make sure that `shtab` and the application you're trying to complete are both accessible from your environment. - Make sure that `prog` is set: - if using [`options.entry_points.console_scripts=MY_PROG=...`](https://setuptools.pypa.io/en/latest/userguide/entry_point.html), then ensure the main parser's `prog` matches `argparse.ArgumentParser(prog="MY_PROG")` 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 `argparse.ArgumentParser(prog="MY_PROG.py")` or override it using `shtab MY_PROG.get_main_parser --prog=MY_PROG.py`. - Make sure that all arguments have `help` messages (`parser.add_argument('positional', help="documented; i.e. not hidden")`). - [Ask a general question on StackOverflow](https://stackoverflow.com/questions/tagged/shtab). - [Report bugs and open feature requests on GitHub][GH-issue]. "Eager" installation (completions are re-generated upon login/terminal start) is recommended. Naturally, `shtab` and the CLI application to complete should be accessible/importable from the login environment. If installing `shtab` in a different virtual environment, you'd have to add a line somewhere appropriate (e.g. `$CONDA_PREFIX/etc/conda/activate.d/env_vars.sh`). By default, `shtab` will silently do nothing if it cannot import the requested application. Use `-u, --error-unimportable` to noisily complain. ## Alternatives - [argcomplete](https://pypi.org/project/argcomplete) - executes the underlying script *every* time `` is pressed (slow and has side-effects) - only provides `bash` completion - [pyzshcomplete](https://pypi.org/project/pyzshcomplete) - executes the underlying script *every* time `` is pressed (slow and has side-effects) - only provides `zsh` completion - [click](https://pypi.org/project/click) - different framework completely replacing the builtin `argparse` - solves multiple problems (rather than POSIX-style "do one thing well") ## Contributions Please do open [issues][GH-issue] & [pull requests][GH-pr]! Some ideas: - support `fish` - support `powershell` See [CONTRIBUTING.md](https://github.com/iterative/shtab/tree/main/CONTRIBUTING.md) for more guidance. [![Hits](https://caspersci.uk.to/cgi-bin/hits.cgi?q=shtab&style=social&r=https://github.com/iterative/shtab&a=hidden)](https://caspersci.uk.to/cgi-bin/hits.cgi?q=shtab&a=plot&r=https://github.com/iterative/shtab&style=social) [GH-issue]: https://github.com/iterative/shtab/issues [GH-pr]: https://github.com/iterative/shtab/pulls ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/docs/pydoc-markdown.yml0000644000175100001770000000442414572406201016644 0ustar00runnerdockerloaders: - type: python search_path: [..] processors: - type: filter - type: pydoc_markdown_shtab.ShtabProcessor - type: crossref hooks: pre-render: - sed 's#](./#](https://github.com/iterative/shtab/tree/main/#g' ../CONTRIBUTING.md > contributing.md renderer: type: mkdocs markdown: source_linker: type: github repo: iterative/shtab root: .. 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.complete, shtab.add_argument_to] - title: External Links children: - title: Source Code href: https://github.com/iterative/shtab - title: Changelog href: https://github.com/iterative/shtab/releases - title: Issues href: https://github.com/iterative/shtab/issues?q= - title: Contributing name: contributing source: contributing.md - title: Licence name: licence source: ../LICENCE mkdocs_config: site_name: shtab documentation site_description: Automagic shell tab completion for Python CLI applications site_url: https://docs.iterative.ai/shtab/ site_author: Iterative repo_name: iterative/shtab repo_url: https://github.com/iterative/shtab copyright: | © Casper da Costa-Luis @casperdcl 2021 extra: generator: false theme: name: material logo: https://github.com/iterative/shtab/raw/main/meta/logo.png favicon: https://github.com/iterative/shtab/raw/main/meta/logo.png 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/docs/pydoc_markdown_shtab.py0000644000175100001770000000135414572406201017735 0ustar00runnerdockerimport re from pydoc_markdown.contrib.processors.pydocmd import PydocmdProcessor class ShtabProcessor(PydocmdProcessor): def _process(self, node): if not getattr(node, "docstring", None): return super()._process(node) # convert parameter lists to markdown list node.docstring.content = re.sub(r"^(\w+)(:.*?)$", r"* __\1__\2", node.docstring.content, flags=re.M) # fix code cross-references node.docstring.content = re.sub(r"<../(\S+)>", r"[\1](https://github.com/iterative/shtab/tree/main/\1)", node.docstring.content, flags=re.M) return super()._process(node) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/docs/requirements.txt0000644000175100001770000000022714572406201016444 0ustar00runnerdockermkdocs-material git+https://github.com/tqdm/jsmin@python3-only#egg=jsmin mkdocs-minify-plugin pydoc-markdown>=4,!=4.8.0 pygments pymdown-extensions>=9 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/docs/use.md0000644000175100001770000001464314572406201014305 0ustar00runnerdocker# 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}` - subparser mode: end-users execute `your_cli_app completion {bash,zsh,tcsh}` ## 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. Once that's done, simply put the output of `shtab --shell=your_shell your_cli_app.your_parser_object` somewhere 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. === "bash" ```sh shtab --shell=bash shtab.main.get_main_parser --error-unimportable \ | sudo tee "$BASH_COMPLETION_COMPAT_DIR"/shtab ``` === "Eager bash" There are a few options: ```sh # Install locally echo 'eval "$(shtab --shell=bash shtab.main.get_main_parser)"' \ >> ~/.bash_completion # Install locally (lazy load for bash-completion>=2.8) echo 'eval "$(shtab --shell=bash shtab.main.get_main_parser)"' \ > "${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions/shtab" # Install system-wide echo 'eval "$(shtab --shell=bash shtab.main.get_main_parser)"' \ | sudo tee "$(pkg-config --variable=completionsdir bash-completion)"/shtab # Install system-wide (legacy) echo 'eval "$(shtab --shell=bash shtab.main.get_main_parser)"' \ | sudo tee "$BASH_COMPLETION_COMPAT_DIR"/shtab ``` === "zsh" Note that `zsh` requires completion script files to be named `_{EXECUTABLE}` (with an underscore prefix). ```sh # note the underscore `_` prefix shtab --shell=zsh shtab.main.get_main_parser --error-unimportable \ | sudo tee /usr/local/share/zsh/site-functions/_shtab ``` === "Eager zsh" To be more 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 > ~/.zsh/completions/_shtab ``` === "tcsh" ```sh shtab --shell=tcsh shtab.main.get_main_parser --error-unimportable \ | sudo tee /etc/profile.d/shtab.completion.csh ``` === "Eager tcsh" There are a few options: ```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/eager-completion.csh ``` !!! tip See the [examples/](https://github.com/iterative/shtab/tree/main/examples) folder for more. 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.command.main`, simply run: === "bash" ```sh shtab --shell=bash -u MY_PROG.command.main.get_main_parser \ | sudo tee "$BASH_COMPLETION_COMPAT_DIR"/MY_PROG ``` === "zsh" ```sh shtab --shell=zsh -u MY_PROG.command.main.get_main_parser \ | sudo tee /usr/local/share/zsh/site-functions/_MY_PROG ``` === "tcsh" ```sh shtab --shell=tcsh -u MY_PROG.command.main.get_main_parser \ | sudo tee /etc/profile.d/MY_PROG.completion.csh ``` ## Library Usage !!! tip See the [examples/](https://github.com/iterative/shtab/tree/main/examples) folder for more. Complex projects with subparsers and custom completions for paths matching certain patterns (e.g. `--file=*.txt`) are fully supported (see [examples/customcomplete.py](https://github.com/iterative/shtab/tree/main/examples/customcomplete.py) or even [iterative/dvc:commands/completion.py](https://github.com/iterative/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="7 9-10"} #!/usr/bin/env python import argparse import shtab # for completion magic def get_main_parser(): 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 return parser if __name__ == "__main__": parser = get_main_parser() args = parser.parse_args() print("received =%r --dir=%r" % (args.file, args.dir)) ``` === "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="17"} #!/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! if __name__ == "__main__": args = parser.parse_args() msg = "k thx bai!" if args.goodbye else "hai!" print("{} says '{}' to {}".format(args.me, msg, args.you)) ``` ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1709837457.0181 shtab-1.7.1/examples/0000755000175100001770000000000014572406221014047 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/examples/customcomplete.py0000755000175100001770000000417614572406201017475 0ustar00runnerdocker#!/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 import shtab # for completion magic TXT_FILE = { "bash": "_shtab_greeter_compgen_TXTFiles", "zsh": "_files -g '(*.txt|*.TXT)'", "tcsh": "f:*.txt"} PREAMBLE = { "bash": """ # $1=COMP_WORDS[1] _shtab_greeter_compgen_TXTFiles() { compgen -d -- $1 # recurse into subdirs compgen -f -X '!*?.txt' -- $1 compgen -f -X '!*?.TXT' -- $1 } """, "zsh": "", "tcsh": ""} def process(args): print( "received =%r [=%r] --input-file=%r --output-name=%r --hidden-opt=%r" % (args.input_txt, args.suffix, args.input_file, args.output_name, args.hidden_opt)) def get_main_parser(): main_parser = argparse.ArgumentParser(prog="customcomplete") subparsers = main_parser.add_subparsers() # make required (py3.7 API change); vis. https://bugs.python.org/issue16308 subparsers.required = True subparsers.dest = "subcommand" parser = subparsers.add_parser("completion", help="print tab completion") shtab.add_argument_to(parser, "shell", parent=main_parser, preamble=PREAMBLE) # magic! parser = subparsers.add_parser("process", help="parse files") # `*.txt` file tab completion parser.add_argument("input_txt", nargs='?').complete = TXT_FILE # file tab completion builtin shortcut parser.add_argument("-i", "--input-file").complete = shtab.FILE parser.add_argument( "-o", "--output-name", help=("output file name. Completes directory names to avoid users" " accidentally overwriting existing files."), ).complete = shtab.DIRECTORY # directory tab completion builtin shortcut parser.add_argument("suffix", choices=['json', 'csv'], default='json', nargs='?', help="Output format") parser.add_argument("--hidden-opt", action='store_true', help=argparse.SUPPRESS) parser.set_defaults(func=process) return main_parser if __name__ == "__main__": parser = get_main_parser() args = parser.parse_args() args.func(args) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/examples/docopt-greeter.py0000755000175100001770000000103014572406201017337 0ustar00runnerdocker#!/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! if __name__ == "__main__": args = parser.parse_args() msg = "k thx bai!" if args.goodbye else "hai!" print(f"{args.me} says '{msg}' to {args.you}") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/examples/pathcomplete.py0000755000175100001770000000135214572406201017110 0ustar00runnerdocker#!/usr/bin/env python """ `argparse`-based CLI app using `add_argument().complete = shtab.(FILE|DIR)` for file/dir tab completion. See `customcomplete.py` for a more advanced version. """ import argparse import shtab # for completion magic def get_main_parser(): 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 return parser if __name__ == "__main__": parser = get_main_parser() args = parser.parse_args() print(f"received ={args.file!r} --dir={args.dir!r}") ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1709837457.0181 shtab-1.7.1/meta/0000755000175100001770000000000014572406221013157 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/meta/logo.png0000644000175100001770000003742114572406201014632 0ustar00runnerdocker‰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Ú˜FE ˆòÕ “É 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ížfROÛ$&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þ´—.ÛdZDDõ|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²þý¬Ývvàô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"GLSðÔŠ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ÖùÄ=*Tsò‹ÿçjÛ÷ŸÔ½€IybË8Uê” ZDB–6_²Ù,§@I}(:h(Þ¿ë;–}—Å×»×À ®¤¡õJÚW^CçŠ-4·¯¸4LV`ï+ì¾ÉyrŠ»ùá´!q2†Ô¶ø¸ô¾¥”8Ud2ÐÂNmi÷@ËÄÉV§uº­1äSé)Î?Œ“ÑûH¦äž½ûXÓþtUp€@xí{‰4/'j@Ì“­òTjjVåÈñ‡þDm»ãþ} u‹·ó 0ITÐQú2ÒßÔD3š,ÉmÛŒ!¼Çf›Áµ\ñºÿNÇòÍ‹ ™‹HÉÄddJágT 99é—Àí/^ýϺÍul2é>ém4UdâtfB26FSûRo@‚•$d™“ #ǤË`Ï9íµoßt†æðaM-bÛ ¥cù–E„39ug6„ÊΈ„UScˆÜóíÈvCæ¸=¥ÛšÂÒjL*‰’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$Áˆ„+,ÕÕ¾OO62ëð"´@E„9 ~MnáX’`iÎsÝï((›©ËY†Y‹¯iGfí‹§.ÑZX*O•­æFÂ8ŽK ?W!É)Û‚Ž‡[  £Àí⹩t ¢jEN˜]5KÍDzSdŽ 0 %ñÄ«dZ¡šù_À:‹Ù€¸ «Æ¡ÌbIÛù|¹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¡¿ê,íqVmZ¯¹L*¥½<™&9©Aü4ƒŸvÀŠB2¾jX5G1 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òg7C4†éÙ±ž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ÊÊPNÓ 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`‚././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/pyproject.toml0000644000175100001770000001006614572406201015146 0ustar00runnerdocker[build-system] requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "shtab/_dist_ver.py" write_to_template = "__version__ = '{version}'\n" [tool.setuptools.packages.find] exclude = ["docs", "tests", "examples"] [tool.setuptools.exclude-package-data] "*" = ["logo.png"] [project.urls] documentation = "https://docs.iterative.ai/shtab" repository = "https://github.com/iterative/shtab" changelog = "https://github.com/iterative/shtab/releases" [project] name = "shtab" dynamic = ["version"] authors = [{name = "Casper da Costa-Luis", email = "casper.dcl@physics.org"}] maintainers = [{name = "Iterative", email = "support@iterative.ai"}] description = "Automagic shell tab completion for Python CLI applications" readme = "README.rst" requires-python = ">=3.7" keywords = ["tab", "complete", "completion", "shell", "bash", "zsh", "argparse"] license = {text = "Apache-2.0"} 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", "License :: OSI Approved :: Apache Software License", "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.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation", "Programming Language :: Python :: Implementation :: IronPython", "Programming Language :: Python :: Implementation :: PyPy", "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", "pytest-timeout"] [project.scripts] shtab = "shtab.main:main" [tool.flake8] max_line_length = 99 extend_ignore = ["E261", "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] profile = "black" line_length = 99 known_first_party = ["shtab", "tests"] [tool.mypy] warn_unused_configs = true warn_unused_ignores = true show_error_codes = true [tool.pytest.ini_options] minversion = "6.0" timeout = 5 log_level = "DEBUG" python_files = ["test_*.py"] testpaths = ["tests"] addopts = "-v --tb=short -rxs -W=error --durations=0 --cov=shtab --cov-report=term-missing --cov-report=xml" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1709837457.0221002 shtab-1.7.1/setup.cfg0000644000175100001770000000004614572406221014052 0ustar00runnerdocker[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1709837457.0181 shtab-1.7.1/shtab/0000755000175100001770000000000014572406221013332 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/shtab/__init__.py0000644000175100001770000007534214572406201015454 0ustar00runnerdockerimport logging import re from argparse import ( ONE_OR_MORE, REMAINDER, SUPPRESS, ZERO_OR_MORE, Action, ArgumentParser, _AppendAction, _AppendConstAction, _CountAction, _HelpAction, _StoreConstAction, _VersionAction, ) from collections import defaultdict from functools import total_ordering from itertools import starmap from string import Template from typing import Any, Dict, List from typing import Optional as Opt from typing import Union # version detector. Precedence: installed dist, git, 'UNKNOWN' try: from ._dist_ver import __version__ except ImportError: try: from setuptools_scm import get_version __version__ = get_version(root="..", relative_to=__file__) except (ImportError, LookupError): __version__ = "UNKNOWN" __all__ = ["complete", "add_argument_to", "SUPPORTED_SHELLS", "FILE", "DIRECTORY", "DIR"] log = logging.getLogger(__name__) SUPPORTED_SHELLS: List[str] = [] _SUPPORTED_COMPLETERS = {} CHOICE_FUNCTIONS: Dict[str, Dict[str, str]] = { "file": {"bash": "_shtab_compgen_files", "zsh": "_files", "tcsh": "f"}, "directory": {"bash": "_shtab_compgen_dirs", "zsh": "_files -/", "tcsh": "d"}} FILE = CHOICE_FUNCTIONS["file"] DIRECTORY = DIR = CHOICE_FUNCTIONS["directory"] FLAG_OPTION = ( _StoreConstAction, _HelpAction, _VersionAction, _AppendConstAction, _CountAction, ) 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: str): try: return _SUPPORTED_COMPLETERS[shell] except KeyError: supported = ",".join(SUPPORTED_SHELLS) raise NotImplementedError(f"shell ({shell}) must be in {supported}") @total_ordering class Choice: """ Placeholder to mark a special completion ``. >>> ArgumentParser.add_argument(..., choices=[Choice("")]) """ def __init__(self, choice_type: str, required: bool = False) -> None: """ See below for parameters. 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: """Example: `ArgumentParser.add_argument(..., choices=Optional.FILE)`.""" FILE = [Choice("file")] DIR = DIRECTORY = [Choice("directory")] class Required: """Example: `ArgumentParser.add_argument(..., choices=Required.FILE)`.""" FILE = [Choice("file", True)] DIR = DIRECTORY = [Choice("directory", True)] def complete2pattern(opt_complete, shell: str, choice_type2fn) -> str: return (opt_complete.get(shell, "") if isinstance(opt_complete, dict) else choice_type2fn[opt_complete]) def wordify(string: str) -> str: """Replace non-word chars [\\W] with underscores [_]""" return re.sub("\\W", "_", string) def get_public_subcommands(sub): """Get all the publicly-visible subcommands for a given subparser.""" public_parsers = {id(sub.choices[i.dest]) for i in sub._get_subactions()} return {k for k, v in sub.choices.items() if id(v) in public_parsers} 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) """ choice_type2fn = {k: v["bash"] for k, v in CHOICE_FUNCTIONS.items()} if choice_functions: choice_type2fn.update(choice_functions) def get_option_strings(parser): """Flattened list of all `parser`'s option strings.""" return sum( (opt.option_strings for opt in parser._get_optional_actions() if opt.help != SUPPRESS), [], ) def recurse(parser, prefix): """recurse through subparsers, appending to the return lists""" subparsers = [] option_strings = [] compgens = [] choices = [] nargs = [] # temp lists for recursion results sub_subparsers = [] sub_option_strings = [] sub_compgens = [] sub_choices = [] sub_nargs = [] # 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) compgens.append(f"{prefix}_pos_{i}_COMPGEN={comp_pattern}") if positional.choices: # choices (including subparsers & shtab `.complete` functions) log.debug(f"choices:{prefix}:{sorted(positional.choices)}") 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={choice_type2fn[choice.type]}") elif isinstance(positional.choices, dict): # subparser, so append to list of subparsers & recurse 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)) ( new_subparsers, new_option_strings, new_compgens, new_choices, new_nargs, ) = recurse( positional.choices[choice], f"{prefix}_{wordify(choice)}", ) sub_subparsers.extend(new_subparsers) sub_option_strings.extend(new_option_strings) sub_compgens.extend(new_compgens) sub_choices.extend(new_choices) sub_nargs.extend(new_nargs) else: log.debug("skip:subcommand:%s", choice) else: # simple choice this_positional_choices.append(str(choice)) if this_positional_choices: choices_str = "' '".join(this_positional_choices) choices.append(f"{prefix}_pos_{i}_choices=('{choices_str}')") # skip default `nargs` values if positional.nargs not in (None, "1", "?"): nargs.append(f"{prefix}_pos_{i}_nargs={positional.nargs}") if discovered_subparsers: subparsers_str = "' '".join(discovered_subparsers) subparsers.append(f"{prefix}_subparsers=('{subparsers_str}')") log.debug(f"subcommands:{prefix}:{discovered_subparsers}") # optional arguments options_strings_str = "' '".join(get_option_strings(parser)) option_strings.append(f"{prefix}_option_strings=('{options_strings_str}')") for optional in parser._get_optional_actions(): if optional == 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) compgens.append( f"{prefix}_{wordify(option_string)}_COMPGEN={comp_pattern_str}") if 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={func_str}") else: # simple choice this_optional_choices.append(str(choice)) if this_optional_choices: this_choices_str = "' '".join(this_optional_choices) choices.append( f"{prefix}_{wordify(option_string)}_choices=('{this_choices_str}')") # Check for nargs. if optional.nargs is not None and optional.nargs != 1: nargs.append(f"{prefix}_{wordify(option_string)}_nargs={optional.nargs}") # append recursion results subparsers.extend(sub_subparsers) option_strings.extend(sub_option_strings) compgens.extend(sub_compgens) choices.extend(sub_choices) nargs.extend(sub_nargs) return subparsers, option_strings, compgens, choices, nargs 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 = get_bash_commands( parser, root_prefix, choice_functions=choice_functions) # 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 `shtab` ${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 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 # "--" delimeter 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" != *"..." ]] && \\ (( $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 # "--" delimeter encountered fi let "word_index+=1" done # Generate the completions if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then # optional argument started: use option strings COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") ) else # use choices & compgen local IFS=$'\\n' # items may contain spaces, so delimit using newline COMPREPLY=( $([ -n "${current_action_compgen}" ] \\ && "${current_action_compgen}" "${completing_word}") ) unset IFS COMPREPLY+=( $(compgen -W "${current_action_choices[*]}" -- "${completing_word}") ) fi return 0 } complete -o filenames -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=("\n# Custom Preamble\n" + preamble + "\n# End Custom Preamble\n" if preamble else ""), root_prefix=root_prefix, prog=parser.prog, ) def escape_zsh(string): # excessive but safe return re.sub(r"([^\w\s.,()-])", r"\\\1", str(string)) @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 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 is_opt_end(opt): return isinstance(opt, OPTION_END) or opt.nargs == REMAINDER def is_opt_multiline(opt): return isinstance(opt, OPTION_MULTI) def format_optional(opt, parser): get_help = parser._get_formatter()._expand_help return (('{nargs}{options}"[{help}]"' if isinstance( opt, FLAG_OPTION) else '{nargs}{options}"[{help}]:{dest}:{pattern}"').format( nargs=('"(- : *)"' if is_opt_end(opt) else '"*"' if is_opt_multiline(opt) 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 ""), dest=opt.dest, pattern=complete2pattern(opt.complete, "zsh", choice_type2fn) if hasattr( opt, "complete") else (choice_type2fn[opt.choices[0].type] if isinstance(opt.choices[0], Choice) else "({})".format(" ".join(map(str, opt.choices)))) if opt.choices else "", ).replace('""', "")) def format_positional(opt, parser): get_help = parser._get_formatter()._expand_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.dest).strip().split("\n")[0]), pattern=complete2pattern(opt.complete, "zsh", choice_type2fn) if hasattr( opt, "complete") else (choice_type2fn[opt.choices[0].type] if isinstance(opt.choices[0], Choice) else "({})".format(" ".join(map(str, opt.choices)))) if opt.choices else "", ) # {cmd: {"help": help, "arguments": [arguments]}} all_commands = { root_prefix: { "cmd": prog, "arguments": [ format_optional(opt, parser) for opt in parser._get_optional_actions() if opt.help != SUPPRESS] + [ format_positional(opt, parser) for opt in parser._get_positional_actions() if opt.help != SUPPRESS and opt.choices is None], "help": (parser.description or "").strip().split("\n")[0], "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 sub.choices: continue if not sub.choices or not isinstance(sub.choices, dict): # positional argument all_commands[prefix]["arguments"].append(format_positional(sub, parser)) else: # subparser log.debug(f"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) # optionals arguments = [ format_optional(opt, parser) for opt in subparser._get_optional_actions() if opt.help != SUPPRESS] # positionals arguments.extend( format_positional(opt, parser) for opt in subparser._get_positional_actions() if not isinstance(opt.choices, dict) if opt.help != SUPPRESS) # help text formatter = subparser._get_formatter() backup_width = formatter._width formatter._width = 1234567 # large number to effectively disable wrapping desc = formatter._format_text(subparser.description or "").strip() formatter._width = backup_width new_pref = f"{prefix}_{wordify(cmd)}" options = all_commands[new_pref] = { "cmd": cmd, "help": desc.split("\n")[0], "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 f"""\ {prefix}() {{ local context state line curcontext="$curcontext" one_or_more='(-)*' remainder='(*)' if ((${{{prefix}_options[(I)${{(q)one_or_more}}*]}} + ${{{prefix}_options[(I)${{(q)remainder}}*]}} == 0)); then # noqa: E501 {prefix}_options+=(': :{prefix}_commands' '*::: :->{name}') 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 }} """ def command_option(prefix, options): arguments = "\n ".join(options["arguments"]) return f"""\ {prefix}_options=( {arguments} ) """ def command_list(prefix, options): name = " ".join([prog, *options["paths"]]) commands = "\n ".join(f'"{escape_zsh(cmd)}:{escape_zsh(opt["help"])}"' for cmd, opt in sorted(options["commands"].items())) return f""" {prefix}_commands() {{ local _commands=( {commands} ) _describe '{name} commands' _commands }}""" preamble = (f"""\ # Custom Preamble {preamble.rstrip()} # End Custom Preamble """ if preamble else "") # 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 `shtab` ${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=preamble, ) @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 = [] index_choices = defaultdict(dict) 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): if arg.choices: choice_strs = ' '.join(map(str, arg.choices)) yield f"'{arg_type}/{arg_sel}/({choice_strs})/'" elif hasattr(arg, 'complete'): complete_fn = complete2pattern(arg.complete, 'tcsh', choice_type2fn) if complete_fn: yield f"'{arg_type}/{arg_sel}/{complete_fn}/'" 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(): log.debug("%s| Optional: %s", log_prefix, optional.dest) if optional.help != SUPPRESS: # 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)) for positional in cparser._get_positional_actions(): if positional.help != SUPPRESS: positional_idx += 1 log.debug("%s| Positional #%d: %s", log_prefix, positional_idx, positional.dest) index_choices[positional_idx][tuple(requirements)] = positional if not requirements and isinstance(positional.choices, dict): for subcmd, subparser in positional.choices.items(): log.debug("%s| | SubParser: %s", log_prefix, subcmd) recurse_parser(subparser, positional_idx, requirements + [subcmd]) recurse_parser(parser, 0) for idx, ndict in index_choices.items(): if len(ndict) == 1: # Single choice, no requirements arg = list(ndict.values())[0] specials.extend(get_specials(arg, 'p', str(idx))) else: # Multiple requirements nlist = [] for nn, arg in ndict.items(): if arg.choices: checks = [f'[ "$cmd[{iidx}]" == "{n}" ]' for iidx, n in enumerate(nn, start=2)] choices_str = "' '".join(arg.choices) checks_str = ' && '.join(checks + [f"echo '{choices_str}'"]) nlist.append(f"( {checks_str} || false )") # Ugly hack nlist_str = ' || '.join(nlist) specials.append(f"'p@{str(idx)}@`set cmd=($COMMAND_LINE); {nlist_str}`@'") if optionals_double: if optionals_single: optionals_single.add('-') else: # Don't add a space after completing "--" from "-" optionals_single = ('-', '-') return Template("""\ # AUTOMATICALLY GENERATED by `shtab` ${preamble} complete ${prog} \\ 'c/--/(${optionals_double_str})/' \\ 'c/-/(${optionals_single_str})/' \\ ${optionals_special_str} \\ 'p/*/()/'""").safe_substitute( preamble=("\n# Custom Preamble\n" + preamble + "\n# End Custom Preamble\n" if preamble else ""), root_prefix=root_prefix, prog=parser.prog, optionals_double_str=' '.join(sorted(optionals_double)), optionals_single_str=' '.join(sorted(optionals_single)), optionals_special_str=' \\\n '.join(specials)) def complete(parser: ArgumentParser, shell: str = "bash", root_prefix: Opt[str] = None, preamble: Union[str, Dict[str, str]] = "", choice_functions: Opt[Any] = None) -> str: """ shell: bash/zsh/tcsh root_prefix: prefix for shell functions to avoid clashes (default: "_{parser.prog}") preamble: mapping shell to text to prepend to generated script (e.g. `{"bash": "_myprog_custom_function(){ echo hello }"}`) choice_functions: *deprecated* N.B. `parser.add_argument().complete = ...` can be used to define custom completions (e.g. filenames). See <../examples/pathcomplete.py>. """ if isinstance(preamble, dict): 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: ArgumentParser, option_string: Union[str, List[str]] = "--print-completion", help: str = "print shell completion script", parent: Opt[ArgumentParser] = None, preamble: Union[str, Dict[str, str]] = "", ): """ option_string: iff positional (no `-` prefix) then `parser` is assumed to actually be a subparser (subcommand mode) parent: required in subcommand mode """ 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="?") assert parent is not None, "subcommand mode: parent required" parser.add_argument(*option_string, **kwargs) return parser ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/shtab/__main__.py0000644000175100001770000000024014572406201015416 0ustar00runnerdockerimport logging import sys from .main import main if __name__ == "__main__": logging.basicConfig(level=logging.INFO) sys.exit(main(sys.argv[1:]) or 0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837456.0 shtab-1.7.1/shtab/_dist_ver.py0000644000175100001770000000002614572406220015657 0ustar00runnerdocker__version__ = '1.7.1' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/shtab/main.py0000644000175100001770000000410714572406201014630 0ustar00runnerdockerimport argparse import logging import os import sys from importlib import import_module 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)") 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("--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): other_parser = other_parser() if args.prog: other_parser.prog = args.prog print( complete(other_parser, shell=args.shell, root_prefix=args.prefix or args.parser.split(".", 1)[0], preamble=args.preamble)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/shtab/py.typed0000644000175100001770000000014314572406201015025 0ustar00runnerdockerThis file exists solely to signal that the `shtab` package carries inline types. Do not delete it. ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1709837457.0181 shtab-1.7.1/shtab.egg-info/0000755000175100001770000000000014572406221015024 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837456.0 shtab-1.7.1/shtab.egg-info/PKG-INFO0000644000175100001770000001622514572406220016126 0ustar00runnerdockerMetadata-Version: 2.1 Name: shtab Version: 1.7.1 Summary: Automagic shell tab completion for Python CLI applications Author-email: Casper da Costa-Luis Maintainer-email: Iterative License: Apache-2.0 Project-URL: documentation, https://docs.iterative.ai/shtab Project-URL: repository, https://github.com/iterative/shtab Project-URL: changelog, https://github.com/iterative/shtab/releases Keywords: tab,complete,completion,shell,bash,zsh,argparse 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: License :: OSI Approved :: Apache Software License 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.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation Classifier: Programming Language :: Python :: Implementation :: IronPython Classifier: Programming Language :: Python :: Implementation :: PyPy 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.7 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: pytest-timeout; extra == "dev" |Logo| shtab ===== |PyPI-Downloads| |Tests| |Coverage| |PyPI| |Conda| - 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 - ``bash`` - ``zsh`` - ``tcsh`` - Supports - `argparse `_ - `docopt `_ (via `argopt `_) - Supports arguments, options and subparsers - Supports choices (e.g. ``--say={hello,goodbye}``) - Supports file and directory path completion - Supports custom path completion (e.g. ``--file={*.txt}``) ------------------------------------------ .. 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}`` - subparser mode: end-users execute ``your_cli_app completion {bash,zsh,tcsh}`` Examples -------- See `the docs for usage examples `_. FAQs ---- Not working? Check out `frequently asked questions `_. Alternatives ------------ - `argcomplete `_ - executes the underlying script *every* time ```` is pressed (slow and has side-effects) - only provides ``bash`` completion - `pyzshcomplete `_ - executes the underlying script *every* time ```` is pressed (slow and has side-effects) - only provides ``zsh`` completion - `click `_ - different framework completely replacing the builtin ``argparse`` - solves multiple problems (rather than POSIX-style "do one thing well") Contributions ------------- Please do open `issues `_ & `pull requests `_! Some ideas: - support ``fish`` - support ``powershell`` See `CONTRIBUTING.md `_ for more guidance. |Hits| .. |Logo| image:: https://github.com/iterative/shtab/raw/main/meta/logo.png .. |Tests| image:: https://img.shields.io/github/actions/workflow/status/iterative/shtab/test.yml?logo=github&label=tests :target: https://github.com/iterative/shtab/actions :alt: Tests .. |Coverage| image:: https://codecov.io/gh/iterative/shtab/branch/main/graph/badge.svg :target: https://codecov.io/gh/iterative/shtab :alt: Coverage .. |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://img.shields.io/pypi/dm/shtab.svg?label=pypi%20downloads&logo=PyPI&logoColor=white :target: https://pepy.tech/project/shtab :alt: Downloads .. |Hits| image:: https://caspersci.uk.to/cgi-bin/hits.cgi?q=shtab&style=social&r=https://github.com/iterative/shtab&a=hidden :target: https://caspersci.uk.to/cgi-bin/hits.cgi?q=shtab&a=plot&r=https://github.com/iterative/shtab&style=social :alt: Hits ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837457.0 shtab-1.7.1/shtab.egg-info/SOURCES.txt0000644000175100001770000000113314572406221016706 0ustar00runnerdocker.gitignore .pre-commit-config.yaml CONTRIBUTING.md LICENCE README.rst pyproject.toml .github/workflows/test.yml docs/index.md docs/pydoc-markdown.yml docs/pydoc_markdown_shtab.py docs/requirements.txt docs/use.md examples/customcomplete.py examples/docopt-greeter.py examples/pathcomplete.py meta/logo.png shtab/__init__.py shtab/__main__.py shtab/_dist_ver.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/top_level.txt tests/__init__.py tests/test_shtab.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837456.0 shtab-1.7.1/shtab.egg-info/dependency_links.txt0000644000175100001770000000000114572406220021071 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837456.0 shtab-1.7.1/shtab.egg-info/entry_points.txt0000644000175100001770000000005214572406220020316 0ustar00runnerdocker[console_scripts] shtab = shtab.main:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837456.0 shtab-1.7.1/shtab.egg-info/requires.txt0000644000175100001770000000005314572406220017421 0ustar00runnerdocker [dev] pytest>=6 pytest-cov pytest-timeout ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837456.0 shtab-1.7.1/shtab.egg-info/top_level.txt0000644000175100001770000000002014572406220017545 0ustar00runnerdockerdist meta shtab ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1709837457.0181 shtab-1.7.1/tests/0000755000175100001770000000000014572406221013373 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/tests/__init__.py0000644000175100001770000000000014572406201015470 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1709837441.0 shtab-1.7.1/tests/test_shtab.py0000644000175100001770000002431114572406201016104 0ustar00runnerdocker""" Tests for `shtab`. """ import logging import subprocess from argparse import ArgumentParser import pytest import shtab from shtab.main import get_main_parser, main fix_shell = pytest.mark.parametrize("shell", shtab.SUPPORTED_SHELLS) 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} ]]"]) 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"', "fo", "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 @fix_shell def test_main(shell, caplog): with caplog.at_level(logging.INFO): main(["-s", shell, "shtab.main.get_main_parser"]) assert not caplog.record_tuples @fix_shell def test_main_self_completion(shell, caplog, capsys): with caplog.at_level(logging.INFO): try: main(["--print-own-completion", shell]) except SystemExit: pass captured = capsys.readouterr() assert not captured.err expected = { "bash": "complete -o filenames -F _shtab_shtab shtab", "zsh": "_shtab_shtab_commands()", "tcsh": "complete shtab"} assert expected[shell] in captured.out assert not caplog.record_tuples @fix_shell def test_prog_override(shell, caplog, capsys): with caplog.at_level(logging.INFO): main(["-s", shell, "--prog", "foo", "shtab.main.get_main_parser"]) captured = capsys.readouterr() assert not captured.err if shell == "bash": assert "complete -o filenames -F _shtab_shtab foo" in captured.out assert not caplog.record_tuples @fix_shell def test_prog_scripts(shell, caplog, capsys): with caplog.at_level(logging.INFO): 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] if shell == "bash": assert script_py == ["complete -o filenames -F _shtab_shtab script.py"] elif shell == "zsh": assert script_py == [ "#compdef script.py", "_describe 'script.py commands' _commands", "_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 \\"] else: raise NotImplementedError(shell) assert not caplog.record_tuples @fix_shell def test_prefix_override(shell, caplog, capsys): with caplog.at_level(logging.INFO): main(["-s", shell, "--prefix", "foo", "shtab.main.get_main_parser"]) captured = capsys.readouterr() print(captured.out) assert not captured.err if shell == "bash": shell = Bash(captured.out) shell.compgen('-W "${_shtab_foo_option_strings[*]}"', "--h", "--help") assert not caplog.record_tuples @fix_shell def test_complete(shell, caplog): parser = get_main_parser() with caplog.at_level(logging.INFO): completion = shtab.complete(parser, shell=shell) print(completion) if shell == "bash": shell = Bash(completion) shell.compgen('-W "${_shtab_shtab_option_strings[*]}"', "--h", "--help") assert not caplog.record_tuples @fix_shell def test_positional_choices(shell, caplog): parser = ArgumentParser(prog="test") parser.add_argument("posA", choices=["one", "two"]) with caplog.at_level(logging.INFO): completion = shtab.complete(parser, shell=shell) print(completion) if shell == "bash": shell = Bash(completion) shell.compgen('-W "$_shtab_test_pos_0_choices"', "o", "one") assert not caplog.record_tuples @fix_shell def test_custom_complete(shell, caplog): parser = ArgumentParser(prog="test") parser.add_argument("posA").complete = {"bash": "_shtab_test_some_func"} preamble = {"bash": "_shtab_test_some_func() { compgen -W 'one two' -- $1 ;}"} with caplog.at_level(logging.INFO): completion = shtab.complete(parser, shell=shell, preamble=preamble) print(completion) if shell == "bash": shell = Bash(completion) shell.test('"$($_shtab_test_pos_0_COMPGEN o)" = "one"') assert not caplog.record_tuples @fix_shell def test_subparser_custom_complete(shell, caplog): 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"} preamble = {"bash": "_shtab_test_some_func() { compgen -W 'one two' -- $1 ;}"} with caplog.at_level(logging.INFO): completion = shtab.complete(parser, shell=shell, preamble=preamble) print(completion) 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-}"') assert not caplog.record_tuples @fix_shell def test_subparser_aliases(shell, caplog): 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 ;}"} with caplog.at_level(logging.INFO): completion = shtab.complete(parser, shell=shell, preamble=preamble) print(completion) 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-}"') assert not caplog.record_tuples @fix_shell def test_subparser_colons(shell, caplog): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() subparsers.add_parser("sub:cmd", help="help message") with caplog.at_level(logging.INFO): completion = shtab.complete(parser, shell=shell) print(completion) 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-}"') assert not caplog.record_tuples @fix_shell def test_subparser_slashes(shell, caplog): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() subparsers.add_parser("sub/cmd", help="help message") with caplog.at_level(logging.INFO): completion = shtab.complete(parser, shell=shell) print(completion) 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 @fix_shell def test_add_argument_to_optional(shell, caplog): parser = ArgumentParser(prog="test") shtab.add_argument_to(parser, ["-s", "--shell"]) with caplog.at_level(logging.INFO): completion = shtab.complete(parser, shell=shell) print(completion) if shell == "bash": shell = Bash(completion) shell.compgen('-W "${_shtab_test_option_strings[*]}"', "--s", "--shell") assert not caplog.record_tuples @fix_shell def test_add_argument_to_positional(shell, caplog, 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 with caplog.at_level(logging.INFO): completion_manual = shtab.complete(parser, shell=shell) with pytest.raises(SystemExit) as exc: sub._actions[-1](sub, Namespace(), shell) assert exc.type == SystemExit assert exc.value.code == 0 completion, err = capsys.readouterr() print(completion) 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") assert not caplog.record_tuples @fix_shell def test_get_completer(shell): shtab.get_completer(shell) def test_get_completer_invalid(): try: shtab.get_completer("invalid") except NotImplementedError: pass else: raise NotImplementedError("invalid")