├── .gitignore ├── .travis.yml ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── TODO.md ├── completion.jinja ├── examples └── future ├── pylintrc └── zargparse.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Vim files 10 | *~ 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # VSCode 107 | .vscode 108 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.6 3 | install: 4 | - 'pip install pipenv' 5 | - 'pipenv install --dev' 6 | script: 7 | - 'pipenv run pylint *.py' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ctil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [dev-packages] 9 | 10 | pylint = "*" 11 | 12 | 13 | [packages] 14 | 15 | "jinja2" = "*" 16 | 17 | [requires] 18 | 19 | python_version = "3" 20 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "f6060424cf12370939bec403a17cf9167c853d6f93857373e1200bbd27572e10" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "jinja2": { 20 | "hashes": [ 21 | "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", 22 | "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" 23 | ], 24 | "index": "pypi", 25 | "version": "==2.11.3" 26 | }, 27 | "markupsafe": { 28 | "hashes": [ 29 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 30 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 31 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 32 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 33 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 34 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", 35 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", 36 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 37 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 38 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", 39 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", 40 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 41 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 42 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 43 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 44 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 45 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 46 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 47 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 48 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", 49 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", 50 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 51 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 52 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 53 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", 54 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", 55 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 56 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 57 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", 58 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 59 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 60 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 61 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", 62 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", 63 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 64 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 65 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 66 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", 67 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 68 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", 69 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 70 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", 71 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 72 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", 73 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 74 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 75 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", 76 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", 77 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", 78 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 79 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", 80 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" 81 | ], 82 | "version": "==1.1.1" 83 | } 84 | }, 85 | "develop": { 86 | "astroid": { 87 | "hashes": [ 88 | "sha256:21d735aab248253531bb0f1e1e6d068f0ee23533e18ae8a6171ff892b98297cf", 89 | "sha256:cfc35498ee64017be059ceffab0a25bedf7548ab76f2bea691c5565896e7128d" 90 | ], 91 | "version": "==2.5.1" 92 | }, 93 | "isort": { 94 | "hashes": [ 95 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 96 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 97 | ], 98 | "version": "==4.3.21" 99 | }, 100 | "lazy-object-proxy": { 101 | "hashes": [ 102 | "sha256:1d33d6f789697f401b75ce08e73b1de567b947740f768376631079290118ad39", 103 | "sha256:2f2de8f8ac0be3e40d17730e0600619d35c78c13a099ea91ef7fb4ad944ce694", 104 | "sha256:3782931963dc89e0e9a0ae4348b44762e868ea280e4f8c233b537852a8996ab9", 105 | "sha256:37d9c34b96cca6787fe014aeb651217944a967a5b165e2cacb6b858d2997ab84", 106 | "sha256:38c3865bd220bd983fcaa9aa11462619e84a71233bafd9c880f7b1cb753ca7fa", 107 | "sha256:429c4d1862f3fc37cd56304d880f2eae5bd0da83bdef889f3bd66458aac49128", 108 | "sha256:522b7c94b524389f4a4094c4bf04c2b02228454ddd17c1a9b2801fac1d754871", 109 | "sha256:57fb5c5504ddd45ed420b5b6461a78f58cbb0c1b0cbd9cd5a43ad30a4a3ee4d0", 110 | "sha256:5944a9b95e97de1980c65f03b79b356f30a43de48682b8bdd90aa5089f0ec1f4", 111 | "sha256:6f4e5e68b7af950ed7fdb594b3f19a0014a3ace0fedb86acb896e140ffb24302", 112 | "sha256:71a1ef23f22fa8437974b2d60fedb947c99a957ad625f83f43fd3de70f77f458", 113 | "sha256:8a44e9901c0555f95ac401377032f6e6af66d8fc1fbfad77a7a8b1a826e0b93c", 114 | "sha256:b6577f15d5516d7d209c1a8cde23062c0f10625f19e8dc9fb59268859778d7d7", 115 | "sha256:c8fe2d6ff0ff583784039d0255ea7da076efd08507f2be6f68583b0da32e3afb", 116 | "sha256:cadfa2c2cf54d35d13dc8d231253b7985b97d629ab9ca6e7d672c35539d38163", 117 | "sha256:cd1bdace1a8762534e9a36c073cd54e97d517a17d69a17985961265be6d22847", 118 | "sha256:ddbdcd10eb999d7ab292677f588b658372aadb9a52790f82484a37127a390108", 119 | "sha256:e7273c64bccfd9310e9601b8f4511d84730239516bada26a0c9846c9697617ef", 120 | "sha256:e7428977763150b4cf83255625a80a23dfdc94d43be7791ce90799d446b4e26f", 121 | "sha256:e960e8be509e8d6d618300a6c189555c24efde63e85acaf0b14b2cd1ac743315", 122 | "sha256:ecb5dd5990cec6e7f5c9c1124a37cb2c710c6d69b0c1a5c4aa4b35eba0ada068", 123 | "sha256:ef3f5e288aa57b73b034ce9c1f1ac753d968f9069cd0742d1d69c698a0167166", 124 | "sha256:fa5b2dee0e231fa4ad117be114251bdfe6afe39213bd629d43deb117b6a6c40a", 125 | "sha256:fa7fb7973c622b9e725bee1db569d2c2ee64d2f9a089201c5e8185d482c7352d" 126 | ], 127 | "version": "==1.5.2" 128 | }, 129 | "mccabe": { 130 | "hashes": [ 131 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 132 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 133 | ], 134 | "version": "==0.6.1" 135 | }, 136 | "pylint": { 137 | "hashes": [ 138 | "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", 139 | "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" 140 | ], 141 | "index": "pypi", 142 | "version": "==2.3.1" 143 | }, 144 | "wrapt": { 145 | "hashes": [ 146 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 147 | ], 148 | "version": "==1.12.1" 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zargparse 2 | 3 | ## Usage 4 | 5 | Pass a python script to zargparse.py and it will write out a Zsh completion 6 | file to the current working directory. Make sure the script is executable 7 | under the current environment with Python 3 since zargparse needs to run the 8 | script. 9 | 10 | ```commandline 11 | ./zargparse.py examples/future 12 | ``` 13 | 14 | It is recommended to use [Pipenv](https://docs.pipenv.org) to build a virtual environment. 15 | 16 | ```commandline 17 | pipenv install 18 | pipenv run ./zargparse.py examples/future 19 | ``` 20 | 21 | ## Dependencies 22 | 23 | Zargparse requires Python 3 and the Jinja2 library. It has been tested with 24 | Python 3.6. 25 | 26 | ## Limitations 27 | 28 | The tool does not support complex completions of arguments as described 29 | [here](https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org#main-utility-functions-for-overall-completion). 30 | The completion file may need to be modified by hand after generation in order 31 | to fine tune the completions. 32 | 33 | Nested [sub-commands](https://docs.python.org/3.6/library/argparse.html#sub-commands) 34 | are not supported. 35 | 36 | Positional arguments are not yet supported. 37 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Command line flag to choose name of output file 2 | * Support for argparse actions like append that take multiple arguments 3 | * Support nested subcommands 4 | * Completion for arguments that are file paths or accept limited choices 5 | * Support for add_argument_group 6 | * Support positional arguments 7 | * Unit tests 8 | * Pass mypy checks -------------------------------------------------------------------------------- /completion.jinja: -------------------------------------------------------------------------------- 1 | #compdef {{ tool.name }} 2 | # ---------------------------------------------------------------------------------------- 3 | # Zsh completion file for {{ tool.name }} 4 | # 5 | {% if tool.description %} 6 | {% for line in tool.description.splitlines() %} 7 | # {{ line }} 8 | {% endfor %} 9 | {% endif %} 10 | # 11 | # This file was generated with help from Zargparse (https://github.com/ctil/zargparse) 12 | # ---------------------------------------------------------------------------------------- 13 | 14 | typeset -A opt_args 15 | 16 | _{{ tool.name }}() { 17 | {% if tool.subcommands %} 18 | # Define the subcommands 19 | local -a commands 20 | commands=( 21 | {% for cmd in tool.subcommands %} 22 | '{{ cmd.name }}:{{ cmd.help_text }}' 23 | {% endfor %} 24 | ) 25 | {% endif %} 26 | 27 | # Global flags (i.e. ones not associated with a subcommand) 28 | _arguments \ 29 | {% if tool.subcommands %} 30 | "1: :{_describe 'command' commands}" \ 31 | {% endif %} 32 | {% for flag in tool.flags %} 33 | {{ flag.arg_string }} \ 34 | {% endfor %} 35 | '*:: :->args' 36 | 37 | {% if tool.subcommands %} 38 | # Flags for each subcommand 39 | case $state in 40 | args) 41 | case $words[1] in 42 | {% for cmd in tool.subcommands %} 43 | {{ cmd.name }}) 44 | _arguments \ 45 | {% for flag in cmd.flags %} 46 | {{ flag.arg_string }} \ 47 | {% endfor %} 48 | ;; 49 | {% endfor %} 50 | esac 51 | ;; 52 | esac 53 | {% endif %} 54 | } 55 | _{{ tool.name }} 56 | -------------------------------------------------------------------------------- /examples/future: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Example command line program for testing zargparse.""" 3 | 4 | import argparse 5 | 6 | 7 | def travel(args): 8 | print('Traveling to the year {} at {} MPH!'.format(args.year, args.speed)) 9 | if args.no_roads: 10 | print("We don't need roads.") 11 | 12 | 13 | def fly(args): 14 | print('Flying at {} feet at {} MPH!'.format(args.height, args.speed)) 15 | 16 | 17 | if __name__ == '__main__': 18 | parser = argparse.ArgumentParser(description='A time traveling program') 19 | 20 | # Global flags 21 | parser.add_argument('--speed', '-s', type=int, default=88, 22 | help='Speed in MPH') 23 | subparsers = parser.add_subparsers(title='subcommands', dest='command') 24 | subparsers.required = True 25 | 26 | # Travel subcommand 27 | parser_travel = subparsers.add_parser('travel', help='Travel in time') 28 | parser_travel.add_argument('--year', '-y', type=int, default=1955, 29 | help='Year to travel to.', choices=(1955, 2015, 1885)) 30 | parser_travel.add_argument('--no-roads', action='store_true', default=None, 31 | help='Enable road-free operation.') 32 | parser_travel.set_defaults(func=travel) 33 | 34 | # Fly subcommand 35 | parser_fly = subparsers.add_parser('fly', help='Fly in the air') 36 | parser_fly.add_argument('height', type=int, help='Height to fly, in feet') 37 | parser_fly.set_defaults(func=fly) 38 | 39 | arguments = parser.parse_args() 40 | arguments.func(arguments) 41 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=1 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python modules names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape, 142 | R, 143 | C 144 | 145 | # Enable the message, report, category or checker with the given id(s). You can 146 | # either give multiple identifier separated by comma (,) or put this option 147 | # multiple time (only on the command line, not in the configuration file where 148 | # it should appear only once). See also the "--disable" option for examples. 149 | enable=c-extension-no-member 150 | 151 | 152 | [REPORTS] 153 | 154 | # Python expression which should return a note less than 10 (10 is the highest 155 | # note). You have access to the variables errors warning, statement which 156 | # respectively contain the number of errors / warnings messages and the total 157 | # number of statements analyzed. This is used by the global evaluation report 158 | # (RP0004). 159 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 160 | 161 | # Template used to display messages. This is a python new-style format string 162 | # used to format the message information. See doc for all details. 163 | #msg-template= 164 | 165 | # Set the output format. Available formats are text, parseable, colorized, json 166 | # and msvs (visual studio). You can also give a reporter class, e.g. 167 | # mypackage.mymodule.MyReporterClass. 168 | output-format=text 169 | 170 | # Tells whether to display a full report or only the messages. 171 | reports=no 172 | 173 | # Activate the evaluation score. 174 | score=yes 175 | 176 | 177 | [REFACTORING] 178 | 179 | # Maximum number of nested blocks for function / method body 180 | max-nested-blocks=5 181 | 182 | # Complete name of functions that never returns. When checking for 183 | # inconsistent-return-statements if a never returning function is called then 184 | # it will be considered as an explicit return statement and no message will be 185 | # printed. 186 | never-returning-functions=sys.exit 187 | 188 | 189 | [BASIC] 190 | 191 | # Naming style matching correct argument names. 192 | argument-naming-style=snake_case 193 | 194 | # Regular expression matching correct argument names. Overrides argument- 195 | # naming-style. 196 | #argument-rgx= 197 | 198 | # Naming style matching correct attribute names. 199 | attr-naming-style=snake_case 200 | 201 | # Regular expression matching correct attribute names. Overrides attr-naming- 202 | # style. 203 | #attr-rgx= 204 | 205 | # Bad variable names which should always be refused, separated by a comma. 206 | bad-names=foo, 207 | bar, 208 | baz, 209 | toto, 210 | tutu, 211 | tata 212 | 213 | # Naming style matching correct class attribute names. 214 | class-attribute-naming-style=any 215 | 216 | # Regular expression matching correct class attribute names. Overrides class- 217 | # attribute-naming-style. 218 | #class-attribute-rgx= 219 | 220 | # Naming style matching correct class names. 221 | class-naming-style=PascalCase 222 | 223 | # Regular expression matching correct class names. Overrides class-naming- 224 | # style. 225 | #class-rgx= 226 | 227 | # Naming style matching correct constant names. 228 | const-naming-style=UPPER_CASE 229 | 230 | # Regular expression matching correct constant names. Overrides const-naming- 231 | # style. 232 | #const-rgx= 233 | 234 | # Minimum line length for functions/classes that require docstrings, shorter 235 | # ones are exempt. 236 | docstring-min-length=-1 237 | 238 | # Naming style matching correct function names. 239 | function-naming-style=snake_case 240 | 241 | # Regular expression matching correct function names. Overrides function- 242 | # naming-style. 243 | #function-rgx= 244 | 245 | # Good variable names which should always be accepted, separated by a comma. 246 | good-names=i, 247 | j, 248 | k, 249 | ex, 250 | Run, 251 | _ 252 | 253 | # Include a hint for the correct naming format with invalid-name. 254 | include-naming-hint=no 255 | 256 | # Naming style matching correct inline iteration names. 257 | inlinevar-naming-style=any 258 | 259 | # Regular expression matching correct inline iteration names. Overrides 260 | # inlinevar-naming-style. 261 | #inlinevar-rgx= 262 | 263 | # Naming style matching correct method names. 264 | method-naming-style=snake_case 265 | 266 | # Regular expression matching correct method names. Overrides method-naming- 267 | # style. 268 | #method-rgx= 269 | 270 | # Naming style matching correct module names. 271 | module-naming-style=snake_case 272 | 273 | # Regular expression matching correct module names. Overrides module-naming- 274 | # style. 275 | #module-rgx= 276 | 277 | # Colon-delimited sets of names that determine each other's naming style when 278 | # the name regexes allow several styles. 279 | name-group= 280 | 281 | # Regular expression which should only match function or class names that do 282 | # not require a docstring. 283 | no-docstring-rgx=^_ 284 | 285 | # List of decorators that produce properties, such as abc.abstractproperty. Add 286 | # to this list to register other decorators that produce valid properties. 287 | # These decorators are taken in consideration only for invalid-name. 288 | property-classes=abc.abstractproperty 289 | 290 | # Naming style matching correct variable names. 291 | variable-naming-style=snake_case 292 | 293 | # Regular expression matching correct variable names. Overrides variable- 294 | # naming-style. 295 | #variable-rgx= 296 | 297 | 298 | [FORMAT] 299 | 300 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 301 | expected-line-ending-format= 302 | 303 | # Regexp for a line that is allowed to be longer than the limit. 304 | ignore-long-lines=^\s*(# )??$ 305 | 306 | # Number of spaces of indent required inside a hanging or continued line. 307 | indent-after-paren=4 308 | 309 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 310 | # tab). 311 | indent-string=' ' 312 | 313 | # Maximum number of characters on a single line. 314 | max-line-length=100 315 | 316 | # Maximum number of lines in a module. 317 | max-module-lines=1000 318 | 319 | # List of optional constructs for which whitespace checking is disabled. `dict- 320 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 321 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 322 | # `empty-line` allows space-only lines. 323 | no-space-check=trailing-comma, 324 | dict-separator 325 | 326 | # Allow the body of a class to be on the same line as the declaration if body 327 | # contains single statement. 328 | single-line-class-stmt=no 329 | 330 | # Allow the body of an if to be on the same line as the test if there is no 331 | # else. 332 | single-line-if-stmt=no 333 | 334 | 335 | [LOGGING] 336 | 337 | # Format style used to check logging format string. `old` means using % 338 | # formatting, while `new` is for `{}` formatting. 339 | logging-format-style=old 340 | 341 | # Logging modules to check that the string format arguments are in logging 342 | # function parameter format. 343 | logging-modules=logging 344 | 345 | 346 | [MISCELLANEOUS] 347 | 348 | # List of note tags to take in consideration, separated by a comma. 349 | notes=FIXME, 350 | XXX, 351 | TODO 352 | 353 | 354 | [SIMILARITIES] 355 | 356 | # Ignore comments when computing similarities. 357 | ignore-comments=yes 358 | 359 | # Ignore docstrings when computing similarities. 360 | ignore-docstrings=yes 361 | 362 | # Ignore imports when computing similarities. 363 | ignore-imports=no 364 | 365 | # Minimum lines number of a similarity. 366 | min-similarity-lines=4 367 | 368 | 369 | [SPELLING] 370 | 371 | # Limits count of emitted suggestions for spelling mistakes. 372 | max-spelling-suggestions=4 373 | 374 | # Spelling dictionary name. Available dictionaries: none. To make it working 375 | # install python-enchant package.. 376 | spelling-dict= 377 | 378 | # List of comma separated words that should not be checked. 379 | spelling-ignore-words= 380 | 381 | # A path to a file that contains private dictionary; one word per line. 382 | spelling-private-dict-file= 383 | 384 | # Tells whether to store unknown words to indicated private dictionary in 385 | # --spelling-private-dict-file option instead of raising a message. 386 | spelling-store-unknown-words=no 387 | 388 | 389 | [STRING] 390 | 391 | # This flag controls whether the implicit-str-concat-in-sequence should 392 | # generate a warning on implicit string concatenation in sequences defined over 393 | # several lines. 394 | check-str-concat-over-line-jumps=no 395 | 396 | 397 | [TYPECHECK] 398 | 399 | # List of decorators that produce context managers, such as 400 | # contextlib.contextmanager. Add to this list to register other decorators that 401 | # produce valid context managers. 402 | contextmanager-decorators=contextlib.contextmanager 403 | 404 | # List of members which are set dynamically and missed by pylint inference 405 | # system, and so shouldn't trigger E1101 when accessed. Python regular 406 | # expressions are accepted. 407 | generated-members= 408 | 409 | # Tells whether missing members accessed in mixin class should be ignored. A 410 | # mixin class is detected if its name ends with "mixin" (case insensitive). 411 | ignore-mixin-members=yes 412 | 413 | # Tells whether to warn about missing members when the owner of the attribute 414 | # is inferred to be None. 415 | ignore-none=yes 416 | 417 | # This flag controls whether pylint should warn about no-member and similar 418 | # checks whenever an opaque object is returned when inferring. The inference 419 | # can return multiple potential results while evaluating a Python object, but 420 | # some branches might not be evaluated, which results in partial inference. In 421 | # that case, it might be useful to still emit no-member and other checks for 422 | # the rest of the inferred objects. 423 | ignore-on-opaque-inference=yes 424 | 425 | # List of class names for which member attributes should not be checked (useful 426 | # for classes with dynamically set attributes). This supports the use of 427 | # qualified names. 428 | ignored-classes=optparse.Values,thread._local,_thread._local 429 | 430 | # List of module names for which member attributes should not be checked 431 | # (useful for modules/projects where namespaces are manipulated during runtime 432 | # and thus existing member attributes cannot be deduced by static analysis. It 433 | # supports qualified module names, as well as Unix pattern matching. 434 | ignored-modules= 435 | 436 | # Show a hint with possible names when a member name was not found. The aspect 437 | # of finding the hint is based on edit distance. 438 | missing-member-hint=yes 439 | 440 | # The minimum edit distance a name should have in order to be considered a 441 | # similar match for a missing member name. 442 | missing-member-hint-distance=1 443 | 444 | # The total number of similar names that should be taken in consideration when 445 | # showing a hint for a missing member. 446 | missing-member-max-choices=1 447 | 448 | 449 | [VARIABLES] 450 | 451 | # List of additional names supposed to be defined in builtins. Remember that 452 | # you should avoid defining new builtins when possible. 453 | additional-builtins= 454 | 455 | # Tells whether unused global variables should be treated as a violation. 456 | allow-global-unused-variables=yes 457 | 458 | # List of strings which can identify a callback function by name. A callback 459 | # name must start or end with one of those strings. 460 | callbacks=cb_, 461 | _cb 462 | 463 | # A regular expression matching the name of dummy variables (i.e. expected to 464 | # not be used). 465 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 466 | 467 | # Argument names that match this expression will be ignored. Default to name 468 | # with leading underscore. 469 | ignored-argument-names=_.*|^ignored_|^unused_ 470 | 471 | # Tells whether we should check for unused import in __init__ files. 472 | init-import=no 473 | 474 | # List of qualified module names which can have objects that can redefine 475 | # builtins. 476 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 477 | 478 | 479 | [CLASSES] 480 | 481 | # List of method names used to declare (i.e. assign) instance attributes. 482 | defining-attr-methods=__init__, 483 | __new__, 484 | setUp 485 | 486 | # List of member names, which should be excluded from the protected access 487 | # warning. 488 | exclude-protected=_asdict, 489 | _fields, 490 | _replace, 491 | _source, 492 | _make 493 | 494 | # List of valid names for the first argument in a class method. 495 | valid-classmethod-first-arg=cls 496 | 497 | # List of valid names for the first argument in a metaclass class method. 498 | valid-metaclass-classmethod-first-arg=cls 499 | 500 | 501 | [DESIGN] 502 | 503 | # Maximum number of arguments for function / method. 504 | max-args=5 505 | 506 | # Maximum number of attributes for a class (see R0902). 507 | max-attributes=7 508 | 509 | # Maximum number of boolean expressions in an if statement. 510 | max-bool-expr=5 511 | 512 | # Maximum number of branch for function / method body. 513 | max-branches=12 514 | 515 | # Maximum number of locals for function / method body. 516 | max-locals=15 517 | 518 | # Maximum number of parents for a class (see R0901). 519 | max-parents=7 520 | 521 | # Maximum number of public methods for a class (see R0904). 522 | max-public-methods=20 523 | 524 | # Maximum number of return / yield for function / method body. 525 | max-returns=6 526 | 527 | # Maximum number of statements in function / method body. 528 | max-statements=50 529 | 530 | # Minimum number of public methods for a class (see R0903). 531 | min-public-methods=2 532 | 533 | 534 | [IMPORTS] 535 | 536 | # Allow wildcard imports from modules that define __all__. 537 | allow-wildcard-with-all=no 538 | 539 | # Analyse import fallback blocks. This can be used to support both Python 2 and 540 | # 3 compatible code, which means that the block might have code that exists 541 | # only in one or another interpreter, leading to false positives when analysed. 542 | analyse-fallback-blocks=no 543 | 544 | # Deprecated modules which should not be used, separated by a comma. 545 | deprecated-modules=optparse,tkinter.tix 546 | 547 | # Create a graph of external dependencies in the given file (report RP0402 must 548 | # not be disabled). 549 | ext-import-graph= 550 | 551 | # Create a graph of every (i.e. internal and external) dependencies in the 552 | # given file (report RP0402 must not be disabled). 553 | import-graph= 554 | 555 | # Create a graph of internal dependencies in the given file (report RP0402 must 556 | # not be disabled). 557 | int-import-graph= 558 | 559 | # Force import order to recognize a module as part of the standard 560 | # compatibility libraries. 561 | known-standard-library= 562 | 563 | # Force import order to recognize a module as part of a third party library. 564 | known-third-party=enchant 565 | 566 | 567 | [EXCEPTIONS] 568 | 569 | # Exceptions that will emit a warning when being caught. Defaults to 570 | # "BaseException, Exception". 571 | overgeneral-exceptions=BaseException, 572 | Exception 573 | -------------------------------------------------------------------------------- /zargparse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Zargparse - a tool for generating Zsh completion files.""" 3 | 4 | import argparse 5 | import os 6 | import runpy 7 | import sys 8 | from typing import Optional 9 | 10 | import jinja2 11 | 12 | TEMPLATE_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), 13 | 'completion.jinja') 14 | 15 | 16 | class Argument: 17 | """A positional argument.""" 18 | def __init__(self, name: str, help_text: str) -> None: 19 | self.name = name 20 | self.help_text = format_help_text(help_text) 21 | 22 | 23 | def format_help_text(help_text: Optional[str]) -> Optional[str]: 24 | """Strip leading/trailing whitespace and replace single with double quotes.""" 25 | if help_text is not None: 26 | help_text = help_text.strip().replace("'",'"').replace("[",r"\[").replace("]",r"\]") 27 | return help_text 28 | 29 | 30 | class Flag: 31 | """A command line flag.""" 32 | def __init__(self, options, help_text: str, has_argument: bool, choices:list ) -> None: 33 | self.options = options 34 | self.help_text = format_help_text(help_text) 35 | self.has_argument = has_argument 36 | self.choices = choices 37 | 38 | @property 39 | def arg_string(self) -> str: 40 | """String passed to _arguments function in a Zsh completion file. 41 | 42 | A flag with one option and no arguments: 43 | '--flag[Description of flag.]' 44 | 45 | The same flag that takes an argument: 46 | '--flag=[Description of flag.]' 47 | 48 | A flag with multiple options that takes an argument: 49 | '(--flag -f)'{--flag,-f}'=[Description of flag.]:' 50 | """ 51 | result = "'" 52 | if len(self.options) == 1: 53 | result += self.options[0] 54 | else: 55 | result += "(" + ' '.join(self.options) + ")'" 56 | result += "{" + ','.join(self.options) + "}'" 57 | if self.has_argument: 58 | result += '=' 59 | result += "[{}]".format(self.help_text) 60 | if self.has_argument: 61 | # This is needed so that completion works for subcommands when a 62 | # global flag is used. 63 | result += ':' 64 | if self.choices: 65 | result += "{} :({})".format(self.options[0], ' '.join([str(c) for c in self.choices])) 66 | result += "'" 67 | return result 68 | 69 | 70 | class Subcommand: 71 | """A subcommand of a command line tool.""" 72 | def __init__(self, name: str, help_text: str) -> None: 73 | self.name = name 74 | self.help_text = format_help_text(help_text) 75 | self.flags = [] 76 | self.arguments = [] 77 | 78 | 79 | class Tool: 80 | """A command line tool.""" 81 | def __init__(self, name: str, description: str) -> None: 82 | self.name = name 83 | self.description = description 84 | self.subcommands = [] 85 | self.flags = [] 86 | self.arguments = [] 87 | 88 | def print(self): 89 | print('Tool Name: {}'.format(self.name)) 90 | print('Description: {}'.format(self.description)) 91 | print('Flags:') 92 | for flag in self.flags: 93 | print(' {}: {}'.format(flag.options, flag.help_text)) 94 | print('Positional arguments:') 95 | for arg in self.arguments: 96 | print(' {}: {}'.format(arg.name, arg.help_text)) 97 | print('Subcommands:') 98 | for subcommand in self.subcommands: 99 | print(' {}: {}'.format(subcommand.name, subcommand.help_text)) 100 | for flag in subcommand.flags: 101 | print(' {}: {}'.format(flag.options, flag.help_text)) 102 | for arg in subcommand.arguments: 103 | print(' {}: {}'.format(arg.name, arg.help_text)) 104 | 105 | 106 | # pylint: disable=protected-access 107 | # noinspection PyProtectedMember 108 | class ParserAnalyzer: 109 | """Analyze the ArgumentParser object. 110 | 111 | This collects the data needed for autocompletion. 112 | """ 113 | def __init__(self, parser: argparse.ArgumentParser) -> None: 114 | self.parser = parser 115 | tool_name = self.parser.prog 116 | description = self.parser.description 117 | self.tool = Tool(tool_name, description) 118 | self._analyze_parser() 119 | 120 | def _analyze_parser(self): 121 | for action in self.parser._actions: 122 | if isinstance(action, argparse._SubParsersAction): 123 | self._analyze_subparsers(action) 124 | elif action.option_strings: 125 | self.tool.flags.append(self._get_flag(action)) 126 | else: 127 | self.tool.arguments.append(self._get_argument(action)) 128 | 129 | def _analyze_subparser(self, subparser: argparse.ArgumentParser, 130 | help_text: str, name: str): 131 | subcommand = Subcommand(name, help_text) 132 | for action in subparser._actions: 133 | if isinstance(action, argparse._SubParsersAction): 134 | # Nested subparsers are not supported 135 | pass 136 | elif action.option_strings: 137 | subcommand.flags.append(self._get_flag(action)) 138 | else: 139 | subcommand.arguments.append(self._get_argument(action)) 140 | self.tool.subcommands.append(subcommand) 141 | 142 | @staticmethod 143 | def _get_argument(action: argparse.Action): 144 | return Argument(action.dest, action.help) 145 | 146 | @staticmethod 147 | def _get_flag(action: argparse.Action): 148 | options = action.option_strings 149 | help_text = action.help 150 | if isinstance(action, (argparse._HelpAction, 151 | argparse._StoreFalseAction, 152 | argparse._StoreTrueAction, 153 | argparse._CountAction, 154 | argparse._AppendConstAction, 155 | argparse._VersionAction, 156 | argparse._StoreConstAction)): 157 | has_argument = False 158 | choices = [] 159 | else: 160 | has_argument = True 161 | choices = action.choices 162 | return Flag(options, help_text, has_argument, choices) 163 | 164 | def _analyze_subparsers(self, action: argparse._SubParsersAction) -> None: 165 | for index, name in enumerate(action.choices.keys()): 166 | # The help text for a subcommand is stored in _choices_action 167 | help_text = action._choices_actions[index].help 168 | self._analyze_subparser(action.choices[name], help_text, name) 169 | 170 | 171 | def fake_parse_args(parser: argparse.ArgumentParser, _args=None, 172 | _namespace=None) -> None: 173 | """Used to monkey patch ArgumentParser.parse_args.""" 174 | analyzer = ParserAnalyzer(parser) 175 | with open(TEMPLATE_FILE) as f: 176 | template = jinja2.Template(f.read(), trim_blocks=True) 177 | 178 | output_file = '_{}'.format(analyzer.tool.name) 179 | print('Writing completion file to {}'.format(output_file)) 180 | with open(output_file, 'w') as f: 181 | f.write(template.render(tool=analyzer.tool)) 182 | sys.exit(0) 183 | 184 | 185 | def main(file_path: str): 186 | # Monkey patch parse_args in order to inspect the ArgumentParser object 187 | argparse.ArgumentParser.parse_args = fake_parse_args 188 | 189 | # Run the file as __main__ to emulate usage via the command line 190 | runpy.run_path(file_path, run_name='__main__') 191 | 192 | 193 | if __name__ == '__main__': 194 | _parser = argparse.ArgumentParser( 195 | description='Zargparse - A Zsh completion file generator') 196 | _parser.add_argument('file_path', help='File to generate completion for') 197 | arguments = _parser.parse_args() 198 | 199 | main(arguments.file_path) 200 | --------------------------------------------------------------------------------