pax_global_header00006660000000000000000000000064152006134750014515gustar00rootroot0000000000000052 comment=bc5afec6d475e6bba748e25ecd3e0b0dbdf52b61 crazy-complete-crazy-complete-bc5afec/000077500000000000000000000000001520061347500202415ustar00rootroot00000000000000crazy-complete-crazy-complete-bc5afec/.gitignore000066400000000000000000000003611520061347500222310ustar00rootroot00000000000000__pycache__ .idea .ruff_cache # packaging cracy_complete.egg-info/ build/ dist/ pkg/ src/ python-crazy-complete-*.pkg.* python-crazy-complete/ # directory 'test' test/tests/output/ test/tests/tmp/ out.json out.yaml help.txt tests.new.yaml crazy-complete-crazy-complete-bc5afec/.readthedocs.yaml000066400000000000000000000011231520061347500234650ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version, and other tools you might need build: os: ubuntu-24.04 tools: python: "3.13" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py # Optionally, but recommended, # declare the Python requirements required to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html # python: # install: # - requirements: docs/requirements.txt crazy-complete-crazy-complete-bc5afec/LICENSE000066400000000000000000000773311520061347500212610ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS crazy-complete-crazy-complete-bc5afec/PKGBUILD000066400000000000000000000025571520061347500213760ustar00rootroot00000000000000# Maintainer: Benjamin Abendroth pkgname=python-crazy-complete pkgver=0.3.8 pkgrel=1 pkgdesc='Generate shell completion files for all major shells' arch=('any') url='https://github.com/crazy-complete/crazy-complete' license=('GPL-3') depends=('python' 'python-yaml') makedepends=( 'git' 'python-build' 'python-installer' 'python-wheel' 'python-setuptools-scm' ) #_commit='f09e930b12081d1afbd06b0376f88040d30a807c' source=("$pkgname::git+$url") ##commit=$_commit") b2sums=('SKIP') build() { cd "$pkgname" python -m build --wheel --no-isolation local site_packages=$(python -c "import site; print(site.getsitepackages()[0])") # install to temporary directory python -m installer --destdir="$PWD/tmp_install" dist/*.whl export PYTHONPATH="$PWD/tmp_install$site_packages" } package() { cd "$pkgname" python -m installer --destdir="$pkgdir" dist/*.whl # # completions # install -vDm644 bash.completion "$pkgdir/usr/share/bash-completion/completions/shtab" # install -vDm644 zsh.completion "$pkgdir/usr/share/zsh/site-functions/_shtab" # # symlink license file # local site_packages=$(python -c "import site; print(site.getsitepackages()[0])") # install -d "$pkgdir/usr/share/licenses/$pkgname" # ln -s "$site_packages/${pkgname#python-}-$pkgver.dist-info/LICENCE" \ # "$pkgdir/usr/share/licenses/$pkgname/LICENCE" } crazy-complete-crazy-complete-bc5afec/README.md000066400000000000000000000154311520061347500215240ustar00rootroot00000000000000crazy-complete ============== Every program should have autocompletion in the shell to enhance user experience and productivity. crazy-complete helps solve this task by generating robust and reliable autocompletion scripts. **Key Features**: - **Generates Robust Scripts**: Ensures that the autocompletion scripts are reliable and efficient. - **Multi-Shell Support**: Works seamlessly with **Bash**, **Fish**, and **Zsh**, providing flexibility across different environments. - **Minimal Dependencies**: The only external dependency is PyYAML. - **Configurable and Extendable**: The generated autocompletion scripts are highly configurable and can be easily extended to suit your specific needs. - **Standalone Scripts**: The generated scripts are standalone and do not depend on modified environments, unlike some alternatives like argcomplete. - **Easy to Use**: Simple and intuitive to set up, allowing you to quickly add autocompletion functionality to your programs. With crazy-complete, adding autocompletion to your programs has never been easier. Try it out and see the difference it makes in your command-line applications! Table of Contents ================= - [Benefits of Using crazy-complete](#benefits-of-using-crazy-complete) - [Disadvantages of crazy-complete](#disadvantages-of-crazy-complete) - [Installation](#installation) - [Synposis](#synopsis) - [Usage examples](#usage-examples) - [Definition file examples](#definition-file-examples) - [Documentation](#documentation) - [Comparision with other auto-complete generators](#comparision-with-other-auto-complete-generators) - [Questions or problems](#questions-or-problems) Benefits of Using crazy-complete ================================ - **Focus on what matters:** crazy-complete generates the basic structure of your autocompletion scripts, so you can focus entirely on defining options and their completions instead of dealing with repetitive setup code. - **Cross-shell consistency:** Write your completion logic once and get reliable completions for **Bash**, **Zsh**, and **Fish**, with identical behavior across all shells. - **Powerful argument completion:** crazy-complete provides a rich set of [built-in](docs/documentation.md#built-in-commands) helpers for argument completion and makes it simple to define your own [custom argument handlers](docs/documentation.md#user-defined-commands). - **Arbitrary levels of subcommands:** No matter how deeply nested your CLI structure is, crazy-complete can handle it. You can define completions for commands, subcommands, and even sub-subcommands without extra effort. - **Full control over options:** - **Repeatable / non-repeatable options**: control whether options can be used once or multiple times. - **Mutually exclusive options**: ensure that incompatible options cannot appear together. - **Conditional options**: only suggest options when certain conditions are met. - **Final options**: options that prevent any further options from being completed. - **Hidden options**: completable options that are not shown in the suggestion list. - **Capturing options**: collect options and their values to enable advanced, context-sensitive completions. Disadvantages of crazy-complete =============================== While crazy-complete offers many advantages, there are some trade-offs to be aware of: - **Code size and verbosity:** Its biggest strength - **secure, fully controlled completions** - can also be its biggest weakness. - For **Bash**, this means the generated scripts contain a significant amount of boilerplate code for parsing options and positional arguments. - For **Fish**, large command-line definitions (more than 500 options) may result in slower completions, although performance is usually acceptable for most use cases. - **Mitigation:** There are ways to reduce script size and improve performance. See [Tips And Tricks](docs/documentation.md#tips-and-tricks) for more details. - **Not as optimized as hand-written scripts:** The generated scripts prioritize correctness and reliability over minimal size or maximum performance. Hand-written scripts may be more compact and slightly faster in some cases. Installation ============ - Using Arch Linux: Use the \*.pkg.tar.zst file that has been released in this repository. Or use: ``` git clone https://github.com/crazy-complete/crazy-complete cd crazy-complete makepkg -c && sudo pacman -U python-crazy-complete*.pkg.* ``` - Using Debian: Use the \*.deb file that has been released in this repository. - Using Fedora / OpenSuse: Use the \*.rpm file that has been released in this repository. - For other Linux distributions: ``` git clone https://github.com/crazy-complete/crazy-complete cd crazy-complete python3 -m pip install . ``` Synopsis ======== > `crazy-complete [OPTIONS] {bash,fish,zsh,yaml,json} ` See [docs/documentation.md#options](docs/documentation.md#options) for a list of options. Completions for crazy-complete ============================== To install system-wide completion files for crazy-complete, execute the following: ``` sudo crazy-complete --input-type=python -i bash "$(which crazy-complete)" sudo crazy-complete --input-type=python -i fish "$(which crazy-complete)" sudo crazy-complete --input-type=python -i zsh "$(which crazy-complete)" ``` If you want to uninstall the completion files, pass `-u` to crazy-complete: ``` sudo crazy-complete --input-type=python -u bash "$(which crazy-complete)" sudo crazy-complete --input-type=python -u fish "$(which crazy-complete)" sudo crazy-complete --input-type=python -u zsh "$(which crazy-complete)" ``` Usage examples ============== Converting a Python script to YAML: ``` crazy-complete --input-type=python yaml my_program.py ``` Generate a YAML file from help text: ``` grep --help > help_file crazy-complete --input-type=help yaml help_file - or - grep --help | crazy-complete --input-type=help yaml /dev/stdin ``` Generate shell auto completions for Bash: ``` crazy-complete --input-type=yaml --include-file my_program.bash bash my_program.yaml ``` Definition file examples ======================== See [examples](https://github.com/crazy-complete/crazy-complete/tree/main/examples) for examples. See [completions](https://github.com/crazy-complete/completions) for real world applications of crazy-complete. You can even have a look at the [tests](https://github.com/crazy-complete/crazy-complete/tree/main/test). Documentation ============= See [docs/documentation.md](docs/documentation.md). Comparision with other auto-complete generators =============================================== See [docs/comparision.md](docs/comparision.md) for a comparision with other tools. Questions or problems ===================== Don't hesitate to open an issue on [GitHub](https://github.com/crazy-complete/crazy-complete/issues). crazy-complete-crazy-complete-bc5afec/crazy-complete000077500000000000000000000046151520061347500231330ustar00rootroot00000000000000#!/usr/bin/env python3 # crazy-complete - Generate shell auto completion files # Copyright (C) 2025-2026 Benjamin Abendroth (braph93@gmx.de) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import json import yaml import argparse import traceback from crazy_complete import application from crazy_complete.utils import print_err # "Import" argparse definitions for: # crazy-complete --input-type=python bash "$(which crazy-complete)" p = application._crazy_complete_argument_parser if __name__ == '__main__': app = application.Application() try: app.parse_args(sys.argv[1:]) except argparse.ArgumentError as e: print_err('Command line error:', e) sys.exit(2) try: app.run() except (application.CrazyError, FileNotFoundError, yaml.scanner.ScannerError, yaml.parser.ParserError, yaml.constructor.ConstructorError, json.decoder.JSONDecodeError) as e: print_err('Error:', e) if app.options.debug: traceback.print_exc() else: print_err('Pass --debug to see full stack trace') sys.exit(1) except Exception: traceback.print_exc() print_err() print_err('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') print_err('!!! !!!') print_err('!!! THIS IS PROBABLY AN ERROR IN THE PROGRAM! !!!') print_err("!!! Don't hesitate to open an issue on GitHub: !!!") print_err('!!! https://github.com/crazy-complete/crazy-complete !!!') print_err('!!! !!!') print_err('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') print_err() sys.exit(3) crazy-complete-crazy-complete-bc5afec/crazy_complete/000077500000000000000000000000001520061347500232615ustar00rootroot00000000000000crazy-complete-crazy-complete-bc5afec/crazy_complete/.trash.py000066400000000000000000000364751520061347500250510ustar00rootroot00000000000000# ============================================================================= # cli.py # ============================================================================= def get_command_paths_pattern(self): paths = [] for cmdline in self.get_parents(include_self=True): paths.append(cmdline.get_all_commands(with_aliases=True)) return '|'.join(shell.escape(" ".join(combo)) for combo in product(*paths)) def get_all_commands(self, with_aliases=True): r = [self.prog] if with_aliases: r.extend(self.aliases) return r # ============================================================================= # bash.py # ============================================================================= def make_switch_case_pattern(option_strings): ''' Generates a switch case pattern for `option_strings'. ''' r = [] for option_string in sorted(option_strings): if len(option_string) == 2: r.append('-!(-*)' + shell.escape(option_string[1])) else: r.append(shell.escape(option_string)) return '|'.join(r) # ============================================================================= def _generate_options_parsing(self): def get_long_options_case_without_arg(options): return '|'.join(options) def get_long_options_case_with_arg(options): return '|'.join('%s=*' % o for o in options) def get_short_options_case(options): return '|'.join(o[1] for o in options) options = self.commandline.get_options(with_parent_options=True) if self.commandline.abbreviate_options: abbreviations = self._get_AbbreviationGeneratorForOptions(options) else: abbreviations = utils.DummyAbbreviationGenerator() local = ' '.join( '%s=0 %s=\'\'' % ( make_option_variable_name(o, prefix='HAVE_'), make_option_variable_name(o, prefix='VALUE_') ) for o in options ) case_long_options = [] case_short_options = [] for option in options: long_options = abbreviations.get_many_abbreviations(option.get_long_option_strings()) long_options += abbreviations.get_many_abbreviations(option.get_old_option_strings()) short_options = option.get_short_option_strings() if long_options: if option.takes_args == '?': r = '%s)\n' % get_long_options_case_without_arg(long_options) r += ' %s=1;;\n' % make_option_variable_name(option, prefix='HAVE_') r += '%s)\n' % get_long_options_case_with_arg(long_options) r += ' %s=1\n' % make_option_variable_name(option, prefix='HAVE_') r += ' %s="${arg#*=}";;' % make_option_variable_name(option, prefix='VALUE_') case_long_options.append(r) elif option.takes_args: r = '%s)\n' % get_long_options_case_without_arg(long_options) r += ' %s=1\n' % make_option_variable_name(option, prefix='HAVE_') r += ' %s="${words[++argi]}";;\n' % make_option_variable_name(option, prefix='VALUE_') r += '%s)\n' % get_long_options_case_with_arg(long_options) r += ' %s=1\n' % make_option_variable_name(option, prefix='HAVE_') r += ' %s="${arg#*=}";;' % make_option_variable_name(option, prefix='VALUE_') case_long_options.append(r) else: r = '%s)\n' % get_long_options_case_without_arg(long_options) r += ' %s=1;;' % make_option_variable_name(option, prefix='HAVE_') case_long_options.append(r) if short_options: if option.takes_args == '?': r = '%s)\n' % get_short_options_case(short_options) r += ' %s=1\n' % make_option_variable_name(option, prefix='HAVE_') r += ' if $has_trailing_chars; then\n' r += ' %s="${arg:$((i + 1))}"\n' % make_option_variable_name(option, 'VALUE_') r += ' fi\n' r += ' break;;' case_short_options.append(r) elif option.takes_args: r = '%s)\n' % get_short_options_case(short_options) r += ' %s=1\n' % make_option_variable_name(option, prefix='HAVE_') r += ' if $has_trailing_chars\n' r += ' then %s="${arg:$((i + 1))}"\n' % make_option_variable_name(option, 'VALUE_') r += ' else %s="${words[++argi]}"\n' % make_option_variable_name(option, 'VALUE_') r += ' fi\n' r += ' break;;' case_short_options.append(r) else: r = '%s)\n' % get_short_options_case(short_options) r += ' %s=1;;' % make_option_variable_name(option, prefix='HAVE_') case_short_options.append(r) s = '''\ local -a POSITIONALS local END_OF_OPTIONS=0 POSITIONAL_NUM=0 %LOCALS% local argi for ((argi=1; argi < ${#words[@]} - 1; ++argi)); do local arg="${words[argi]}" case "$arg" in %CASE_LONG_OPTIONS% --) END_OF_OPTIONS=1 for ((++argi; argi < ${#words[@]}; ++argi)); do POSITIONALS[POSITIONAL_NUM++]="${words[argi]}" done break;; --*) ;; -) POSITIONALS[POSITIONAL_NUM++]="-";; -*) local i for ((i=1; i < ${#arg}; ++i)); do local char="${arg:$i:1}" local has_trailing_chars=$( (( i + 1 < ${#arg} )) && echo true || echo false) case "$char" in %CASE_SHORT_OPTIONS% esac done;; *) POSITIONALS[POSITIONAL_NUM++]="$arg";; esac done for ((; argi < ${#words[@]}; ++argi)); do local arg="${words[$argi]}" case "$arg" in -) POSITIONALS[POSITIONAL_NUM++]="$arg";; -*);; *) POSITIONALS[POSITIONAL_NUM++]="$arg";; esac done''' s = s.replace('%LOCALS%', local) if len(case_long_options): s = s.replace('%CASE_LONG_OPTIONS%', utils.indent('\n'.join(case_long_options), 4)) else: s = s.replace('%CASE_LONG_OPTIONS%\n', '') if len(case_short_options): s = s.replace('%CASE_SHORT_OPTIONS%', utils.indent('\n'.join(case_short_options), 10)) else: s = s.replace('%CASE_SHORT_OPTIONS%\n', '') return s # ============================================================================= _BASH_HELPER = helpers.ShellFunction('bash_helper', r''' # =========================================================================== # # This function is for querying the command line. # # COMMANDS # setup # This is the first call you have to make, otherwise the other commands # won't (successfully) work. # # It parses accordings to and stores results in the # variables POSITIONALS, HAVING_OPTIONS and OPTION_VALUES. # # The first argument is a comma-seperated list of options that the parser # should know about. Short options (-o), long options (--option), and # old-style options (-option) are supported. # # If an option takes an argument, it is suffixed by '='. # If an option takes an optional argument, it is suffixed by '=?'. # # get_positional # Prints out the positional argument number NUM (starting from 1) # # has_option # Checks if a option given in OPTIONS is passed on commandline. # # option_is -- # Checks if one option in OPTIONS has a value of VALUES. # # EXAMPLE # local POSITIONALS HAVING_OPTIONS OPTION_VALUES # zsh_helper setup '-f,-a=,-optional=?' program_name -f -optional -a foo bar # zsh_helper has_option -f # zsh_helper option_is -a -- foo # # Here, -f is a flag, -a takes an argument, and -optional takes an optional # argument. # # Both queries return true. # # =========================================================================== local FUNC="bash_helper" local CONTAINS="__contains" __contains() { local ARG KEY="$1"; shift for ARG; do [[ "$KEY" == "$ARG" ]] && return 0; done return 1 } if [[ $# == 0 ]]; then echo "$FUNC: missing command" >&2 return 1; fi local CMD=$1 shift case "$CMD" in get_positional) if test $# -ne 1; then echo "$FUNC: get_positional: takes exactly one argument" >&2 return 1; fi if test "$1" -eq 0; then echo "$FUNC: get_positional: positionals start at 1, not 0!" >&2 return 1 fi printf "%s" "${POSITIONALS[$1]}" return 0 ;; has_option) if test $# -eq 0; then echo "$FUNC: has_option: arguments required" >&2 return 1; fi local OPTION='' for OPTION in "${HAVING_OPTIONS[@]}"; do $CONTAINS "$OPTION" "$@" && return 0 done return 1 ;; option_is) local -a CMD_OPTION_IS_OPTIONS=() CMD_OPTION_IS_VALUES=() local END_OF_OPTIONS_NUM=0 while test $# -ge 1; do if [[ "$1" == "--" ]]; then (( ++END_OF_OPTIONS_NUM )) elif test $END_OF_OPTIONS_NUM -eq 0; then CMD_OPTION_IS_OPTIONS+=("$1") elif test $END_OF_OPTIONS_NUM -eq 1; then CMD_OPTION_IS_VALUES+=("$1") fi shift done if test ${#CMD_OPTION_IS_OPTIONS[@]} -eq 0; then echo "$FUNC: option_is: missing options" >&2 return 1 fi if test ${#CMD_OPTION_IS_VALUES[@]} -eq 0; then echo "$FUNC: option_is: missing values" >&2 return 1 fi local I=$(( ${#HAVING_OPTIONS[@]} - 1)) while test $I -ge 0; do local OPTION="${HAVING_OPTIONS[$I]}" if $CONTAINS "$OPTION" "${CMD_OPTION_IS_OPTIONS[@]}"; then local VALUE="${OPTION_VALUES[$I]}" $CONTAINS "$VALUE" "${CMD_OPTION_IS_VALUES[@]}" && return 0 fi (( --I )) done return 1 ;; setup) local IFS=',' local -a OPTIONS=(${1}) unset IFS shift ;; *) echo "$FUNC: argv[1]: invalid command" >&2 return 1 ;; esac # continuing setup.... # =========================================================================== # Parsing of available options # =========================================================================== local -a OLD_OPTS_WITH_ARG OLD_OPTS_WITH_OPTIONAL_ARG OLD_OPTS_WITHOUT_ARG local -a LONG_OPTS_WITH_ARG LONG_OPTS_WITH_OPTIONAL_ARG LONG_OPTS_WITHOUT_ARG local -a SHORT_OPTS_WITH_ARG SHORT_OPTS_WITH_OPTIONAL_ARG SHORT_OPTS_WITHOUT_ARG local OPTION for OPTION in "${OPTIONS[@]}"; do case "$OPTION" in --?*=) LONG_OPTS_WITH_ARG+=("${OPTION%=}");; --?*=\?) LONG_OPTS_WITH_OPTIONAL_ARG+=("${OPTION%=?}");; --?*) LONG_OPTS_WITHOUT_ARG+=("$OPTION");; -?=) SHORT_OPTS_WITH_ARG+=("${OPTION%=}");; -?=\?) SHORT_OPTS_WITH_OPTIONAL_ARG+=("${OPTION%=?}");; -?) SHORT_OPTS_WITHOUT_ARG+=("$OPTION");; -??*=) OLD_OPTS_WITH_ARG+=("${OPTION%=}");; -??*=\?) OLD_OPTS_WITH_OPTIONAL_ARG+=("${OPTION%=?}");; -??*) OLD_OPTS_WITHOUT_ARG+=("$OPTION");; *) echo "$FUNC: $OPTION: not a valid short, long or oldstyle option" >&2; return 1;; esac done # =========================================================================== # Parsing of command line options # =========================================================================== POSITIONALS=() HAVING_OPTIONS=() OPTION_VALUES=() local ARGI=2 # ARGI[1] is program name while [[ $ARGI -le $# ]]; do local ARG="${!ARGI}" local HAVE_TRAILING_ARG=$(test $ARGI -lt $# && echo true || echo false) case "$ARG" in -) POSITIONALS+=(-);; --) for ARGI in $(seq $((ARGI + 1)) $#); do POSITIONALS+=("${@[$ARGI]}") done break;; --*) for OPTION in "${LONG_OPTS_WITH_ARG[@]}" "${LONG_OPTS_WITHOUT_ARG[@]}" "${LONG_OPTS_WITH_OPTIONAL_ARG[@]}"; do if [[ "$ARG" == "$OPTION="* ]]; then HAVING_OPTIONS+=("$OPTION") OPTION_VALUES+=("${ARG#$OPTION=}") break elif [[ "$ARG" == "$OPTION" ]]; then if $CONTAINS "$OPTION" "${LONG_OPTS_WITH_ARG[@]}"; then if $HAVE_TRAILING_ARG; then HAVING_OPTIONS+=("$OPTION") OPTION_VALUES+=("${@[$((ARGI + 1))]}") (( ARGI++ )) fi else HAVING_OPTIONS+=("$OPTION") OPTION_VALUES+=("") fi break fi done;; -*) local HAVE_MATCH=false for OPTION in "${OLD_OPTS_WITH_ARG[@]}" "${OLD_OPTS_WITHOUT_ARG[@]}" "${OLD_OPTS_WITH_OPTIONAL_ARG[@]}"; do if [[ "$ARG" == "$OPTION="* ]]; then HAVING_OPTIONS+=("$OPTION") OPTION_VALUES+=("${ARG#$OPTION=}") HAVE_MATCH=true break elif [[ "$ARG" == "$OPTION" ]]; then if $CONTAINS "$OPTION" "${OLD_OPTS_WITH_ARG[@]}"; then if $HAVE_TRAILING_ARG; then HAVING_OPTIONS+=("$OPTION") OPTION_VALUES+=("${@:$((ARGI + 1)):1}") (( ARGI++ )) fi else HAVING_OPTIONS+=("$OPTION") OPTION_VALUES+=("") fi HAVE_MATCH=true break fi done if ! $HAVE_MATCH; then local ARG_LENGTH=${#ARG} local I=1 local IS_END=false while ! $IS_END && test $I -lt $ARG_LENGTH; do local ARG_CHAR="${ARG:$I:1}" local HAVE_TRAILING_CHARS=$(test $((I+1)) -lt $ARG_LENGTH && echo true || echo false) for OPTION in "${SHORT_OPTS_WITH_ARG[@]}" "${SHORT_OPTS_WITHOUT_ARG[@]}" "${SHORT_OPTS_WITH_OPTIONAL_ARG[@]}"; do local OPTION_CHAR="${OPTION:1:1}" if test "$ARG_CHAR" = "$OPTION_CHAR"; then if $CONTAINS "$OPTION" "${SHORT_OPTS_WITH_ARG[@]}"; then if $HAVE_TRAILING_CHARS; then HAVING_OPTIONS+=("$OPTION") OPTION_VALUES+=("${ARG:$((I+1))}") IS_END=true elif $HAVE_TRAILING_ARG; then HAVING_OPTIONS+=("$OPTION") OPTION_VALUES+=("${@:$((ARGI + 1)):1}") (( ARGI++ )) IS_END=true fi elif $CONTAINS "$OPTION" "${SHORT_OPTS_WITH_OPTIONAL_ARG[@]}"; then HAVING_OPTIONS+=("$OPTION") if $HAVE_TRAILING_CHARS; then IS_END=true OPTION_VALUES+=("${ARG:$((I+1))}") else OPTION_VALUES+=("") fi else HAVING_OPTIONS+=("$OPTION") OPTION_VALUES+=("") fi break fi done (( I++ )) done fi;; *) POSITIONALS+=("$ARG");; esac (( ARGI++ )) done ''') # ============================================================================= _EXPORTED_VARIABLES = helpers.ShellFunction('exported_variables', r''' COMPREPLY=() local var for var in $(declare -p -x | sed 's/=.*//; s/.* //'); do [[ "$var" == "$cur"* ]] && COMPREPLY+=("$var") done ''') # ============================================================================= def get_subcommands_choices(subcommands_option): r = OrderedDict() for commandline in subcommands_option.subcommands: for command in get_all_command_variations(commandline): r[command] = commandline.help return r crazy-complete-crazy-complete-bc5afec/crazy_complete/__init__.py000066400000000000000000000011531520061347500253720ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Exporting modules.''' from . import errors from . import cli, config, utils from . import shell, bash, fish, zsh from . import argparse_source, json_source, yaml_source from . import extended_yaml_parser from . import scheme_validator from . import help_parser __all__ = [ 'errors', 'cli', 'config', 'utils', 'shell', 'bash', 'fish', 'zsh', 'argparse_source', 'json_source', 'yaml_source', 'extended_yaml_parser', 'scheme_validator', 'help_parser', ] crazy-complete-crazy-complete-bc5afec/crazy_complete/algo.py000066400000000000000000000041331520061347500245560ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Algorithm functions.''' from collections import OrderedDict def flatten(iterable): '''Flatten a list of iterables into a single list.''' result = [] for item in iterable: result.extend(item) return result def uniq(iterable): '''Return list of unique items, preserving order.''' result = [] seen = set() for item in iterable: if item not in seen: seen.add(item) result.append(item) return result def partition(iterable, predicate): '''Split an iterable into two lists based on a predicate. Args: iterable: The input iterable to split. predicate (callable): A function returning True or False for each element. Returns: tuple: (matched, unmatched) where matched -- list of items for which predicate(item) is True unmatched -- list of items for which predicate(item) is False ''' a, b = [], [] for item in iterable: if predicate(item): a.append(item) else: b.append(item) return (a, b) def group_by(iterable, keyfunc): '''Group items from an iterable by a key function. Args: iterable: The input iterable of items to group. keyfunc (callable): Function applied to each item to determine its group key. Returns: OrderedDict: A mapping of key -> list of items with that key, preserving the order of first appearance of each key. ''' result = OrderedDict() for item in iterable: val = keyfunc(item) if val in result: result[val].append(item) else: result[val] = [item] return result def numbers_are_contiguous(numbers): '''Return True if the list of integers is contiguous (1, 2, 3, 4, ...)''' if not numbers: return False sorted_nums = sorted(numbers) return all(b - a == 1 for a, b in zip(sorted_nums, sorted_nums[1:])) crazy-complete-crazy-complete-bc5afec/crazy_complete/application.py000066400000000000000000000304771520061347500261510ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Class for main command line application.''' import os import sys import shlex import argparse from .errors import CrazyError from . import bash, fish, zsh from . import argparse_source, json_source, yaml_source from . import argparse_mod # .complete() from . import help_converter from . import utils from . import config from . import paths # The import of `argparse_mod` only modifies the classes provided by the # argparse module. We use the dummy function to silence warnings about the # unused import. argparse_mod.dummy() def boolean(string): '''Convert string to bool, else raise ValueError.''' try: return {'true': True, 'false': False}[string.lower()] except KeyError as e: raise ValueError(f"Not a bool: {string}") from e def version(string): '''Parse a version.''' return tuple(map(int, string.split('.'))) def feature_list(string): '''Convert a comma separated string of features to list.''' features = string.split(',') for feature in features: if feature not in ('hidden', 'final', 'groups', 'repeatable', 'when'): raise ValueError(f"Invalid feature: {feature}") return features _URL = "https://github.com/crazy-complete/crazy-complete" _EPILOG = f"For complete documentation, see {_URL}" p = argparse.ArgumentParser( 'crazy-complete', description='Generate shell auto completion files for all major shells', epilog=_EPILOG, exit_on_error=False) p.add_argument( 'shell', choices=('bash', 'fish', 'zsh', 'json', 'yaml'), help='Specify the shell type for the completion script') p.add_argument( 'definition_file', help='The file containing the command line definitions' ).complete('file') p.add_argument( '--version', action='version', version='%(prog)s 0.3.8', help='Show program version') p.add_argument( '--manual', metavar='TOPIC', nargs='?', help='Show manual for topic') p.add_argument( '--parser-variable', default=None, help='Specify the variable name of the ArgumentParser object (for --input-type=python)') p.add_argument( '--input-type', choices=('yaml', 'json', 'python', 'help', 'auto'), default='auto', help='Specify input file type') p.add_argument( '--abbreviate-commands', metavar='BOOL', default=False, type=boolean, help='Sets whether commands can be abbreviated' ).complete('choices', ('True', 'False')) p.add_argument( '--abbreviate-options', metavar='BOOL', default=False, type=boolean, help='Sets whether options can be abbreviated' ).complete('choices', ('True', 'False')) p.add_argument( '--repeatable-options', metavar='BOOL', default=False, type=boolean, help='Sets whether options are suggested multiple times during completion' ).complete('choices', ('True', 'False')) p.add_argument( '--inherit-options', metavar='BOOL', default=False, type=boolean, help='Sets whether parent options are visible to subcommands' ).complete('choices', ('True', 'False')) p.add_argument( '--option-stacking', metavar='BOOL', default=True, type=boolean, help='Sets whether short option stacking is allowed' ).complete('choices', ('True', 'False')) p.add_argument( '--long-option-argument-separator', default='both', choices=('space', 'equals', 'both'), dest='long_opt_arg_sep', help='Sets which separators are used for delimiting a long option from its argument' ).complete('choices', ('space', 'equals', 'both')) p.add_argument( '--disable', metavar='FEATURES', default=[], type=feature_list, help='Disable features (hidden,final,groups,repeatable,when)' ).complete('value_list', {'values': { 'hidden': 'Disable hidden options', 'final': 'Disable final options', 'groups': 'Disable option groups', 'repeatable': 'Disable check for repeatable options', 'when': 'Disable conditional options and positionals', }}) p.add_argument( '--vim-modeline', metavar='BOOL', default=True, type=boolean, help='Sets whether a vim modeline comment shall be appended to the generated code' ).complete('choices', ('True', 'False')) p.add_argument( '--bash-completions-version', metavar='VERSION', default=(2,), type=version, help='Generate code for a specific bash-completions version' ) p.add_argument( '--zsh-compdef', metavar='BOOL', default=True, type=boolean, help='Sets whether #compdef is used in zsh scripts' ).complete('choices', ('True', 'False')) p.add_argument( '--fish-fast', metavar='BOOL', default=False, type=boolean, help='Use faster commandline parsing at the cost of correctness' ).complete('choices', ('True', 'False')) p.add_argument( '--fish-inline-conditions', metavar='BOOL', default=False, type=boolean, help="Don't store conditions in a variable" ).complete('choices', ('True', 'False')) p.add_argument( '--include-file', metavar='FILE', action='append', help='Include file in output' ).complete('file') p.add_argument( '--comment', metavar='COMMENT', action='append', help='Add a comment to output' ) p.add_argument( '--debug', action='store_true', default=False, help='Enable debug mode') p.add_argument( '--keep-comments', action='store_true', default=False, help='Keep comments in generated output') p.add_argument( '--max-line-length', default=80, type=int, help='Set the maximum line length of the generated output' ) p.add_argument( '--function-prefix', metavar='PREFIX', default='_$PROG', help='Set the prefix used for generated functions') grp = p.add_mutually_exclusive_group() grp.add_argument( '-o', '--output', metavar='FILE', default=None, dest='output_file', help='Write output to destination file [default: stdout]' ).complete('file') grp.add_argument( '-i', '--install-system-wide', default=False, action='store_true', help='Write output to the system-wide completions dir of shell') grp.add_argument( '-u', '--uninstall-system-wide', default=False, action='store_true', help='Uninstall the system-wide completion file for program') # We use a unique object name for avoiding name clashes when # importing/executing the foreign python script _crazy_complete_argument_parser = p del p def write_string_to_file(string, file): '''Writes string to file if file is given.''' if file is not None: with open(file, 'w', encoding='utf-8') as fh: fh.write(string) else: print(string) def load_definition_file(opts): '''Load a definition file as specified in `opts`.''' if opts.input_type == 'auto': basename = os.path.basename(opts.definition_file) extension = os.path.splitext(basename)[1].lower().strip('.') if extension == 'json': return json_source.load_from_file(opts.definition_file) if extension in ('yaml', 'yml'): return yaml_source.load_from_file(opts.definition_file) if extension == 'py': msg = 'Reading Python files must be enabled by --input-type=python' raise CrazyError(msg) if extension == '': msg = ('File has no extension. ' 'Please supply --input-type=json|yaml|help|python') raise CrazyError(msg) msg = (f'Unknown file extension `{extension}`. ' 'Please supply --input-type=json|yaml|help|python') raise CrazyError(msg) if opts.input_type == 'json': return json_source.load_from_file(opts.definition_file) if opts.input_type == 'yaml': return yaml_source.load_from_file(opts.definition_file) if opts.input_type == 'python': return argparse_source.load_from_file( opts.definition_file, opts.parser_variable, parser_blacklist=[_crazy_complete_argument_parser]) raise AssertionError("Should not be reached") def _get_config_from_options(opts): conf = config.Config() conf.set_debug(opts.debug) conf.set_function_prefix(opts.function_prefix) conf.set_abbreviate_commands(opts.abbreviate_commands) conf.set_abbreviate_options(opts.abbreviate_options) conf.set_repeatable_options(opts.repeatable_options) conf.set_inherit_options(opts.inherit_options) conf.set_option_stacking(opts.option_stacking) conf.set_long_option_argument_separator(opts.long_opt_arg_sep) conf.set_vim_modeline(opts.vim_modeline) conf.set_bash_completions_version(opts.bash_completions_version) conf.set_zsh_compdef(opts.zsh_compdef) conf.set_fish_fast(opts.fish_fast) conf.set_fish_inline_conditions(opts.fish_inline_conditions) conf.include_many_files(opts.include_file or []) conf.set_keep_comments(opts.keep_comments) conf.add_comments(opts.comment or []) conf.set_line_length(opts.max_line_length) for feature in opts.disable: if feature == 'hidden': conf.disable_hidden(True) elif feature == 'final': conf.disable_final(True) elif feature == 'groups': conf.disable_groups(True) elif feature == 'repeatable': conf.disable_repeatable(True) elif feature == 'when': conf.disable_when(True) return conf def try_print_manual(args): '''Print manual.''' parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--manual', nargs='?', const='all') opts, _ = parser.parse_known_args(args) if not opts.manual: return from . import manual manual.print_help_topic(opts.manual, os.isatty(sys.stdout.fileno())) sys.exit(0) def try_batch(args): '''Do batch processing.''' parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--batch') opts, _ = parser.parse_known_args(args) if not opts.batch: return with open(opts.batch, 'r', encoding='utf-8') as fh: content = fh.read() for line in content.split('\n'): line = line.strip() if not line or line.startswith('#'): continue utils.print_err('Running:', sys.argv[0], line) app = Application() app.parse_args(shlex.split(line)) app.run() sys.exit(0) def generate(opts): '''Generate output file as specified in `opts`.''' if opts.input_type == 'help': if opts.shell != 'yaml': raise CrazyError('The `help` input-type currently only supports YAML generation') output = help_converter.from_file_to_yaml(opts.definition_file) write_string_to_file(output, opts.output_file) return cmdline = load_definition_file(opts) if opts.shell == 'json': output = json_source.commandline_to_json(cmdline) write_string_to_file(output, opts.output_file) return if opts.shell == 'yaml': output = yaml_source.commandline_to_yaml(cmdline) write_string_to_file(output, opts.output_file) return conf = _get_config_from_options(opts) if opts.shell == 'bash': output = bash.generate_completion(cmdline, conf) elif opts.shell == 'fish': output = fish.generate_completion(cmdline, conf) elif opts.shell == 'zsh': output = zsh.generate_completion(cmdline, conf) if opts.install_system_wide or opts.uninstall_system_wide: file = { 'bash': paths.get_bash_completion_file, 'fish': paths.get_fish_completion_file, 'zsh': paths.get_zsh_completion_file, }[opts.shell](cmdline.prog) if opts.install_system_wide: utils.print_err(f'Installing to {file}') write_string_to_file(output, file) else: utils.print_err(f'Removing {file}') os.remove(file) else: write_string_to_file(output, opts.output_file) class Application: '''Class for main command line application.''' def __init__(self): self.options = None def parse_args(self, args): '''Parse command line arguments. Raises: - argparse.ArgumentError ''' try_print_manual(args) try_batch(args) self.options = _crazy_complete_argument_parser.parse_args(args) def run(self): '''Run the crazy-complete program. Raises: - CrazyError - FileNotFoundError - yaml.scanner.ScannerError - yaml.parser.ParserError - json.decoder.JSONDecodeError ''' generate(self.options) crazy-complete-crazy-complete-bc5afec/crazy_complete/argparse_mod.py000066400000000000000000000044001520061347500262740ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth """ This module extends Python's built-in argparse library by adding several custom methods. """ import argparse from .cli import ExtendedBool # We have to use implementation details of the argparse module... # pylint: disable=protected-access def dummy(): '''Since this module only modifies the classes by argparse we provide this function so it can be called to silence warnings.''' def _action_complete(self, command, *args): setattr(self, '_complete', (command, *args)) return self def _action_get_complete(self): return getattr(self, '_complete', None) def _action_when(self, when): setattr(self, '_when', when) return self def _action_get_when(self): return getattr(self, '_when', None) def _action_set_repeatable(self, enable=True): setattr(self, '_repeatable', enable) return self def _action_get_repeatable(self): return getattr(self, '_repeatable', ExtendedBool.INHERIT) def _action_set_final(self, enable=True): setattr(self, '_final', enable) return self def _action_get_final(self): return getattr(self, '_final', False) def _parser_alias(self, alias): setattr(self, '_aliases', [alias]) return self def _parser_aliases(self, aliases): setattr(self, '_aliases', list(aliases)) return self def _parser_get_aliases(self): return getattr(self, '_aliases', []) def _parser_remove_help(self): self._actions.pop(0) self._option_string_actions.pop('-h') self._option_string_actions.pop('--help') argparse.Action.complete = _action_complete argparse.Action.get_complete = _action_get_complete argparse.Action.when = _action_when argparse.Action.get_when = _action_get_when argparse.Action.set_repeatable = _action_set_repeatable argparse.Action.get_repeatable = _action_get_repeatable argparse.Action.set_final = _action_set_final argparse.Action.get_final = _action_get_final argparse.ArgumentParser.alias = _parser_alias argparse.ArgumentParser.aliases = _parser_aliases argparse.ArgumentParser.get_aliases = _parser_get_aliases argparse.ArgumentParser.remove_help = _parser_remove_help crazy-complete-crazy-complete-bc5afec/crazy_complete/argparse_source.py000066400000000000000000000176631520061347500270340ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth """ This module provides functions for creating CommandLine objects from Python's argparse.ArgumentParser. """ import argparse from collections import OrderedDict from . import file_loader from . import utils from .errors import CrazyError from .cli import CommandLine, MutuallyExclusiveGroup # We have to use implementation details of the argparse module... # pylint: disable=protected-access # pylint: disable=redefined-builtin def range_to_complete(r): '''Convert a Python range object to a range complete format.''' start = r.start step = r.step end = r.stop - 1 if step > 0 else r.stop + 1 if step == 1: return ('range', start, end) return ('range', start, end, step) def get_complete(action): ''' Get the `complete` attribute of `action` if it is set. Otherwise determine `complete` from action type. ''' complete = action.get_complete() if isinstance(action, ( argparse._HelpAction, argparse._VersionAction, argparse._StoreTrueAction, argparse._StoreFalseAction, argparse._StoreConstAction, argparse._AppendConstAction, argparse._CountAction)): if complete: utils.warn(f"Action has .complete() set but takes no arguments: {action}") return None if isinstance(action, ( argparse._StoreAction, argparse._ExtendAction, argparse._AppendAction)): if complete: if action.choices: utils.warn(f"Action's choices overridden by .complete(): {action}") return complete if action.choices: if isinstance(action.choices, range): return range_to_complete(action.choices) return ['choices', action.choices] return ['none'] if isinstance(action, argparse.BooleanOptionalAction): raise CrazyError("argparse.BooleanOptionalAction is not supported yet") raise CrazyError(f'Unknown action type: {action}') def get_final(action): '''Return if an argparse action object is final.''' if isinstance(action, (argparse._HelpAction, argparse._VersionAction)): return True return action.get_final() def argumentparser_to_commandline(parser, prog=None, description=None): '''Converts an ArgumentParser object to a CommandLine object. Args: parser (argparse.ArgumentParser): The ArgumentParser object to convert. prog (str, optional): The name of the program. Defaults to the program name of the parser. description (str, optional): The description of the program. Defaults to the description of the parser. Returns: CommandLine: A CommandLine object representing the converted parser. ''' # pylint: disable=too-many-locals # pylint: disable=too-many-branches if not description: description = parser.description if not prog: prog = parser.prog commandline = CommandLine(prog, help=description, aliases=parser.get_aliases()) positional_number = 0 for action in parser._actions: if isinstance(action, argparse._SubParsersAction): subparsers = OrderedDict() for name, subparser in action.choices.items(): subparsers[name] = {'parser': subparser, 'help': None} for subaction in action._get_subactions(): subparsers[subaction.dest]['help'] = subaction.help subcommands = commandline.add_subcommands() for name, data in subparsers.items(): subcmd = argumentparser_to_commandline(data['parser'], name, data['help']) subcommands.add_commandline_object(subcmd) elif not action.option_strings: if action.nargs in ('+', '*'): is_repeatable = True elif action.nargs in (1, None, '?'): is_repeatable = False else: is_repeatable = True utils.warn(f'Truncating nargs={action.nargs} of {action}') positional_number += 1 commandline.add_positional( positional_number, metavar = action.metavar or action.dest, complete = get_complete(action), help = action.help, repeatable = is_repeatable, when = action.get_when() ) else: complete = get_complete(action) optional_arg = False metavar = None help = None if action.nargs == '?': optional_arg = True elif action.nargs not in (None, 0, 1, True): utils.warn(f'Truncating nargs={action.nargs} of {action}') if complete: metavar = action.metavar or action.dest if action.help != argparse.SUPPRESS: help = action.help commandline.add_option( action.option_strings, metavar = metavar, complete = complete, help = help, optional_arg = optional_arg, repeatable = action.get_repeatable(), final = get_final(action), hidden = (action.help == argparse.SUPPRESS), when = action.get_when() ) group_counter = 0 for group in parser._mutually_exclusive_groups: group_counter += 1 group_name = f'group{group_counter}' exclusive_group = MutuallyExclusiveGroup(commandline, group_name) for action in group._group_actions: for option in commandline.get_options(): for option_string in action.option_strings: if option_string in option.option_strings: exclusive_group.add_option(option) break return commandline def find_objects_by_type(module, types): '''Search a module for objects of a specific type.''' r = [] for obj_name in dir(module): obj = getattr(module, obj_name) if isinstance(obj, types): r.append(obj) return r def find_root_argument_parsers(module): '''Return a list of all ArgumentParser objects that have no parent.''' parsers = find_objects_by_type(module, argparse.ArgumentParser) actions = find_objects_by_type(module, argparse._SubParsersAction) for action in actions: for parser in action.choices.values(): try: parsers.remove(parser) except ValueError: pass return parsers def load_from_file(file, parser_variable=None, parser_blacklist=()): ''' Load a Python file, search for the ArgumentParser object and convert it to a CommandLine object. ''' # pylint: disable=broad-exception-caught try: module = file_loader.import_file(file) except Exception as e: utils.warn(e) utils.warn(f"Failed to load `{file}` using importlib, falling back to exec") module = file_loader.execute_file(file) if parser_variable is not None: try: parser = getattr(module, parser_variable) except AttributeError: raise CrazyError(f"No variable named `{parser_variable}` found in `{file}`") from None else: parsers = find_root_argument_parsers(module) for blacklisted in parser_blacklist: try: parsers.remove(blacklisted) except ValueError: pass if len(parsers) == 0: raise CrazyError(f"Could not find any ArgumentParser object in `{file}`") if len(parsers) > 1: raise CrazyError(f"Found too many ArgumentParser objects in `{file}`") parser = parsers[0] return argumentparser_to_commandline(parser) crazy-complete-crazy-complete-bc5afec/crazy_complete/bash.py000066400000000000000000000211021520061347500245440ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for generating a Bash auto completion file.''' from collections import OrderedDict from . import config as config_ from . import shell from . import algo from . import utils from . import helpers from . import bash_helpers from . import bash_complete from . import bash_parser from . import bash_parser_v2 from . import bash_option_completion from . import bash_option_strings_completion from . import bash_positionals_completion from . import bash_versions from . import bash_patterns from .str_utils import indent, join_with_wrap from .bash_utils import VariableManager from .output import Output from . import generation class BashCompletionGenerator: '''Class for generating completions.''' # pylint: disable=too-few-public-methods # pylint: disable=too-many-instance-attributes def __init__(self, ctxt, commandline): self.ctxt = ctxt self.commandline = commandline self.options = commandline.get_options() self.positionals = commandline.get_positionals() self.subcommands = commandline.get_subcommands() self.completer = bash_complete.BashCompleter() self.variable_manager = VariableManager('OPT_') self._generate() def complete_option(self, option): '''Complete an option.''' context = self.ctxt.get_option_context(self.commandline, option) obj = self.completer.complete(context, [], *option.complete) code = obj.get_code(False) if code and option.nosort: code += '\ncompopt -o nosort' return code def _generate_commandline_parsing(self): local_vars = [] for cmdline in self.commandline.get_all_commandlines(): for option in cmdline.options: if option.capture is not None: local_vars.append(option.capture) r = 'local END_OF_OPTIONS POSITIONALS\n' if local_vars: local_vars = algo.uniq(local_vars) line_length = self.ctxt.config.line_length - 2 r += join_with_wrap(' ', '\n', line_length, local_vars, 'local -a ') r += '\n%s' % self.ctxt.helpers.use_function('parse_commandline') return r def _generate_subcommand_call(self): if not self.subcommands: return None positional_num = self.subcommands.get_positional_num() r = 'if (( %i < ${#POSITIONALS[@]} )); then\n' % (positional_num - 1) r += ' case "${POSITIONALS[%i]}" in\n' % (positional_num - 1) for subcommand in self.subcommands.subcommands: cmds = utils.get_all_command_variations(subcommand) pattern = bash_patterns.make_pattern([shell.quote(s) for s in cmds]) funcname = self.ctxt.helpers.make_completion_funcname(subcommand) if utils.is_worth_a_function(subcommand): if self.commandline.inherit_options: r += ' %s) %s && return 0;;\n' % (pattern, funcname) else: r += ' %s) %s && return 0 || return 1;;\n' % (pattern, funcname) else: if self.commandline.inherit_options: r += ' %s);;\n' % pattern else: r += ' %s) return 0;;\n' % pattern r += ' esac\n' r += 'fi' return r def _generate_command_arg_call(self): def is_command_arg(positional): return (positional.complete and positional.complete[0] == 'command_arg') try: positional = list(filter(is_command_arg, self.positionals))[0] except IndexError: return None num = positional.get_positional_num() r = 'if (( ${#POSITIONALS[@]} >= %d )); then\n' % num r += ' local realpos\n' r += ' for realpos in "${!COMP_WORDS[@]}"; do\n' r += ' [[ "${COMP_WORDS[realpos]}" == "${POSITIONALS[%d]}" ]] && {\n' % (num - 2) r += ' %s $realpos\n' % bash_versions.command_offset(self.ctxt) r += ' return 0;\n' r += ' }\n' r += ' done\n' r += 'fi' return r def _generate(self): # The completion function returns 0 (success) if there was a completion match. # This return code is used for dealing with subcommands. if not utils.is_worth_a_function(self.commandline): self.result = '' return code = OrderedDict() code['init_completion'] = None code['command_line_parsing'] = None code['subcommand_call'] = None code['command_arg'] = None code['option_completion'] = None code['option_strings_completion'] = None code['positional_completion'] = None code['subcommand_call'] = self._generate_subcommand_call() code['command_arg'] = self._generate_command_arg_call() code['option_completion'] = bash_option_completion.generate_option_completion(self) code['option_strings_completion'] = bash_option_strings_completion.generate(self) code['positional_completion'] = bash_positionals_completion.generate(self) if self.commandline.parent is None: # The root parser makes those variables local and sets up the completion. r = 'local cur prev words cword split words_dequoted\n' r += '%s -n =: || return\n' % bash_versions.init_completion(self.ctxt) r += self.ctxt.helpers.use_function('dequote_words') code['init_completion'] = r v1 = bash_parser.generate(self.commandline, self.variable_manager) v2 = bash_parser_v2.generate(self.commandline, self.variable_manager) c = v1 if len(v1) < len(v2) else v2 func = helpers.ShellFunction('parse_commandline', c) self.ctxt.helpers.add_function(func) # This sets up END_OF_OPTIONS, POSITIONALS and the OPT_* variables. code['command_line_parsing'] = self._generate_commandline_parsing() r = '%s() {\n' % self.ctxt.helpers.make_completion_funcname(self.commandline) r += '%s\n\n' % indent('\n\n'.join(c for c in code.values() if c), 2) r += ' return 1\n' r += '}' self.result = r def _generate_wrapper(ctxt, commandline): make_completion_funcname = ctxt.helpers.make_completion_funcname completion_funcname = make_completion_funcname(commandline) wrapper_funcname = make_completion_funcname(commandline, '__wrapper') if not commandline.wraps: return (completion_funcname, None) r = '%s %s\n' % (bash_versions.completion_loader(ctxt), commandline.wraps) r += '\n' r += '%s() {\n' % wrapper_funcname r += ' local WRAPS=%s\n' % shell.quote(commandline.wraps) r += ' local COMP_LINE_OLD="$COMP_LINE"\n' r += ' local COMP_POINT_OLD="$COMP_POINT"\n' r += ' local COMP_WORDS_OLD=("${COMP_WORDS[@]}")\n' r += '\n' r += ' COMP_LINE="${COMP_LINE/$1/$WRAPS}"\n' r += ' COMP_WORDS[0]=%s\n' % shell.quote(commandline.wraps) r += ' COMP_POINT=$(( COMP_POINT + ${#WRAPS} - ${#1} ))\n' r += '\n' r += ' %s 0\n' % bash_versions.command_offset(ctxt) r += '\n' r += ' COMP_LINE="$COMP_LINE_OLD"\n' r += ' COMP_POINT="$COMP_POINT_OLD"\n' r += ' COMP_WORDS=("${COMP_WORDS_OLD[@]}")\n' r += ' local COMPREPLY_OLD=("${COMPREPLY[@]}")\n' r += ' %s "$@"\n' % completion_funcname r += '\n' r += ' COMPREPLY=("${COMPREPLY_OLD[@]}" "${COMPREPLY[@]}")\n' r += '}' return (wrapper_funcname, r) def generate_completion(commandline, config=None): '''Code for generating a Bash auto completion file.''' if config is None: config = config_.Config() commandline = generation.enhance_commandline(commandline, config) helpers = bash_helpers.BashHelpers(config, commandline.prog) ctxt = generation.GenerationContext(config, helpers) if ctxt.config.bash_completions_version >= (2, 12): helpers.define('bash_completions_v_2_12') result = generation.visit_commandlines(BashCompletionGenerator, ctxt, commandline) completion_func, wrapper_code = _generate_wrapper(ctxt, commandline) output = Output(config, helpers) output.add_generation_notice() output.add_comments() output.add_included_files() output.add_helper_functions_code() output.extend(generator.result for generator in result) output.add(wrapper_code) output.add('complete -F %s %s' % ( completion_func, ' '.join([commandline.prog] + commandline.aliases) )) output.add_vim_modeline('sh') return output.get() crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_complete.py000066400000000000000000000352651520061347500264530ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains code for completing arguments in Bash.''' from . import shell from . import bash_versions from .str_utils import indent from .type_utils import is_dict_type from .bash_utils import make_file_extension_pattern class BashCompletionBase: '''Base class for Bash completions.''' def get_code(self, append=False): '''Returns the code used for completing an argument. If `append` is `True`, then the results will be appended to $COMPREPLY, otherwise $COMPREPLY will be truncated. Args: append (bool): If True, the results will be appended to $COMPREPLY. Returns: str: The command for Bash completion. ''' raise NotImplementedError def get_function(self): '''Returns a function.''' raise NotImplementedError class BashCompletionCode(BashCompletionBase): '''Used for completion code that internally modify $COMPREPLY.''' def __init__(self, ctxt, code): self.ctxt = ctxt self.code = code def get_code(self, append=False): if not self.code: return '' r = [] if append: r.append('local COMPREPLY_OLD=("${COMPREPLY[@]}")') r.append(self.code) if append: r.append('COMPREPLY=("${COMPREPLY_OLD[@]}" "${COMPREPLY[@]}")') return '\n'.join(r) def get_function(self): if not self.code: return 'true' return self.ctxt.helpers.add_dynamic_func(self.ctxt, self.get_code()) class BashCompletionFunc(BashCompletionBase): '''Used for completion functions that internally modify $COMPREPLY.''' def __init__(self, ctxt, args): self.ctxt = ctxt self.args = args def get_code(self, append=False): r = [] if append: r.append('local COMPREPLY_OLD=("${COMPREPLY[@]}")') r.append(shell.join_quoted(self.args)) if append: r.append('COMPREPLY=("${COMPREPLY_OLD[@]}" "${COMPREPLY[@]}")') return '\n'.join(r) def get_function(self): if len(self.args) == 1: return self.args[0] return self.ctxt.helpers.add_dynamic_func(self.ctxt, self.get_code()) class CompgenW(BashCompletionBase): '''Used for completing a list of words.''' def __init__(self, ctxt, values): self.ctxt = ctxt self.values = [str(value) for value in values] def get_code(self, append=False): needs_quote = any(filter(shell.needs_quote, self.values)) if not needs_quote: return 'COMPREPLY%s=($(compgen -W %s -- "$cur"))' % ( ('+' if append else ''), shell.quote(' '.join(self.values))) return ('%s %s-- %s' % ( self.ctxt.helpers.use_function('values'), ('-a ' if append else ''), shell.join_quoted(self.values))) def get_function(self): return self.ctxt.helpers.add_dynamic_func(self.ctxt, self.get_code()) class BashCompletionCompgen(BashCompletionBase): '''Used for completion that uses Bash's `compgen` command.''' def __init__(self, ctxt, compgen_args, code=None): self.ctxt = ctxt self.compgen_args = compgen_args self.code = code def get_code(self, append=False): compgen_cmd = 'COMPREPLY%s=($(compgen %s -- "$cur"))' % ( ('+' if append else ''), self.compgen_args) if not self.code: return compgen_cmd return f'{self.code}\n{compgen_cmd}' def get_function(self): return self.ctxt.helpers.add_dynamic_func(self.ctxt, self.get_code()) class BashCompleteCombine(BashCompletionBase): '''Used for combining multiple completion commands.''' def __init__(self, ctxt, trace, completer, commands): self.ctxt = ctxt self.completion_objects = [] for command_args in commands: obj = completer.complete_from_def(ctxt, trace, command_args) self.completion_objects.append(obj) def get_code(self, append=False): code = [self.completion_objects[0].get_code(append=append)] for obj in self.completion_objects[1:]: code.append(obj.get_code(append=True)) return '\n'.join(code) def get_function(self): return self.ctxt.helpers.add_dynamic_func(self.ctxt, self.get_code()) class BashCompleteKeyValueList(BashCompletionCode): '''Used for completing a list of key-value pairs.''' # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments def __init__(self, ctxt, trace, completer, pair_separator, value_separator, values): funcs = {} for key, _desc, complete in values: if not complete: funcs[key] = 'false' elif complete[0] == 'none': funcs[key] = 'true' else: obj = completer.complete_from_def(ctxt, trace, complete) funcs[key] = obj.get_function() q = shell.quote args = ' \\\n'.join('%s %s' % (q(k), q(f)) for k, f in funcs.items()) code = '%s %s %s \\\n%s' % ( ctxt.helpers.use_function('key_value_list'), shell.quote(pair_separator), shell.quote(value_separator), indent(args, 2) ) super().__init__(ctxt, code) class BashCompleter(shell.ShellCompleter): '''Code generator for completing arguments in Bash.''' # pylint: disable=missing-function-docstring # pylint: disable=too-many-public-methods # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments def none(self, ctxt, _trace, *_): return BashCompletionCode(ctxt, '') def integer(self, ctxt, _trace, options=None): suffixes = [] if options: if 'suffixes' in options: for suffix, description in options['suffixes'].items(): suffixes.append(f'{suffix}:{description}') if not suffixes: return BashCompletionCode(ctxt, '') func = ctxt.helpers.use_function('number') return BashCompletionFunc(ctxt, [func, *suffixes]) def float(self, ctxt, trace, options=None): return self.integer(ctxt, trace, options) def choices(self, ctxt, _trace, choices): return CompgenW(ctxt, choices) def command(self, ctxt, _trace, opts=None): code = None path = None append = None prepend = None if opts: path = opts.get('path', None) append = opts.get('path_append', None) prepend = opts.get('path_prepend', None) if path: code = 'local -x PATH=%s' % shell.quote(path) elif append and prepend: append = shell.quote(append) prepend = shell.quote(prepend) code = 'local -x PATH=%s:"$PATH":%s' % (prepend, append) elif append: code = 'local -x PATH="$PATH":%s' % shell.quote(append) elif prepend: code = 'local -x PATH=%s:"$PATH"' % shell.quote(prepend) return BashCompletionCompgen(ctxt, '-A command', code=code) def directory(self, ctxt, _trace, opts=None): directory = None if opts is None else opts.get('directory', None) filedir = bash_versions.filedir(ctxt) if directory: r = 'builtin pushd %s &>/dev/null && {\n' % shell.quote(directory) r += ' %s -d\n' % filedir r += ' builtin popd >/dev/null\n' r += '}' return BashCompletionCode(ctxt, r) return BashCompletionFunc(ctxt, [filedir, '-d']) def file(self, ctxt, _trace, opts=None): fuzzy = False directory = None extensions = None ignore_globs = None if opts: fuzzy = opts.get('fuzzy', False) directory = opts.get('directory', None) extensions = opts.get('extensions', None) ignore_globs = opts.get('ignore_globs', None) args = [bash_versions.filedir(ctxt)] if extensions: args.append(make_file_extension_pattern(extensions, fuzzy)) if not directory and not ignore_globs: return BashCompletionFunc(ctxt, args) if directory: r = 'builtin pushd %s &>/dev/null && {\n' % shell.quote(directory) r += ' %s\n' % shell.join_quoted(args) r += ' builtin popd >/dev/null\n' r += '}' else: r = shell.join_quoted(args) if ignore_globs: func = ctxt.helpers.use_function('file_filter') r += '\n%s %s' % (func, shell.join_quoted(ignore_globs)) return BashCompletionCode(ctxt, r) def mime_file(self, ctxt, _trace, pattern): func = ctxt.helpers.use_function('mime_file') return BashCompletionFunc(ctxt, [func, pattern]) def group(self, ctxt, _trace): return BashCompletionCompgen(ctxt, '-A group') def hostname(self, ctxt, _trace): return BashCompletionCompgen(ctxt, '-A hostname') def pid(self, ctxt, _trace): return BashCompletionFunc(ctxt, [bash_versions.pids(ctxt)]) def process(self, ctxt, _trace): return BashCompletionFunc(ctxt, [bash_versions.pnames(ctxt)]) def range(self, ctxt, _trace, start, stop, step=1): if step == 1: return BashCompletionCompgen(ctxt, f"-W '{{{start}..{stop}}}'") return BashCompletionCompgen(ctxt, f"-W '{{{start}..{stop}..{step}}}'") def service(self, ctxt, _trace): return BashCompletionCompgen(ctxt, '-A service') def user(self, ctxt, _trace): return BashCompletionCompgen(ctxt, '-A user') def variable(self, ctxt, _trace): return BashCompletionCompgen(ctxt, '-A variable') def environment(self, ctxt, _trace): return BashCompletionCompgen(ctxt, '-A export') def exec(self, ctxt, _trace, command): func = ctxt.helpers.use_function('exec') return BashCompletionFunc(ctxt, [func, command]) def exec_fast(self, ctxt, _trace, command): func = ctxt.helpers.use_function('exec_fast') return BashCompletionFunc(ctxt, [func, command]) def exec_internal(self, ctxt, _trace, command): return BashCompletionCode(ctxt, command) def value_list(self, ctxt, _trace, opts): separator = opts.get('separator', ',') duplicates = opts.get('duplicates', False) values = opts['values'] if is_dict_type(values): values = list(values.keys()) args = [ctxt.helpers.use_function('value_list')] if duplicates: ctxt.helpers.use_function('value_list', 'duplicates') args.append('-d') args.append(separator) return BashCompletionFunc(ctxt, args + values) def key_value_list(self, ctxt, trace, pair_separator, value_separator, values): return BashCompleteKeyValueList(ctxt, trace, self, pair_separator, value_separator, values) def combine(self, ctxt, trace, commands): return BashCompleteCombine(ctxt, trace, self, commands) def list(self, ctxt, trace, command, opts=None): separator = opts.get('separator', ',') if opts else ',' duplicates = opts.get('duplicates', False) if opts else False obj = self.complete_from_def(ctxt, trace, command) code = obj.get_code() func = ctxt.helpers.use_function('value_list') dup_arg = '' if duplicates: ctxt.helpers.use_function('value_list', 'duplicates') dup_arg = ' -d' r = 'local cur_old="$cur"\n' r += 'cur="${cur##*%s}"\n' % shell.quote(separator) r += '%s\n' % code r += 'cur="$cur_old"\n' r += '%s%s %s "${COMPREPLY[@]}"' % (func, dup_arg, shell.quote(separator)) return BashCompletionCode(ctxt, r) def history(self, ctxt, _trace, pattern): func = ctxt.helpers.use_function('history') return BashCompletionFunc(ctxt, [func, pattern]) def commandline_string(self, ctxt, _trace): func = ctxt.helpers.use_function('commandline_string') return BashCompletionFunc(ctxt, [func]) def command_arg(self, ctxt, _trace): return BashCompletionCode(ctxt, '') def date(self, ctxt, _trace, _format): return BashCompletionCode(ctxt, '') def date_format(self, ctxt, _trace): return BashCompletionCode(ctxt, '') def uid(self, ctxt, _trace): return BashCompletionFunc(ctxt, [bash_versions.uids(ctxt)]) def gid(self, ctxt, _trace): return BashCompletionFunc(ctxt, [bash_versions.gids(ctxt)]) def signal(self, ctxt, _trace): return BashCompletionFunc(ctxt, [bash_versions.signals(ctxt)]) def filesystem_type(self, ctxt, _trace): return BashCompletionFunc(ctxt, [bash_versions.fstypes(ctxt)]) def prefix(self, ctxt, trace, prefix, command): obj = self.complete_from_def(ctxt, trace, command) func = obj.get_function() prefix_func = ctxt.helpers.use_function('prefix') return BashCompletionFunc(ctxt, [prefix_func, prefix, func]) def ip_address(self, ctxt, _trace, type_='all'): func = bash_versions.ip_addresses(ctxt) if type_ == 'ipv6': return BashCompletionFunc(ctxt, [func, '-6']) if type_ == 'ipv4': return BashCompletionFunc(ctxt, [func]) return BashCompletionFunc(ctxt, [func, '-a']) # ========================================================================= # Bonus # ========================================================================= def mountpoint(self, ctxt, _trace): func = ctxt.helpers.use_function('mountpoint') return BashCompletionFunc(ctxt, [func]) def locale(self, ctxt, trace): func = ctxt.helpers.use_function('locales') return BashCompletionFunc(ctxt, [func]) def charset(self, ctxt, trace): func = ctxt.helpers.use_function('charsets') return BashCompletionFunc(ctxt, [func]) def login_shell(self, ctxt, _trace): return BashCompletionFunc(ctxt, [bash_versions.shells(ctxt)]) def net_interface(self, ctxt, _trace): func = bash_versions.available_interfaces(ctxt) return BashCompletionFunc(ctxt, [func]) def timezone(self, ctxt, _trace): exec_func = ctxt.helpers.use_function('exec') list_func = ctxt.helpers.use_function('timezone_list') return BashCompletionFunc(ctxt, [exec_func, list_func]) def alsa_card(self, ctxt, _trace): func = ctxt.helpers.use_function('alsa_list_cards') return BashCompletionFunc(ctxt, [func]) def alsa_device(self, ctxt, _trace): func = ctxt.helpers.use_function('alsa_list_devices') return BashCompletionFunc(ctxt, [func]) crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_helpers.py000066400000000000000000000354311520061347500263000ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains helper functions for Bash.''' from .helpers import GeneralHelpers, ShellFunction _VALUES = ShellFunction('values', r''' local word append=0 [[ "$1" == "-a" ]] && { shift; append=1; } [[ "$1" == "--" ]] && { shift; } (( append )) || COMPREPLY=() for word; do if [[ "$word" == "$cur"* ]]; then COMPREPLY+=("$(printf '%q' "$word")") fi done ''') _DEQUOTE = ShellFunction('dequote', r''' local in="$1" len=${#1} i=0 result='' ___break_pos=-1 ___in_quotes=0 for ((; i < len; ++i)); do case "${in:i:1}" in "'") ___in_quotes=1 for ((++i; i < len; ++i)); do [[ "${in:i:1}" == "'" ]] && { ___in_quotes=0; break; } result+="${in:i:1}" done;; '"') ___in_quotes=1 for ((++i; i < len; ++i)); do [[ "${in:i:1}" == '"' ]] && { ___in_quotes=0; break; } if [[ "${in:i:1}" == '\' ]]; then result+="${in:$((++i)):1}" else result+="${in:i:1}" fi done;; '\') result+="${in:$((++i)):1}";; [$COMP_WORDBREAKS]) result+="${in:i:1}" ___break_pos=${#result};; *) result+="${in:i:1}";; esac done local -n ___RESULT=$2 local -n ___BREAK_POS=$3 local -n ___IN_QUOTES=$4 ___RESULT="$result" ___BREAK_POS=$___break_pos ___IN_QUOTES=$___in_quotes ''') _DEQUOTE_WORDS = ShellFunction('dequote_words', r''' local word dequoted break_pos in_quotes words_dequoted=() for word in "${words[@]}"; do dequote "$word" dequoted break_pos in_quotes words_dequoted+=("$dequoted") done ''', ['dequote']) _PARSE_LINE = ShellFunction('parse_line', r''' local -n out="$1" local in="$2" len=${#2} i=0 word='' active=0 if (( len == 0 )); then out+=('') return fi for ((i=0; i < len; ++i)); do case "${in:i:1}" in ' ') if (( active )); then out+=("$word") word='' active=0; fi;; '"') for ((; i < len; ++i)); do if [[ "${in:i:1}" == '\' ]]; then word+="${in:$((++i)):1}" else word+="${in:i:1}" [[ "${in:i:1}" == '"' ]] && break; fi done active=1;; "'") for ((; i < len; ++i)); do word+="${in:i:1}" [[ "${in:i:1}" == "'" ]] && break; done active=1;; '\') word+='\' word+="${in:$((++i)):1}" active=1;; *) word+="${in:i:1}" active=1;; esac done if (( active )); then out+=("$word") elif [[ "${in:$((len - 1)):1}" == ' ' ]]; then out+=('') fi ''') _GET_PREFIX_SUFFIX_LEN = ShellFunction('get_prefix_suffix_len', r''' local s1="$1" local s2="$2" local len1=${#s1} local len2=${#s2} local maxk=$len1 if (( len2 < maxk )); then maxk=$len2 fi local k=$maxk while (( k > 0 )); do local start=$((len1 - k)) local suf="${s1:start:k}" local pre="${s2:0:k}" if [[ "$suf" == "$pre" ]]; then if (( len1 == k )); then echo 0 else echo $((len1-k)) fi return fi ((k--)) done echo ${#s1} ''') _COMMANDLINE_STRING = ShellFunction('commandline_string', r''' local l line break_pos in_quotes dequote "$cur" line break_pos in_quotes local COMP_LINE_OLD="$COMP_LINE" local COMP_POINT_OLD=$COMP_POINT local COMP_WORDS_OLD=("${COMP_WORDS[@]}") local COMP_CWORD_OLD=$COMP_CWORD COMP_LINE="$line" COMP_POINT=${#line} COMP_WORDS=() parse_line COMP_WORDS "$line" COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 )) COMPREPLY=() #ifdef bash_completions_v_2_12 _comp_command_offset 0 #else _command_offset 0 #endif compopt -o nospace compopt -o noquote if (( break_pos >= 0 )); then line="${line:break_pos}" fi if (( ${#COMPREPLY[@]} )); then local len=$(get_prefix_suffix_len "$line" "${COMPREPLY[0]}") line="${line:0:len}" for i in "${!COMPREPLY[@]}"; do local REPLY="${COMPREPLY[i]}" if (( in_quotes )); then COMPREPLY[i]="$line$REPLY" else COMPREPLY[i]="$(printf '%q' "$line$REPLY")" fi done fi COMP_LINE="$COMP_LINE_OLD" COMP_POINT=$COMP_POINT_OLD COMP_WORDS=("${COMP_WORDS_OLD[@]}") COMP_CWORD=$COMP_CWORD_OLD ''', ['dequote', 'parse_line', 'get_prefix_suffix_len']) _EXEC = ShellFunction('exec', r''' local item desc special="$COMP_WORDBREAKS\"'><=;|&({:\\\$\`" while IFS=$'\t' read -r item desc; do if [[ "$item" == "$cur"* ]]; then [[ "$item" == *[$special]* ]] && item="$(printf '%q' "$item")" COMPREPLY+=("$item") fi done < <(eval "$1") ''') _EXEC_FAST = ShellFunction('exec_fast', r''' local item desc while IFS=$'\t' read -r item desc; do if [[ "$item" == "$cur"* ]]; then COMPREPLY+=("$item") fi done < <(eval "$1") ''') _ARRAY_CONTAINS = ShellFunction('array_contains', r''' local w='' search="$1"; shift; for w; do [[ "$search" == "$w" ]] && return 0; done return 1 ''') _VALUE_LIST = ShellFunction('value_list', r''' #ifdef duplicates local duplicates=0 [[ "$1" == '-d' ]] && { duplicates=1; shift; } #endif local separator="$1"; shift compopt -o nospace local cur_unquoted break_pos in_quotes dequote "$cur" cur_unquoted break_pos in_quotes if [[ -z "$cur_unquoted" ]]; then COMPREPLY=("$@") return fi local value having_value having_values=() remaining_values=() IFS="$separator" read -r -a having_values <<< "$cur_unquoted" #ifdef duplicates if (( duplicates )); then remaining_values=("$@") else for value; do if ! array_contains "$value" "${having_values[@]}"; then remaining_values+=("$value") fi done fi #else for value; do if ! array_contains "$value" "${having_values[@]}"; then remaining_values+=("$value") fi done #endif COMPREPLY=() local cur_stripped="$cur_unquoted" if (( break_pos > -1 )); then cur_stripped="${cur_stripped:break_pos}" fi if [[ "${cur_unquoted: -1}" == "$separator" ]]; then for value in "${remaining_values[@]}"; do COMPREPLY+=("$cur_stripped$value") done elif (( ${#remaining_values[@]} )); then if array_contains "${having_values[-1]}" "$@"; then COMPREPLY+=("$cur_stripped$separator") elif (( ${#having_values[@]} )); then local cur_last_value=${having_values[-1]} cur_stripped="${cur_stripped%"$cur_last_value"}" for value in "${remaining_values[@]}"; do if [[ "$value" == "$cur_last_value"* ]]; then COMPREPLY+=("$cur_stripped$value") fi done fi fi ''', ['dequote', 'array_contains']) _KEY_VALUE_LIST = ShellFunction('key_value_list', r''' local sep1="$1"; shift local sep2="$1"; shift local -A keys=() local i for ((i=1; i <= $#; i += 2)); do keys["${@:i:1}"]="${@:i + 1:1}" done local strip_chars='' [[ "$COMP_WORDBREAKS" == *"$sep1"* ]] && strip_chars+="$sep1" [[ "$COMP_WORDBREAKS" == *"$sep2"* ]] && strip_chars+="$sep2" [[ "${cur:0:1}" == '"' ]] && strip_chars='' [[ "${cur:0:1}" == "'" ]] && strip_chars='' local cur="$cur" break_pos in_quotes dequote "$cur" cur break_pos in_quotes if [[ -z "$cur" ]]; then COMPREPLY=("${!keys[@]}") return fi local pair key value found_key cur_stripped="$cur" local -a having_pairs having_keys remaining_keys IFS="$sep1" read -r -a having_pairs <<< "$cur" for pair in "${having_pairs[@]}"; do having_keys+=("${pair%%"$sep2"*}") done for key in "${!keys[@]}"; do found_key=0 for having_key in "${having_keys[@]}"; do if [[ "$key" == "$having_key" ]]; then found_key=1 break fi done if (( ! found_key )); then remaining_keys+=("$key") fi done COMPREPLY=() if [[ "${cur: -1}" == "$sep1" ]]; then [[ -n "$strip_chars" ]] && cur_stripped="${cur_stripped##*[$strip_chars]}" for key in "${remaining_keys[@]}"; do COMPREPLY+=("$cur_stripped$key") done else pair="${cur##*"$sep1"}" if [[ "$pair" == *"$sep2"* ]]; then key="${pair%%"$sep2"*}" value="${pair##*"$sep2"}" cur="$value" ${keys[$key]} cur_stripped="${cur_stripped:0:$(( ${#cur_stripped} - ${#value} ))}" if [[ -n "$strip_chars" ]]; then cur_stripped="${cur_stripped##*[$strip_chars]}" fi for i in "${!COMPREPLY[@]}"; do COMPREPLY[i]="$cur_stripped${COMPREPLY[i]}" done else [[ -n "$strip_chars" ]] && cur_stripped="${cur_stripped##*[$strip_chars]}" cur_stripped="${cur_stripped%"$pair"}" for key in "${remaining_keys[@]}"; do if [[ "$key" == "$pair"* ]]; then COMPREPLY+=("$cur_stripped$key") fi done fi fi ''', ['dequote']) _PREFIX_COMPREPLY = ShellFunction('prefix_compreply', r''' [[ "$cur" == *[$COMP_WORDBREAKS]* ]] && return local i prefix="$1" for ((i=0; i < ${#COMPREPLY[@]}; ++i)); do COMPREPLY[i]="$prefix${COMPREPLY[i]}" done ''') _HISTORY = ShellFunction('history', r''' [[ -f "$HISTFILE" ]] || return local match while read -r match; do if [[ "$match" == "$cur"* ]]; then COMPREPLY+=("$(printf '%q' "$match")") fi done < <(command grep -E -o -- "$1" "$HISTFILE") ''') _GET_LAST_BREAK_POSITION = ShellFunction('get_last_break_position', r''' local in="$1" i=0 len=${#1} pos=-1 for ((i=0; i < len; ++i)); do case "${in:i:1}" in '"') for ((++i; i < len; ++i)); do if [[ "${in:i:1}" == '\' ]]; then ((++i)) elif [[ "${in:i:1}" == '"' ]]; then break; fi done;; "'") for ((++i; i < len; ++i)); do [[ "${in:i:1}" == "'" ]] && break; done;; '\') ((++i));; [$COMP_WORDBREAKS]) pos=$i;; esac done echo $pos ''') _PREFIX = ShellFunction('prefix', r''' local prefix="$1" func="$2" local stripped="$(strip_prefix_keep_quoting "$prefix" "$cur")" if [[ "$stripped" == "$cur" ]]; then COMPREPLY=($(compgen -W "$1" -- "$cur")) compopt -o nospace return fi local break_position=$(get_last_break_position "$cur") local cur_old="$cur" cur="$stripped" $func if (( break_position < 0 )); then for i in "${!COMPREPLY[@]}"; do COMPREPLY[i]="$prefix${COMPREPLY[i]}" done fi ''', ['get_last_break_position', 'strip_prefix_keep_quoting']) _STRIP_PREFIX_KEEP_QUOTING = ShellFunction('strip_prefix_keep_quoting', r''' local p="$1" pi=0 plen=${#1} local s="$2" si=0 slen=${#2} state=w for (( pi=0; pi < plen; ++pi )); do pc=${p:pi:1} sc='' while (( si < slen )); do case "$state" in w) case "${s:si:1}" in '"') state=dq; ((++si));; "'") state=sq; ((++si));; '\') ((++si)); sc="${s:si:1}"; ((++si)); break;; *) sc="${s:si:1}"; ((++si)); break;; esac;; sq) case "${s:si:1}" in "'") state=w; ((++si));; *) sc="${s:si:1}"; ((++si)); break;; esac;; dq) case "${s:si:1}" in '"') state=w; ((++si));; '\') ((++si)); sc="${s:si:1}"; ((++si)); break;; *) sc="${s:si:1}"; ((++si)); break;; esac;; esac done [[ "$pc" != "$sc" ]] && { echo "$s"; return; } done case "$state" in w) printf '%s\n' "${s:si}";; sq) printf '%s\n' "'${s:si}";; dq) printf '%s\n' "\"${s:si}";; esac ''') _MIME_FILE = ShellFunction('mime_file', r''' local line file mime i_opt cur_dequoted if command file -i /dev/null &>/dev/null; then i_opt="-i" elif command file -I /dev/null &>/dev/null; then i_opt="-I" else #ifdef bash_completions_v_2_12 _comp_compgen_filedir #else _filedir #endif return fi local cur_dequoted break_pos in_quotes dequote "$cur" cur_dequoted break_pos in_quotes while read -r line; do mime="${line##*:}" if [[ "$mime" == *inode/directory* ]] || command grep -q -E -- "$1" <<< "$mime"; then file="${line%:*}" if [[ "$file" == *\\* ]]; then file="$(command perl -pe 's/\\([0-7]{3})/chr(oct($1))/ge' <<< "$file")" fi if [[ "$mime" == *inode/directory* ]]; then file="$file/" fi if [[ "$file" == "$cur_dequoted"* ]]; then COMPREPLY+=("$(printf '%q' "$file")") fi fi done < <(command file -L $i_opt -- "$cur_dequoted"* 2>/dev/null) ''', ['dequote']) _FILE_FILTER = ShellFunction('file_filter', ''' local pattern REPLY COMPREPLY_OLD=("${COMPREPLY[@]}") COMPREPLY=() for REPLY in "${COMPREPLY_OLD[@]}"; do for pattern; do [[ "${REPLY##*/}" == $pattern ]] && continue 2 done COMPREPLY+=("$REPLY") done ''') _NUMBER = ShellFunction('number', ''' local number="$(command grep -E -o '^[0-9,.]+' <<< "$cur")" suffix_and_desc [[ -z "$number" ]] && return COMPREPLY=("$number") for suffix_and_desc; do COMPREPLY+=("${number}${suffix_and_desc%%:*}") done ''') # ============================================================================= # Bonus # ============================================================================= _MOUNTPOINT = ShellFunction('mountpoint', r''' local REPLY COMPREPLY=() while read -r REPLY; do COMPREPLY+=("$(printf '%q' "$REPLY")") done < <(command findmnt -lno TARGET) ''') _LOCALES = ShellFunction('''locales''', r''' COMPREPLY=($(compgen -W "$(command locale -a)" -- "$cur")) ''') _CHARSETS = ShellFunction('''charsets''', r''' COMPREPLY=($(compgen -W "$(command locale -m)" -- "$cur")) ''') _TIMEZONE_LIST = ShellFunction('timezone_list', r''' if ! command timedatectl list-timezones 2>/dev/null; then command find /usr/share/zoneinfo -type f |\ command sed 's|/usr/share/zoneinfo/||g' |\ command grep -E -v '^(posix|right)' fi''') _ALSA_LIST_CARDS = ShellFunction('alsa_list_cards', r''' local card id cards=() while builtin read card; do card="${card#card }" id="${card%%: *}" cards+=("$id") done < <(command aplay -l | command grep -Eo '^card [0-9]+: [^,]+') COMPREPLY=($(compgen -W "${cards[*]}" -- "$cur")) ''') _ALSA_LIST_DEVICES = ShellFunction('alsa_list_devices', r''' local card id devices=() while builtin read card; do card="${card#card }" id="${card%%: *}" devices+=("hw:$id") done < <(command aplay -l | command grep -Eo '^card [0-9]+: [^,]+') COMPREPLY=($(compgen -W "${devices[*]}" -- "$cur")) ''') class BashHelpers(GeneralHelpers): '''Class holding helper functions for Bash.''' def __init__(self, config, function_prefix): super().__init__(config, function_prefix, ShellFunction) self.add_function(_VALUES) self.add_function(_DEQUOTE) self.add_function(_DEQUOTE_WORDS) self.add_function(_PREFIX) self.add_function(_STRIP_PREFIX_KEEP_QUOTING) self.add_function(_ARRAY_CONTAINS) self.add_function(_GET_LAST_BREAK_POSITION) self.add_function(_PARSE_LINE) self.add_function(_GET_PREFIX_SUFFIX_LEN) self.add_function(_COMMANDLINE_STRING) self.add_function(_EXEC) self.add_function(_EXEC_FAST) self.add_function(_VALUE_LIST) self.add_function(_KEY_VALUE_LIST) self.add_function(_FILE_FILTER) self.add_function(_PREFIX_COMPREPLY) self.add_function(_HISTORY) self.add_function(_NUMBER) self.add_function(_MIME_FILE) self.add_function(_MOUNTPOINT) self.add_function(_LOCALES) self.add_function(_CHARSETS) self.add_function(_TIMEZONE_LIST) self.add_function(_ALSA_LIST_CARDS) self.add_function(_ALSA_LIST_DEVICES) crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_option_completion.py000066400000000000000000000301461520061347500303750ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Module for option completion in Bash.''' from . import cli from . import algo from . import utils from . import bash_when from . import bash_utils from . import bash_patterns from .str_utils import indent class MasterCompletionFunctionBase: '''Base class for generating master completion functions.''' def __init__(self, options, abbreviations, generator): self.options = options self.abbreviations = abbreviations self.generator = generator self.complete = generator.complete_option def get(self, funcname): '''Get the function code.''' raise NotImplementedError def get_all_option_strings(self, option): '''Return all possible option strings for option.''' opts = option.get_short_option_strings() opts += self.abbreviations.get_many_abbreviations( option.get_long_option_strings()) opts += self.abbreviations.get_many_abbreviations( option.get_old_option_strings()) return opts class MasterCompletionFunction(MasterCompletionFunctionBase): '''Class for generating a master completion function.''' # pylint: disable=too-many-instance-attributes # pylint: disable=too-few-public-methods def __init__(self, options, abbreviations, generator): super().__init__(options, abbreviations, generator) self.optionals = False self.code = [] optional_arg = list(filter(cli.Option.has_optional_arg, options)) required_arg = list(filter(cli.Option.has_required_arg, options)) self._add_options(required_arg) if optional_arg: self.optionals = True self.code.append('[[ "$mode" == WITH_OPTIONAL ]] || return 1') self._add_options(optional_arg) def _add_options(self, options): with_when, without_when = algo.partition(options, lambda o: o.when) self._add_options_with_when(with_when) self._add_options_without_when(without_when) def _add_options_without_when(self, options_): options_group_by_complete = algo.group_by(options_, self.complete) if options_group_by_complete: r = 'case "$opt" in\n' for complete, options in options_group_by_complete.items(): opts = algo.flatten(map(self.get_all_option_strings, options)) r += ' %s)\n' % bash_patterns.make_pattern(opts) if complete: r += '%s\n' % indent(complete, 4) r += ' return 0;;\n' r += 'esac' self.code.append(r) def _add_options_with_when(self, options): for option in options: opts = self.get_all_option_strings(option) completion_code = self.complete(option) cond = bash_when.generate_when_conditions( self.generator.commandline, self.generator.variable_manager, option.when) r = 'case "$opt" in %s)\n' % bash_patterns.make_pattern(opts) r += ' if %s; then\n' % cond if completion_code: r += '%s\n' % indent(completion_code, 4) r += ' return 0\n' r += ' fi;;\n' r += 'esac' self.code.append(r) def get(self, funcname): '''Get the function code.''' if self.code: r = '%s() {\n' % funcname if self.optionals: r += ' local opt="$1" cur="$2" mode="$3"\n\n' else: r += ' local opt="$1" cur="$2"\n\n' r += '%s\n\n' % indent('\n\n'.join(self.code), 2) r += ' return 1\n' r += '}' return r return None class MasterCompletionFunctionNoWhen(MasterCompletionFunctionBase): '''Class for generating a master completion function.''' def __init__(self, options, abbreviations, generator): super().__init__(options, abbreviations, generator) self.optionals = False self.code = [] optional_arg = list(filter(cli.Option.has_optional_arg, options)) required_arg = list(filter(cli.Option.has_required_arg, options)) self._add_options(required_arg) if optional_arg: self.optionals = True r = '(( ! ret )) && return 0\n' r += '[[ "$mode" == WITH_OPTIONAL ]] || return 1\n' r += 'ret=0' self.code.append(r) self._add_options(optional_arg) @staticmethod def accept(options): '''Return True if none of the options have the when attribute set.''' return not any(option.when for option in options) def _add_options(self, options_): options_group_by_complete = algo.group_by(options_, self.complete) if options_group_by_complete: r = 'case "$opt" in\n' for complete, options in options_group_by_complete.items(): opts = algo.flatten(map(self.get_all_option_strings, options)) if complete: r += ' %s)\n' % bash_patterns.make_pattern(opts) r += '%s;;\n' % indent(complete, 4) else: r += ' %s);;\n' % bash_patterns.make_pattern(opts) r += ' *) ret=1;;\n' r += 'esac' self.code.append(r) def get(self, funcname): '''Get the function code.''' if self.code: r = '%s() {\n' % funcname if self.optionals: r += ' local opt="$1" cur="$2" mode="$3" ret=0\n\n' else: r += ' local opt="$1" cur="$2" ret=0\n\n' r += '%s\n\n' % indent('\n\n'.join(self.code), 2) r += ' return $ret\n' r += '}' return r return None def _get_master_completion_obj(options, abbreviations, generator): if MasterCompletionFunctionNoWhen.accept(options): klass = MasterCompletionFunctionNoWhen else: klass = MasterCompletionFunction return klass(options, abbreviations, generator) class _Info: # pylint: disable=too-many-instance-attributes # pylint: disable=too-few-public-methods def __init__(self, options, abbreviations, commandline, ctxt): self.commandline = commandline self.ctxt = ctxt self.short_required = False # Short with required argument self.short_optional = False # Short with optional argument self.long_required = False # Long with required argument self.long_optional = False # Long with optional argument self.old_required = False # Old-Style with required argument self.old_optional = False # Old-Style with optional argument self._collect_options_info(options) all_options = self.commandline.get_options( with_parent_options=self.commandline.inherit_options) old_option_strings = algo.flatten([o.get_old_option_strings() for o in all_options]) old_option_strings = abbreviations.get_many_abbreviations(old_option_strings) self.old_option_strings = bash_utils.CasePatterns.for_old_without_arg(old_option_strings) self.short_no_args = '' self.short_required_args = '' self.short_optional_args = '' for option in all_options: short_opts = ''.join(o.lstrip('-') for o in option.get_short_option_strings()) if option.has_required_arg(): self.short_required_args += short_opts elif option.has_optional_arg(): self.short_optional_args += short_opts elif option.complete is None: self.short_no_args += short_opts if self.short_no_args: self.short_no_args_pattern = '*([%s])' % self.short_no_args else: self.short_no_args_pattern = '' def _collect_options_info(self, options): for option in options: if option.get_long_option_strings(): if option.has_optional_arg(): self.long_optional = True elif option.has_required_arg(): self.long_required = True if option.get_old_option_strings(): if option.has_optional_arg(): self.old_optional = True elif option.has_required_arg(): self.old_required = True if option.get_short_option_strings(): if option.has_optional_arg(): self.short_optional = True elif option.has_required_arg(): self.short_required = True def _get_prev_completion(info): r = 'case "$prev" in\n' if info.long_required: r += ' --*) __complete_option "$prev" "$cur" WITHOUT_OPTIONAL && return 0;;\n' else: r += ' --*);;\n' if info.old_required and info.short_required: r += ' -*) __complete_option "$prev" "$cur" WITHOUT_OPTIONAL && return 0;&\n' r += ' -%s[%s])\n' % (info.short_no_args_pattern, info.short_required_args) r += ' __complete_option "-${prev: -1}" "$cur" WITHOUT_OPTIONAL && return 0;;\n' elif info.old_required: r += ' -*) __complete_option "$prev" "$cur" WITHOUT_OPTIONAL && return 0;;\n' elif info.short_required: r += ' -%s[%s])\n' % (info.short_no_args_pattern, info.short_required_args) r += ' __complete_option "-${prev: -1}" "$cur" WITHOUT_OPTIONAL && return 0;;\n' r += 'esac' return r def _get_cur_completion(info): short_no_args_pattern = info.short_no_args_pattern short_required_args = info.short_required_args short_optional_args = info.short_optional_args r = 'case "$cur" in\n' if info.long_required or info.long_optional: r += ' --*=*)\n' r += ' __complete_option "${cur%%=*}" "${cur#*=}" WITH_OPTIONAL && return 0;;\n' else: r += ' --*=*);;\n' r += ' --*);;\n' if (info.short_required or info.short_optional) and info.old_option_strings: r += ' %s);;\n' % info.old_option_strings if info.old_required or info.old_optional: r += ' -*=*)\n' r += ' __complete_option "${cur%%=*}" "${cur#*=}" WITH_OPTIONAL && return 0;&\n' if info.short_required or info.short_optional: r += ' -%s[%s%s]*)\n' % (short_no_args_pattern, short_required_args, short_optional_args) r += ' local i\n' r += ' for ((i=2; i <= ${#cur}; ++i)); do\n' r += ' local pre="${cur:0:$i}" value="${cur:$i}"\n' r += ' __complete_option "-${pre: -1}" "$value" WITH_OPTIONAL && {\n' r += ' %s "$pre"\n' % info.ctxt.helpers.use_function('prefix_compreply') r += ' return 0\n' r += ' }\n' r += ' done;;\n' # Optimization: Strip unused case patterns at the end if r.endswith(' -*=*);;\n'): r = r.replace(' -*=*);;\n', '') if r.endswith(' --*);;\n'): r = r.replace(' --*);;\n', '') r += 'esac' return r def _get_dispatcher(info): r = [] required = info.short_required or info.long_required or info.old_required optional = info.short_optional or info.long_optional or info.old_optional if required: r.append(_get_prev_completion(info)) if required or optional: r.append(_get_cur_completion(info)) return '\n\n'.join(r) def generate_option_completion(self): '''Generate code for completing options.''' options = self.commandline.get_options(only_with_arguments=True) abbreviations = utils.get_option_abbreviator(self.commandline) complete_function = _get_master_completion_obj(options, abbreviations, self) complete_function_code = complete_function.get('__complete_option') if not complete_function_code: return None info = _Info(options, abbreviations, self.commandline, self.ctxt) dispatcher_code = _get_dispatcher(info) if not complete_function.optionals: dispatcher_code = dispatcher_code.replace('WITH_OPTIONAL ', '') dispatcher_code = dispatcher_code.replace('WITHOUT_OPTIONAL ', '') return f'{complete_function_code}\n\n{dispatcher_code}' crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_option_strings_completion.py000066400000000000000000000073061520061347500321500ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains code for completing option strings in Bash.''' from collections import namedtuple from . import cli from . import algo from . import shell from . import bash_when from .str_utils import indent _Option = namedtuple('_Option', ('option', 'conditions', 'when')) def _make_option_strings(options): opt_strings = [] for opt in options: opt_strings.extend(opt.get_short_option_strings()) add_equal_sign = ( opt.has_optional_arg() or ( opt.has_required_arg() and opt.long_opt_arg_sep in ('both', 'equals') ) ) if add_equal_sign: opt_strings.extend(f'{s}=' for s in opt.get_long_option_strings()) opt_strings.extend(f'{s}=' for s in opt.get_old_option_strings()) else: opt_strings.extend(opt.get_long_option_strings()) opt_strings.extend(opt.get_old_option_strings()) return shell.join_quoted(opt_strings) def _get_option_full_condition(option): if option.conditions and option.when: return '(( %s )) && %s' % (' && '.join(option.conditions), option.when) if option.conditions: return '(( %s ))' % (' && '.join(option.conditions)) if option.when: return '%s' % option.when return '' def _generate_option_strings_completion(options): r = [] grouped_by_condition = algo.group_by(options, _get_option_full_condition) for condition, opts in grouped_by_condition.items(): s = 'opts+=(%s)' % _make_option_strings([o.option for o in opts]) if condition: s = '%s && %s' % (condition, s) r.append(s) return '\n'.join(r) def _generate_final_check_with_options(final_conditions, options): option_strings_completion = _generate_option_strings_completion(options) if not final_conditions: return option_strings_completion r = 'if (( %s )); then\n' % ' && '.join(final_conditions) r += '%s\n' % indent(option_strings_completion, 2) r += 'fi' return r def generate(generator): '''Generate option strings completion code.''' if len(generator.options) == 0: return None commandline = generator.commandline variable_manager = generator.variable_manager options = [] final_conditions = [] for final_option in commandline.get_final_options(): final_conditions += ["! ${#%s[@]}" % variable_manager.capture_variable(final_option)] for option in commandline.options: if option.hidden: continue conditions = [] for exclusive_option in option.get_conflicting_options(): conditions += ["! ${#%s[@]}" % variable_manager.capture_variable(exclusive_option)] if not option.repeatable: conditions += ["! ${#%s[@]}" % variable_manager.capture_variable(option)] for final_condition in final_conditions: try: conditions.remove(final_condition) except ValueError: pass conditions = algo.uniq(sorted(conditions)) when = None if option.when is not None: when = bash_when.generate_when_conditions(commandline, variable_manager, option.when) options.append(_Option(option, conditions, when)) r = 'if (( ! END_OF_OPTIONS )) && [[ "$cur" = -* ]]; then\n' r += ' local -a opts\n' r += '%s\n' % indent(_generate_final_check_with_options(final_conditions, options), 2) r += ' COMPREPLY+=($(compgen -W "${opts[*]}" -- "$cur"))\n' r += ' [[ ${COMPREPLY-} == *= ]] && compopt -o nospace\n' r += ' return 1\n' r += 'fi' return r crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_parser.py000066400000000000000000000131501520061347500261240ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for parsing a command line in Bash.''' from collections import namedtuple from . import utils from .str_utils import indent from .bash_utils import CasePatterns from .bash_parser_subcommand_code import ( make_subcommand_switch_code, get_subcommand_path ) _PARSER_CODE = '''\ POSITIONALS=() END_OF_OPTIONS=0 local cmd="root" argi arg i char trailing_chars for ((argi=1; argi < cword; ++argi)); do arg="${words_dequoted[argi]}" case "$arg" in --) END_OF_OPTIONS=1 POSITIONALS+=("${words_dequoted[@]:$((++argi))}") return;; -?*) # ignore '-' %LONG_OPTION_CASES% for ((i=1; i < ${#arg}; ++i)); do char="${arg:$i:1}" trailing_chars="${arg:$((i + 1))}" %SHORT_OPTION_CASES% done;; *) POSITIONALS+=("$arg") %SUBCOMMAND_SWITCH_CODE% ;; esac done for ((; argi <= cword; ++argi)); do case "${words_dequoted[argi]}" in -?*);; *) POSITIONALS+=("${words_dequoted[argi]}");; esac done''' _OPT_ISSET = '_OPT_ISSET_' def generate(commandline, variable_manager): '''Generate code for parsing the command line.''' commandlines = list(reversed(commandline.get_all_commandlines())) subcommand_switch_code = make_subcommand_switch_code(commandline) long_option_cases = [] short_option_cases = [] for cmdline in commandlines: option_cases = _generate_option_cases(cmdline, variable_manager) command = get_subcommand_path(cmdline) if cmdline.inherit_options: command += '*' if option_cases.long_options: r = 'case "$cmd" in %s)\n' % command r += ' case "$arg" in\n' for case in option_cases.long_options: r += '%s\n' % indent(case, 4) r += ' esac\n' r += 'esac' long_option_cases.append(r) if option_cases.short_options: r = 'case "$cmd" in %s)\n' % command r += ' case "$char" in\n' for case in option_cases.short_options: r += '%s\n' % indent(case, 4) r += ' esac\n' r += 'esac' short_option_cases.append(r) s = _PARSER_CODE if long_option_cases: s = s.replace('%LONG_OPTION_CASES%', indent('\n\n'.join(long_option_cases), 6)) else: s = s.replace('%LONG_OPTION_CASES%\n', '') if short_option_cases: s = s.replace('%SHORT_OPTION_CASES%', indent('\n\n'.join(short_option_cases), 8)) else: s = s.replace('%SHORT_OPTION_CASES%\n', '') if subcommand_switch_code: s = s.replace('%SUBCOMMAND_SWITCH_CODE%', indent(subcommand_switch_code, 6)) else: s = s.replace('%SUBCOMMAND_SWITCH_CODE%\n', '') return s def _make_long_option_case(long_options, option, variable): r = '' if option.has_optional_arg(): r += '%s)\n' % CasePatterns.for_long_without_arg(long_options) r += ' %s+=(%s);\n' % (variable, _OPT_ISSET) r += ' continue;;\n' r += '%s)\n' % CasePatterns.for_long_with_arg(long_options) r += ' %s+=("${arg#*=}")\n' % variable r += ' continue;;' elif option.has_required_arg(): r += '%s)\n' % CasePatterns.for_long_without_arg(long_options) r += ' %s+=("${words_dequoted[++argi]}")\n' % variable r += ' continue;;\n' r += '%s)\n' % CasePatterns.for_long_with_arg(long_options) r += ' %s+=("${arg#*=}")\n' % variable r += ' continue;;' else: r += '%s)\n' % CasePatterns.for_long_without_arg(long_options) r += ' %s+=(%s)\n' % (variable, _OPT_ISSET) r += ' continue;;' return r def _make_short_option_case(short_options, option, variable): r = '' if option.has_optional_arg(): r += '%s)\n' % CasePatterns.for_short(short_options) r += ' if [[ -n "$trailing_chars" ]]\n' r += ' then %s+=("$trailing_chars")\n' % variable r += ' else %s+=(%s)\n' % (variable, _OPT_ISSET) r += ' fi\n' r += ' continue 2;;' elif option.has_required_arg(): r += '%s)\n' % CasePatterns.for_short(short_options) r += ' if [[ -n "$trailing_chars" ]]\n' r += ' then %s+=("$trailing_chars")\n' % variable r += ' else %s+=("${words_dequoted[++argi]}")\n' % variable r += ' fi\n' r += ' continue 2;;' else: r += '%s)\n' % CasePatterns.for_short(short_options) r += ' %s+=(%s);;' % (variable, _OPT_ISSET) return r def _generate_option_cases(commandline, variable_manager): OptionCases = namedtuple('OptionCases', ['long_options', 'short_options']) options = commandline.get_options() abbreviations = utils.get_option_abbreviator(commandline) option_cases = OptionCases([], []) for option in options: short_options = option.get_short_option_strings() long_options = abbreviations.get_many_abbreviations( option.get_long_option_strings()) long_options += abbreviations.get_many_abbreviations( option.get_old_option_strings()) variable = variable_manager.capture_variable(option) if long_options: option_cases.long_options.append( _make_long_option_case(long_options, option, variable)) if short_options: option_cases.short_options.append( _make_short_option_case(short_options, option, variable)) return option_cases crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_parser_subcommand_code.py000066400000000000000000000053621520061347500313340ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for creating the subcommand switch code in Bash''' from collections import OrderedDict from . import utils from . import shell from . import bash_patterns from .str_utils import indent def get_subcommand_path(commandline): '''Return the path for `commandline`.''' commandlines = commandline.get_parents(include_self=True)[1:] prognames = ['root'] + [c.prog for c in commandlines] return ':'.join(prognames) def make_subcommand_switch_code(commandline): ''' Generate Bash code that tracks and updates the current subcommand path. This function walks through all known command lines (starting from the given top-level `commandline`) and generates conditional Bash code that appends the currently matched subcommand to the `cmd` variable. This allows the generated completion script to keep track of the full subcommand hierarchy as the user types. While tracking, all variations of a subcommand are considered, including defined aliases and any supported abbreviations. This ensures that the resulting `cmd` variable always contains the canonical subcommand name, regardless of how the user typed it. ''' assert commandline.parent is None, \ "This function should only be used on top-level command lines" cases = [] for cmdline in commandline.get_all_commandlines(): if not cmdline.get_subcommands(): continue positional_num = cmdline.get_subcommands().get_positional_num() command_path = get_subcommand_path(cmdline) switch_code = _make_switch_case(cmdline) cases.append(('%s|%d' % (command_path, positional_num), switch_code)) if not cases: return None r = 'case "$cmd|${#POSITIONALS[@]}" in\n' for case in cases: if '\n' not in case[1]: r += ' %s) %s;;\n' % (shell.quote(case[0]), case[1]) else: r += ' %s)\n%s;;\n' % (shell.quote(case[0]), indent(case[1], 4)) r += 'esac' return r def _make_switch_case(cmdline): subcommand_aliases = OrderedDict() for subcommand in cmdline.get_subcommands().subcommands: aliases = utils.get_all_command_variations(subcommand) aliases.remove(subcommand.prog) if aliases: subcommand_aliases[subcommand.prog] = aliases # We have no aliases or abbreviations, just add $arg to $cmd if not subcommand_aliases: return 'cmd+=":$arg"' r = 'case "$arg" in\n' for command, aliases in subcommand_aliases.items(): r += ' %s) cmd+=":%s";;\n' % (bash_patterns.make_pattern(aliases), command) r += ' *) cmd+=":$arg";;\n' r += 'esac' return r crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_parser_v2.py000066400000000000000000000137741520061347500265470ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for parsing a command line in Bash, version 2''' from collections import namedtuple from . import utils from . import shell from . import bash_patterns from .str_utils import indent from .bash_parser_subcommand_code import ( make_subcommand_switch_code, get_subcommand_path ) from .preprocessor import preprocess _PARSER_CODE = '''\ #ifdef positionals POSITIONALS=() #endif END_OF_OPTIONS=0 local cmd="root" argi arg i char trailing_chars VAR ARGS %FIND_OPTION_CODE% __append_to_array() { local -n arr=$1 arr+=("$2") } for ((argi=1; argi < cword; ++argi)); do arg="${words_dequoted[argi]}" case "$arg" in --) END_OF_OPTIONS=1 #ifdef positionals POSITIONALS+=("${words_dequoted[@]:$((++argi))}") #endif return;; #ifdef long_options --*=*) if __find_option "$cmd" "${arg%%=*}" then __append_to_array "$VAR" "${arg#*=}" fi;; --*) if __find_option "$cmd" "$arg"; then if [[ "$ARGS" == 1 ]] then __append_to_array "$VAR" "${words_dequoted[++argi]}" else __append_to_array "$VAR" "_OPT_ISSET_" fi fi;; #else --*);; #endif -?*) # ignore '-' #ifdef old_options if [[ "$arg" == -*=* ]]; then if __find_option "$cmd" "${arg%%=*}"; then __append_to_array "$VAR" "${arg#*=}" continue fi fi if __find_option "$cmd" "$arg"; then if [[ "$ARGS" == 1 ]] then __append_to_array "$VAR" "${words_dequoted[++argi]}" else __append_to_array "$VAR" "_OPT_ISSET_" fi continue fi #endif #ifdef short_options for ((i=1; i < ${#arg}; ++i)); do char="${arg:$i:1}" trailing_chars="${arg:$((i + 1))}" if __find_option "$cmd" "-$char"; then if [[ "$ARGS" == 1 ]]; then if [[ -n "$trailing_chars" ]] then __append_to_array "$VAR" "$trailing_chars" else __append_to_array "$VAR" "${words_dequoted[++argi]}" fi break; #ifdef short_optionals elif [[ "$ARGS" == '?' ]]; then if [[ -n "$trailing_chars" ]] then __append_to_array "$VAR" "$trailing_chars" else __append_to_array "$VAR" _OPT_ISSET_ fi break; #endif else __append_to_array "$VAR" "_OPT_ISSET_" fi fi done #endif ;; #ifdef positionals *) POSITIONALS+=("$arg") %SUBCOMMAND_SWITCH_CODE% ;; #endif esac done #ifdef positionals for ((; argi <= cword; ++argi)); do case "${words_dequoted[argi]}" in -?*);; *) POSITIONALS+=("${words_dequoted[argi]}");; esac done #endif''' _OPT_ISSET = '_OPT_ISSET_' def _make_find_option_code(commandline, variable_manager): cmdlines = list(reversed(commandline.get_all_commandlines())) code = [] if len(cmdlines) == 1: code += [_make_cmd_plus_option_switch_code(cmdlines[0], variable_manager, True)] else: for cmdline in cmdlines: code += [_make_cmd_plus_option_switch_code(cmdline, variable_manager)] r = '__find_option() {\n' r += '%s\n' % indent('\n'.join(filter(None, code)), 2) r += ' return 1\n' r += '}' return r def _make_cmd_plus_option_switch_code(commandline, variable_manager, omit_cmd_check=False): option_cases = _generate_option_cases(commandline, variable_manager) if not option_cases: return None if omit_cmd_check: return _make_option_switch_code(option_cases) command = get_subcommand_path(commandline) command = shell.quote(command) if commandline.inherit_options: command += '*' r = f'case "$1" in {command})\n' r += '%s\n' % indent(_make_option_switch_code(option_cases), 2) r += 'esac' return r def _make_option_switch_code(option_cases): r = 'case "$2" in\n' for case in option_cases: pattern = bash_patterns.make_pattern(case.option_strings) r += f' {pattern}) VAR={case.variable}; ARGS={case.args}; return;;\n' r += 'esac' return r def generate(commandline, variable_manager): '''Generate code for parsing the command line.''' find_option_code = _make_find_option_code(commandline, variable_manager) subcommand_switch_code = make_subcommand_switch_code(commandline) defines = [] types = utils.get_defined_option_types(commandline) if types.short: defines.append('short_options') if types.long: defines.append('long_options') if types.old: defines.append('old_options') if types.short_optional: defines.append('short_optionals') if types.positionals: defines.append('positionals') s = preprocess(_PARSER_CODE, defines) if subcommand_switch_code: s = s.replace('%SUBCOMMAND_SWITCH_CODE%', indent(subcommand_switch_code, 6)) else: s = s.replace('%SUBCOMMAND_SWITCH_CODE%\n', '') if find_option_code: s = s.replace('%FIND_OPTION_CODE%', find_option_code) else: s = s.replace('%FIND_OPTION_CODE%\n', '') return s def _generate_option_cases(commandline, variable_manager): OptionCase = namedtuple('OptionCase', ['option_strings', 'variable', 'args']) options = commandline.get_options() abbreviations = utils.get_option_abbreviator(commandline) option_cases = [] for option in options: option_strings = option.get_short_option_strings() option_strings += abbreviations.get_many_abbreviations( option.get_long_option_strings()) option_strings += abbreviations.get_many_abbreviations( option.get_old_option_strings()) value_variable = variable_manager.capture_variable(option) if option.has_optional_arg(): args = "'?'" elif option.has_required_arg(): args = '1' else: args = '0' option_cases.append(OptionCase(option_strings, value_variable, args)) return option_cases crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_patterns.py000066400000000000000000000037521520061347500264770ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Module for creating Bash globs.''' from collections import defaultdict from .algo import numbers_are_contiguous class TrieNode: '''Trie class.''' # pylint: disable=too-few-public-methods def __init__(self): self.children = defaultdict(TrieNode) self.is_terminal = False def insert(self, string): '''Insert string into trie.''' node = self for char in string: node = node.children[char] node.is_terminal = True def _build_pattern(parts): if len(parts) == 1: return parts[0] try: ints = sorted(map(int, parts)) if numbers_are_contiguous(ints): return f'[{ints[0]}-{ints[-1]}]' except ValueError: pass return "@(" + "|".join(sorted(parts)) + ")" def trie_to_pattern(node): ''' Recursively convert the trie into a Bash extglob pattern. ''' if not node.children: return "" parts = [] for token, child in node.children.items(): sub = trie_to_pattern(child) if sub: parts.append(token + sub) else: parts.append(token) # Terminal means that this path can end here if node.is_terminal: parts.append("") # Represent the option to stop early return _build_pattern(parts) def compact_glob_trie(strings): ''' Build a recursive glob pattern that matches all input strings. ''' strings = sorted(set(strings)) if len(strings) == 1: return strings[0] # Build Trie root = TrieNode() for string in strings: root.insert(string) # Convert Trie to pattern return trie_to_pattern(root) def make_pattern(strings): '''Make a glob pattern that matches `strings`.''' candidate0 = '|'.join(strings) candidate1 = compact_glob_trie(strings) return candidate0 if len(candidate0) < len(candidate1) else candidate1 crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_positionals_completion.py000066400000000000000000000050031520061347500314230ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Module for positionals completion in Bash.''' from . import algo from . import bash_when from .str_utils import indent def _make_block(code): if not code: return '{\n return 0;\n}' return '{\n%s\n return 0;\n}' % indent(code, 2) def _is_not_command_arg(positional): return ( not positional.complete or positional.complete[0] != 'command_arg' ) def _generate_subcommand_positional(generator): cmds = generator.subcommands.get_choices().keys() complete = generator.completer.choices(generator.ctxt, [], cmds).get_code() return '(( ${#POSITIONALS[@]} == %d )) && %s' % ( generator.subcommands.get_positional_num(), _make_block(complete)) def _get_positional_condition(positional): op = '==' if positional.repeatable: op = '>=' return '${#POSITIONALS[@]} %s %s' % (op, positional.get_positional_num()) def _generate_positionals_with_when(generator): code = [] positionals = filter(_is_not_command_arg, generator.positionals) for positional in positionals: condition = _get_positional_condition(positional) r = '(( %s )) && ' % condition if positional.when: r += '%s && ' % bash_when.generate_when_conditions( generator.commandline, generator.variable_manager, positional.when) r += '%s' % _make_block(generator.complete_option(positional)) code.append(r) return '\n\n'.join(code) def _generate_positionals_without_when(generator): code = [] positionals = filter(_is_not_command_arg, generator.positionals) grouped_by_complete = algo.group_by( positionals, generator.complete_option) for complete, positionals in grouped_by_complete.items(): conditions = [_get_positional_condition(p) for p in positionals] r = '(( %s )) && ' % ' || '.join(conditions) r += '%s' % _make_block(complete) code.append(r) return '\n\n'.join(code) def generate(generator): '''Generate code for completing positionals.''' code = [] has_when = any(p.when for p in generator.positionals) if has_when: code += [_generate_positionals_with_when(generator)] else: code += [_generate_positionals_without_when(generator)] if generator.subcommands: code += [_generate_subcommand_positional(generator)] return '\n\n'.join(c for c in code if c) crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_utils.py000066400000000000000000000057501520061347500257770ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Bash utility functions.''' from . import shell def make_option_variable_name(option, prefix=''): '''Make a variable for an option.''' long_options = option.get_long_option_strings() if long_options: return prefix + shell.make_identifier(long_options[0].lstrip('-')) old_options = option.get_old_option_strings() if old_options: return prefix + shell.make_identifier(old_options[0].lstrip('-')) short_options = option.get_short_option_strings() if short_options: return prefix + short_options[0][1] raise AssertionError("make_option_variable_name: Should not be reached") class VariableManager: '''Variable manager.''' def __init__(self, prefix): self.prefix = prefix self.variables = set() def make_variable(self, option): '''Make a variable for an option.''' var = make_option_variable_name(option, self.prefix) self.variables.add(var) return var def capture_variable(self, option): '''Return the capture variable of an option, creating it if necessary.''' if option.capture is None: option.capture = self.make_variable(option) return option.capture def get_variables(self): '''Get a list of all defined variables.''' return list(sorted(self.variables)) class CasePatterns: '''Functions for creating case patterns.''' @staticmethod def for_long_without_arg(option_strings): '''Return a case pattern for long options without arguments.''' return '|'.join(option_strings) @staticmethod def for_long_with_arg(option_strings): '''Return a case pattern for long options with arguments.''' return '|'.join(f'{o}=*' for o in option_strings) @staticmethod def for_short(option_strings): '''Return a case pattern for short options.''' return '|'.join(o[1] for o in option_strings) @staticmethod def for_old_without_arg(option_strings): '''Return a case pattern for old options without arguments.''' if len(option_strings) <= 2: return '|'.join(option_strings) return '-@(%s)' % '|'.join(o.lstrip('-') for o in option_strings) def make_file_extension_pattern(extensions, fuzzy): '''Make a case-insensitive glob pattern matching `extensions`. Takes a list of extensions (e.g. ['txt', 'jpg']) and returns a Bash-style pattern that matches all of them, ignoring case. Example output: '@([tT][xX][tT]|[jJ][pP][gG])' ''' patterns = [] for extension in extensions: pattern = '' for c in extension: if c.isalpha(): pattern += '[%s%s]' % (c.lower(), c.upper()) else: pattern += c patterns.append(pattern) if not fuzzy: return '@(%s)' % '|'.join(patterns) return '@(%s)*' % '|'.join(patterns) crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_versions.py000066400000000000000000000040211520061347500264750ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Version-dependent Bash code.''' # pylint: disable=missing-function-docstring def filedir(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen_filedir' return '_filedir' def init_completion(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_initialize' return '_init_completion' def completion_loader(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_complete_load' return '_completion_loader' def pids(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen_pids' return '_pids' def pnames(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen_pnames' return '_pnames' def uids(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen_uids' return '_uids' def gids(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen_gids' return '_gids' def shells(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen -a shells' return '_shells' def signals(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen_signals' return '_signals' def fstypes(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen -a fstypes' return '_fstypes' def command_offset(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_command_offset' return '_command_offset' def available_interfaces(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen_available_interfaces' return '_available_interfaces' def ip_addresses(ctxt): if ctxt.config.bash_completions_version >= (2, 12): return '_comp_compgen_ip_addresses' return '_ip_addresses' crazy-complete-crazy-complete-bc5afec/crazy_complete/bash_when.py000066400000000000000000000050511520061347500255720ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for creating when conditions in Bash.''' from . import when from . import shell class ConditionGenerator: '''Class for generating conditions.''' def __init__(self, commandline, variable_manager): self.commandline = commandline self.variable_manager = variable_manager def generate(self, tokens): '''Turn tokens/objects into condition code.''' r = [] for obj in tokens: if isinstance(obj, when.OptionIs): r.append(self._gen_option_is(obj)) elif isinstance(obj, when.HasOption): r.append(self._gen_has_option(obj)) elif obj in ('&&', '||', '!'): r.append(obj) elif obj == '(': r.append('{') elif obj == ')': r.append(';}') else: raise AssertionError("Not reached") if when.needs_braces(tokens): return '{ %s; }' % ' '.join(r) return ' '.join(r) def _gen_option_is(self, obj): conditions = [] for o in self.commandline.get_options_by_option_strings(obj.options): have_option = '(( ${#%s[@]} ))' % self.variable_manager.capture_variable(o) value_equals = [] for value in obj.values: value_equals.append('[[ "${%s[-1]}" == %s ]]' % ( self.variable_manager.capture_variable(o), shell.quote(value) )) if len(value_equals) == 1: cond = '{ %s && %s; }' % (have_option, value_equals[0]) else: cond = '{ %s && { %s; } }' % (have_option, ' || '.join(value_equals)) conditions.append(cond) if len(conditions) == 1: return conditions[0] return '{ %s; }' % ' || '.join(conditions) def _gen_has_option(self, obj): conditions = [] for o in self.commandline.get_options_by_option_strings(obj.options): cond = '(( ${#%s[@]} ))' % self.variable_manager.capture_variable(o) conditions.append(cond) if len(conditions) == 1: return conditions[0] return '{ %s; }' % ' || '.join(conditions) def generate_when_conditions(commandline, variable_manager, when_): '''Generate when condition code.''' tokens = when.parse_when(when_) generator = ConditionGenerator(commandline, variable_manager) return generator.generate(tokens) crazy-complete-crazy-complete-bc5afec/crazy_complete/cli.py000066400000000000000000000745341520061347500244170ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains the CommandLine, Option and Positional classes.''' from collections import OrderedDict from types import NoneType from .type_utils import validate_type from .errors import CrazyError, CrazyTypeError from .str_utils import ( contains_space, is_valid_option_string, is_valid_variable_name, is_empty_or_whitespace ) # pylint: disable=redefined-builtin # pylint: disable=too-few-public-methods # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments class ExtendedBool: '''Class that holds an extended bool.''' TRUE = True FALSE = False INHERIT = 'INHERIT' def is_extended_bool(obj): '''Check if `obj` an instance of `ExtendedBool`.''' return obj in (True, False, ExtendedBool.INHERIT) class CommandLine: '''Represents a command line with options, positionals, and subcommands.''' def __init__(self, prog, parent=None, help=None, aliases=None, wraps=None, abbreviate_commands=ExtendedBool.INHERIT, abbreviate_options=ExtendedBool.INHERIT, inherit_options=ExtendedBool.INHERIT): '''Initializes a CommandLine object with the specified parameters. Args: prog (str): The name of the program (or subcommand). help (str): The help message for the program (or subcommand). parent (CommandLine or None): The parent command line object, if any. aliases (list of str or None): Aliases for this command. wraps (str or None): Specify the command to inherit options from. abbreviate_commands (ExtendedBool): Specifies if commands can be abbreviated. abbreviate_options (ExtendedBool): Specifies if options can be abbreviated. inherit_options (ExtendedBool): Specifies if options are visible to subcommands. ''' if aliases is None: aliases = [] validate_type(prog, (str,), 'prog') validate_type(parent, (CommandLine, NoneType), 'parent') validate_type(help, (str, NoneType), 'help') validate_type(aliases, (list,), 'aliases') validate_type(wraps, (str, NoneType), 'wraps') if wraps is not None: if is_empty_or_whitespace(wraps): raise CrazyError('wraps is empty') if contains_space(wraps): raise CrazyError('wraps: cannot contain space') for index, alias in enumerate(aliases): if not isinstance(alias, str): raise CrazyTypeError(f'aliases[{index}]', 'str', alias) if contains_space(alias): raise CrazyError(f'aliases[{index}]: cannot contain space') if not is_extended_bool(abbreviate_commands): raise CrazyTypeError('abbreviate_commands', 'ExtendedBool', abbreviate_commands) if not is_extended_bool(abbreviate_options): raise CrazyTypeError('abbreviate_options', 'ExtendedBool', abbreviate_options) if not is_extended_bool(inherit_options): raise CrazyTypeError('inherit_options', 'ExtendedBool', inherit_options) self.prog = prog self.parent = parent self.help = help self.aliases = aliases self.wraps = wraps self.abbreviate_commands = abbreviate_commands self.abbreviate_options = abbreviate_options self.inherit_options = inherit_options self.options = [] self.positionals = [] self.subcommands = None def add_option(self, option_strings, **parameters): '''Adds a new option to the command line. For a list of valid parameters, see `class Option`. Returns: Option: The newly added Option object. ''' o = Option(self, option_strings, **parameters) self.options.append(o) return o def add_positional(self, number, **parameters): '''Adds a new positional argument to the command line. For a list of valid parameters, see `class Positional`. Returns: Positional: The newly added Positional object. ''' p = Positional(self, number, **parameters) self.positionals.append(p) return p def add_mutually_exclusive_group(self, group): '''Adds a new mutually exclusive group. Returns: MutuallyExclusiveGroup: The newly created mutually exclusive group. ''' return MutuallyExclusiveGroup(self, group) def add_subcommands(self): '''Adds a subcommands option to the command line. Returns: SubCommandsOption: The newly created subcommands option. Raises: CrazyError: If the command line object already has subcommands. ''' if self.subcommands: raise CrazyError('CommandLine object already has subcommands') self.subcommands = SubCommandsOption(self) return self.subcommands class OptionsGetter: '''A class for getting options from a CommandLine object.''' def __init__(self, commandline, with_parent_options=False, only_with_arguments=False): self.options = OrderedDict() if with_parent_options: commandlines = commandline.get_parents(include_self=True) else: commandlines = [commandline] for cmdline in reversed(commandlines): for option in cmdline.options: if not only_with_arguments or option.complete: self._add(option, cmdline) def _add(self, option, commandline): key = option.get_option_strings_key() if key not in self.options: self.options[key] = (commandline, []) if self.options[key][0] == commandline: self.options[key][1].append(option) def get(self): '''Return the result.''' r = [] for _, options in self.options.values(): for option in options: r.append(option) return r def get_options(self, with_parent_options=False, only_with_arguments=False): '''Gets a list of options associated with the command line. Args: with_parent_options (bool): If True, include options from parent command lines. only_with_arguments (bool): If True, include only options that take arguments. Returns: list: A list of Option objects ''' assert isinstance(with_parent_options, bool) assert isinstance(only_with_arguments, bool) getter = CommandLine.OptionsGetter( self, with_parent_options=with_parent_options, only_with_arguments=only_with_arguments) return getter.get() def get_option_strings(self, with_parent_options=False, only_with_arguments=False): '''Gets a list of option strings associated with the command line. Args: with_parent_options (bool): If True, include options from parent command lines. only_with_arguments (bool): If True, include only options that take arguments. Returns: list: A list of option strings ''' assert isinstance(with_parent_options, bool) assert isinstance(only_with_arguments, bool) option_strings = [] options = self.get_options( with_parent_options=with_parent_options, only_with_arguments=only_with_arguments) for o in options: option_strings.extend(o.option_strings) return option_strings def get_final_options(self): '''Gets a list of all final options.''' r = [] cmdline = self while cmdline: for option in cmdline.options: if option.final: r.append(option) cmdline = cmdline.parent return r def get_final_option_strings(self): '''Gets a list of all final option strings.''' r = [] for option in self.get_final_options(): r.extend(option.option_strings) return r def get_positionals(self): '''Gets a list of positional arguments associated with the command line. Note: SubCommandsOption objects are not considered positional arguments and are not included in the list. Returns: list: A list of positional arguments ''' return list(self.positionals) def get_subcommands(self): '''Gets the subcommands of the command line. Returns: SubCommandsOption or None: The subcommands option if it exists, otherwise None. ''' return self.subcommands def get_parents(self, include_self=False): '''Gets a list of parent CommandLine objects. Args: include_self (bool): If True, includes the current CommandLine object in the list. Returns: list: A list of parent CommandLine objects. ''' assert isinstance(include_self, bool) parents = [] parent = self.parent while parent: parents.insert(0, parent) parent = parent.parent if include_self: parents.append(self) return parents def get_root_commandline(self): '''Return the root commandline.''' if not self.parent: return self return self.parent.get_root_commandline() def get_options_by_option_strings(self, option_strings): '''Return all options containing one option_strings.''' result = [] for option_string in option_strings: found = False for option in self.options: if option_string in option.option_strings: if option not in result: result.append(option) found = True break if not found: raise CrazyError('Option %r not found' % option_string) return result def get_highest_positional_num(self): '''Get the highest positional number.''' highest = 0 for positional in self.positionals: highest = max(highest, positional.number) if self.subcommands: highest += 1 return highest def get_program_name(self): '''Return the program name.''' commandlines = self.get_parents(include_self=True) return commandlines[0].prog def get_command_path(self, progname=None): '''Return the full command path.''' prognames = [c.prog for c in self.get_parents(include_self=True)] if progname is not None: prognames[0] = progname return ' '.join(prognames) def visit_commandlines(self, callback): '''Apply a callback to all CommandLine objects.''' callback(self) if self.get_subcommands(): for sub in self.get_subcommands().subcommands: sub.visit_commandlines(callback) def get_all_commandlines(self): '''Get a list of all defined commandlines.''' result = [] self.visit_commandlines(result.append) return result def copy(self): '''Make a deep copy of the current CommandLine object.''' copy = CommandLine( self.prog, parent = None, help = self.help, aliases = self.aliases, wraps = self.wraps, abbreviate_commands = self.abbreviate_commands, abbreviate_options = self.abbreviate_options, inherit_options = self.inherit_options) for option in self.options: copy.add_option( option.option_strings, metavar = option.metavar, help = option.help, complete = option.complete, nosort = option.nosort, optional_arg = option.optional_arg, groups = option.groups, repeatable = option.repeatable, final = option.final, hidden = option.hidden, when = option.when, capture = option.capture, long_opt_arg_sep = option.long_opt_arg_sep ) for positional in self.positionals: copy.add_positional( positional.number, metavar = positional.metavar, help = positional.help, repeatable = positional.repeatable, nosort = positional.nosort, complete = positional.complete, when = positional.when, capture = positional.capture ) if self.subcommands is not None: subcommands_option = copy.add_subcommands() for subparser in self.subcommands.subcommands: subcommands_option.add_commandline_object(subparser.copy()) return copy def __eq__(self, other): return ( isinstance(other, CommandLine) and self.prog == other.prog and self.aliases == other.aliases and self.help == other.help and self.wraps == other.wraps and self.abbreviate_commands == other.abbreviate_commands and self.abbreviate_options == other.abbreviate_options and self.inherit_options == other.inherit_options and self.options == other.options and self.positionals == other.positionals and self.subcommands == other.subcommands ) def __repr__(self): r = '{' r += f'\nprog: {self.prog!r},' r += f'\naliases: {self.aliases!r},' r += f'\nwraps: {self.wraps!r},' r += f'\nhelp: {self.help!r},' r += f'\nabbreviate_commands: {self.abbreviate_commands!r},' r += f'\nabbreviate_options: {self.abbreviate_options!r},' r += f'\ninherit_options: {self.inherit_options!r},' r += f'\noptions: {self.options!r},' r += f'\npositionals: {self.positionals!r},' r += f'\nsubcommands: {self.subcommands!r},' r += '\n}' return r class Positional: '''Class representing a command line positional.''' def __init__( self, parent, number, metavar=None, help=None, complete=None, nosort=False, repeatable=False, when=None, capture=None): '''Initializes a Positional object with the specified parameters. Args: parent (CommandLine): The parent command line object, if any. number (int): The number of the positional argument (starting from 1) metavar (str): The metavar for the positional. help (str): The help message for the positional. repeatable (bool): Specifies if positional can be specified more times complete (list): The completion specification for the positional. nosort (bool): Do not sort the completion suggestions. when (str): Specifies a condition for showing this positional. capture (str): Specifies the variable name for capturing ''' validate_type(parent, (CommandLine, NoneType), 'parent') validate_type(number, (int,), 'number') validate_type(metavar, (str, NoneType), 'metavar') validate_type(help, (str, NoneType), 'help') validate_type(complete, (list, tuple, NoneType), 'complete') validate_type(repeatable, (bool,), 'repeatable') validate_type(nosort, (bool,), 'nosort') validate_type(when, (str, NoneType), 'when') validate_type(capture, (str, NoneType), 'capture') if capture is not None and not is_valid_variable_name(capture): raise CrazyError(f"Invalid variable name: {capture!r}") if number <= 0: raise CrazyError(f'number: value ({number}) is invalid, number has to be >= 1') self.parent = parent self.number = number self.metavar = metavar self.help = help self.repeatable = repeatable self.complete = complete if complete else ['none'] self.nosort = nosort self.when = when self.capture = capture def get_positional_index(self): '''Returns the index of the current positional argument within the current commandline, including parent commandlines. Returns: int: The index of the positional argument. ''' positional_no = self.number - 1 for commandline in self.parent.get_parents(): highest = 0 for positional in commandline.get_positionals(): highest = max(highest, positional.number) positional_no += highest if commandline.get_subcommands(): positional_no += 1 return positional_no def get_positional_num(self): '''Returns the number of the current positional argument within the current commandline, including parent commandlines. Note: This is the same as `CommandLine.get_positional_index() + 1`. Returns: int: The number of the positional argument. ''' return self.get_positional_index() + 1 def __eq__(self, other): return ( isinstance(other, Positional) and self.number == other.number and self.metavar == other.metavar and self.help == other.help and self.repeatable == other.repeatable and self.complete == other.complete and self.nosort == other.nosort and self.when == other.when and self.capture == other.capture ) def is_short_option_string(string): '''Return True if string is a short option string.''' return len(string) == 2 def is_long_option_string(string): '''Return True if string is a long option string.''' return string.startswith('--') def is_old_option_string(string): '''Return True if string is an old option string.''' return not string.startswith('--') and len(string) > 2 class Option: '''Class representing a command line option.''' # pylint: disable=too-many-locals def __init__( self, parent, option_strings, metavar=None, help=None, complete=None, nosort=False, groups=None, optional_arg=False, repeatable=ExtendedBool.INHERIT, final=False, hidden=False, when=None, capture=None, long_opt_arg_sep=ExtendedBool.INHERIT): '''Initializes an Option object with the specified parameters. Args: parent (CommandLine): The parent command line object, if any. option_strings (list of str): The list of option strings. metavar (str): The metavar for the option. help (str): The help message for the option. complete (tuple): The completion specification for the option. nosort (bool): Do not sort the completion suggestions. optional_arg (bool): Specifies if option's argument is optional. groups (list of str): Specify to which mutually exclusive groups this option belongs to. repeatable (ExtendedBool): Specifies if the option can be repeated. final (bool): If True, no more options are suggested after this one. hidden (bool): Specifies if this option is hidden. when (str): Specifies a condition for showing this option. capture (str): Specifies the variable name for capturing long_opt_arg_sep (str): Specifies which separators are used for delimiting a long option from its argument. If `space`, only the form `--option argument` is allowed. If `equals`, only the form `--option=argument` is allowed. If `both`, both forms are allowed. If `INHERIT`, the default setting from the config object is used. Returns: Option: The newly added Option object. ''' validate_type(parent, (CommandLine, NoneType), 'parent') validate_type(option_strings, (list,), 'option_strings') validate_type(metavar, (str, NoneType), 'metavar') validate_type(help, (str, NoneType), 'help') validate_type(complete, (list, tuple, NoneType), 'complete') validate_type(groups, (list, NoneType), 'groups') if groups is not None: for index, group in enumerate(groups): if not isinstance(group, str): raise CrazyTypeError(f'groups[{index}]', 'str', group) validate_type(optional_arg, (bool,), 'optional_arg') if not is_extended_bool(repeatable): raise CrazyTypeError('repeatable', 'ExtendedBool', repeatable) validate_type(final, (bool,), 'final') validate_type(hidden, (bool,), 'hidden') validate_type(nosort, (bool,), 'nosort') validate_type(when, (str, NoneType), 'when') validate_type(capture, (str, NoneType), 'capture') validate_type(long_opt_arg_sep, (str,), 'long_opt_arg_sep') if not option_strings: raise CrazyError('Empty option strings') for index, option_string in enumerate(option_strings): if not isinstance(option_string, str): raise CrazyTypeError(f'option_strings[{index}]', 'str', option_string) if not is_valid_option_string(option_string): raise CrazyError(f"Invalid option string: {option_string!r}") if capture is not None and not is_valid_variable_name(capture): raise CrazyError(f"Invalid variable name: {capture!r}") if metavar and not complete: raise CrazyError(f'Option {option_strings} has metavar set, but has no complete') if optional_arg and not complete: raise CrazyError(f'Option {option_strings} has optional_arg=True, but has no complete') if hidden is True and repeatable is True: raise CrazyError(f'Option {option_strings} has both hidden and repeatable set to True') if long_opt_arg_sep not in (ExtendedBool.INHERIT, 'space', 'equals', 'both'): raise CrazyError('Invalid value for `long_opt_arg_sep`') self.parent = parent self.option_strings = option_strings self.metavar = metavar self.help = help self.complete = complete self.nosort = nosort self.groups = groups self.optional_arg = optional_arg self.repeatable = repeatable self.final = final self.hidden = hidden self.when = when self.capture = capture self.long_opt_arg_sep = long_opt_arg_sep def get_option_strings(self): '''Returns the option strings associated with the Option object. Returns: list: A list of strings representing the option strings. ''' return self.option_strings def get_option_strings_key(self, delimiter=' '): '''Returns a key with consisting of all option strings.''' return delimiter.join(sorted(self.option_strings)) def get_short_option_strings(self): '''Returns the short option strings associated with the object. Returns: list: A list of short option strings ("-o"). ''' return list(filter(is_short_option_string, self.option_strings)) def get_long_option_strings(self): '''Returns the long option strings associated with the object. Returns: list: A list of long option strings ("--option"). ''' return list(filter(is_long_option_string, self.option_strings)) def get_old_option_strings(self): '''Returns the old-style option strings associated with the object. Returns: list: A list of old-style option strings ("-option"). ''' return list(filter(is_old_option_string, self.option_strings)) def get_conflicting_options(self): '''Returns a list of conflicting options within the same mutually exclusive groups. Returns: list: A list of Option objects representing conflicting options. ''' if not self.groups: return [] r = [] for group in self.groups: for option in self.parent.options: if option.groups is not None and group in option.groups: if option not in r: r.append(option) r.remove(self) return r def get_conflicting_option_strings(self): '''Returns a list of option strings conflicting with the current option within the same mutually exclusive groups. Returns: list: A list of option strings representing conflicting options. ''' r = [] for option in self.get_conflicting_options(): r.extend(option.get_option_strings()) return r def has_required_arg(self): '''Return True if the option takes a required argument.''' return self.complete is not None and self.optional_arg is False def has_optional_arg(self): '''Return True if the option takes an optional argument.''' return self.complete is not None and self.optional_arg is True def __eq__(self, other): return ( isinstance(other, Option) and self.option_strings == other.option_strings and self.metavar == other.metavar and self.help == other.help and self.optional_arg == other.optional_arg and self.repeatable == other.repeatable and self.final == other.final and self.hidden == other.hidden and self.complete == other.complete and self.nosort == other.nosort and self.groups == other.groups and self.when == other.when and self.capture == other.capture ) def __repr__(self): return '{option_strings: %r, metavar: %r, help: %r}' % ( self.option_strings, self.metavar, self.help) class SubCommandsOption(Positional): '''Class holding subcommands of a commandline.''' def __init__(self, parent): self.subcommands = [] super().__init__( parent, parent.get_highest_positional_num() + 1, metavar='command') def add_commandline_object(self, commandline): '''Add an existing commandline object as a subcommand.''' if self.get_subcommand_by_name(commandline.prog): raise CrazyError(f'Duplicate subcommand. {commandline.prog} already defined.') commandline.parent = self.parent self.subcommands.append(commandline) def add_commandline(self, name, help=''): '''Create a new commandline object and add it as a subcommand.''' if self.get_subcommand_by_name(name): raise CrazyError(f'Duplicate subcommand. {name} already defined.') commandline = CommandLine(name, help=help, parent=self.parent) self.subcommands.append(commandline) return commandline def get_choices(self, with_aliases=True): '''Return a mapping of command and its description.''' r = OrderedDict() for subcommand in self.subcommands: r[subcommand.prog] = subcommand.help if with_aliases: for alias in subcommand.aliases: r[alias] = subcommand.help return r def get_subcommand_by_name(self, name): '''Return a subcommand by name or None if not found.''' for subcommand in self.subcommands: if subcommand.prog == name: return subcommand return None def __eq__(self, other): return ( isinstance(other, SubCommandsOption) and self.subcommands == other.subcommands and self.help == other.help and self.metavar == other.metavar and self.complete == other.complete ) def __repr__(self): return '{help: %r, subcommands %r}' % (self.help, self.subcommands) class MutuallyExclusiveGroup: '''Helper class for adding mutually exclusive options.''' def __init__(self, parent, group): assert isinstance(parent, CommandLine) assert isinstance(group, str) self.parent = parent self.group = group def add(self, option_strings, **parameters): '''Creates and adds a new option. For a list of valid parameters, see `class Option`. ''' if 'groups' in parameters: raise CrazyError('Paramter `groups` not allowed') if 'group' in parameters: raise CrazyError('Paramter `group` not allowed') return self.parent.add_option( option_strings, groups=[self.group], **parameters) def add_option(self, option): '''Adds an option object.''' option.parent = self.parent option.groups = [self.group] crazy-complete-crazy-complete-bc5afec/crazy_complete/compat.py000066400000000000000000000026161520061347500251230ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for maintaining backward compatibility with previous versions.''' from . import utils # Wrong types in configuration structures are silently ignored, because # they are handled elsewhere def fix_option_dictionary(dictionary): '''Fix an option dictionary.''' if not isinstance(dictionary, dict): return if 'group' in dictionary: if 'groups' in dictionary: msg = ('Both `group` and `groups` found. `group` is deprecated. ' 'Removing `group` in favour of `groups`') utils.warn(msg) dictionary.pop('group') else: utils.warn('`group` is deprecated. Please use `groups` instead') dictionary['groups'] = [dictionary.pop('group')] def fix_commandline_dictionary(dictionary): '''Fix a commandline dictionary.''' if not isinstance(dictionary, dict): return options = dictionary.get('options', []) if isinstance(options, list): for option in options: fix_option_dictionary(option) def fix_commandline_dictionaries(dictionaries): '''Fix a list of commandline dictionaries.''' if not isinstance(dictionaries, list): return for commandline_dictionary in dictionaries: fix_commandline_dictionary(commandline_dictionary) crazy-complete-crazy-complete-bc5afec/crazy_complete/completion_validator.py000066400000000000000000000063311520061347500300540ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains code for validating completing definitions.''' # Unlike the `scheme_validator`, this validator operates directly on Python # objects and therefore does not have access to line and column information. # To save code and maintain consistency, we reuse the validation logic # from `scheme_validator` by wrapping the objects in `ValueWithoutTrace`. # This provides the same structure as used in `scheme_validator`, except that # we use `ValueWithoutTrace` instead of `ValueWithTrace`. from types import NoneType from . import scheme_validator from .value_with_trace import ValueWithOutTrace def _mk_value_without_trace(value): if isinstance(value, ValueWithOutTrace): return value if isinstance(value, (NoneType, str, int, float, bool)): return ValueWithOutTrace(value) mk = _mk_value_without_trace if isinstance(value, (list, tuple)): return ValueWithOutTrace(list(map(mk, value))) if isinstance(value, dict): return ValueWithOutTrace({mk(k): mk(v) for k, v in value.items()}) raise AssertionError(f"Not reached: {value!r}") def _option_to_definition(option): return { 'option_strings': option.option_strings, 'metavar': option.metavar, 'help': option.help, 'complete': option.complete, 'nosort': option.nosort, 'groups': option.groups, 'optional_arg': option.optional_arg, 'repeatable': option.repeatable, 'final': option.final, 'hidden': option.hidden, 'when': option.when, 'capture': option.capture, } def _positional_to_definition(positional): return { 'number': positional.number, 'metavar': positional.metavar, 'help': positional.help, 'repeatable': positional.repeatable, 'complete': positional.complete, 'nosort': positional.nosort, 'when': positional.when, 'capture': positional.capture, } def _commandline_to_definition(cmdline): options = list(map(_option_to_definition, cmdline.options)) positionals = list(map(_positional_to_definition, cmdline.positionals)) definition = { 'prog': cmdline.get_command_path(), 'help': cmdline.help, 'aliases': cmdline.aliases, 'wraps': cmdline.wraps, 'abbreviate_commands': cmdline.abbreviate_commands, 'abbreviate_options': cmdline.abbreviate_options, 'inherit_options': cmdline.inherit_options, 'options': options, 'positionals': positionals, } return _mk_value_without_trace(definition) def _commandlines_to_definition_list(commandline): result = [] def callback(cmdline): result.append(_commandline_to_definition(cmdline)) commandline.visit_commandlines(callback) return result def validate_commandlines(cmdline): '''Validate commandlines.''' definitions = _commandlines_to_definition_list(cmdline) scheme_validator.validate(definitions) crazy-complete-crazy-complete-bc5afec/crazy_complete/config.py000066400000000000000000000324451520061347500251100ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains the configuration class.''' import sys from .type_utils import is_list_type def _assert_is_bool(obj, func, param): if isinstance(obj, bool): return raise AssertionError(f"Config.{func}: {param}: expected bool, got `{obj}`") class Config: '''Configuration settings for command line completion.''' # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods def __init__(self): self.function_prefix = '_$PROG' self.debug = False self.abbreviate_commands = False self.abbreviate_options = False self.repeatable_options = False self.inherit_options = False self.option_stacking = True self.long_opt_arg_sep = 'both' self.vim_modeline = True self.include_files = [] self.comments = [] self.keep_comments = False self.line_length = 80 self.bash_completions_version = (2,) self.zsh_compdef = True self.fish_fast = False self.fish_inline_conditions = False self.disabled_hidden = False self.disabled_final = False self.disabled_groups = False self.disabled_repeatable = False self.disabled_when = False def set_function_prefix(self, prefix): '''Sets the function prefix used for generated functions. Args: prefix (str): The prefix to be used. May contain the `$PROG` placeholder. Notes: This defaults to `_$PROG` ''' assert isinstance(prefix, str) self.function_prefix = prefix def set_debug(self, enable): '''Sets debug mode. Args: enable (bool): If True, additional code for debugging is generated. Notes: This feature defaults to `False`. ''' _assert_is_bool(enable, "set_debug", "enable") self.debug = enable def set_abbreviate_commands(self, enable): '''Sets whether commands can be abbreviated. Args: enable (bool): If True, commands can be abbreviated; if False, they cannot. Notes: This feature defaults to `False`. Implementation status for shells: Bash: - set_abbreviate_commands(True): works - set_abbreviate_commands(False): works Fish: - set_abbreviate_commands(True): works - set_abbreviate_commands(False): works Zsh: - set_abbreviate_commands(True): works - set_abbreviate_commands(False): works See Also: cli.CommandLine(..., abbreviate_commands=BOOL, ...) ''' _assert_is_bool(enable, "set_abbreviate_commands", "enable") self.abbreviate_commands = enable def set_abbreviate_options(self, enable): '''Sets whether options can be abbreviated. Args: enable (bool): If True, options can be abbreviated; if False, they cannot. Notes: This feature defaults to `False`. Implementation status for shells: Bash: - set_abbreviate_options(True): works - set_abbreviate_options(False): works Fish: - set_abbreviate_options(True): not implemented - set_abbreviate_options(False): works Zsh: - set_abbreviate_options(True): not implemented - set_abbreviate_options(False): works See Also: cli.CommandLine(..., abbreviate_options=BOOL, ...) ''' _assert_is_bool(enable, "set_abbreviate_options", "enable") self.abbreviate_options = enable def set_repeatable_options(self, enable): '''Sets whether options are suggested multiple times during completion. Args: enable (bool): If True, options can appear multiple times during completion; if False, options are suggested only once. Notes: This feature defaults to `False`. Implementation status for shells: Bash: - set_repeatable_options(True): works - set_repeatable_options(False): works Fish: - set_repeatable_options(True): works - set_repeatable_options(False): works Zsh: - set_repeatable_options(True): works - set_repeatable_options(False): works See Also: cli.CommandLine.add_option(..., repeatable=BOOL, ...) ''' _assert_is_bool(enable, "set_repeatable_options", "enable") self.repeatable_options = enable def set_inherit_options(self, enable): '''Sets whether parent options are visible to subcommands. Args: enable (bool): If True, parent options are visible to subcommands. If False, they are not. Notes: This feature defaults to `False`. Implementation status for shells: Bash: - set_inherit_options(True): works - set_inherit_options(False): works Fish: - set_inherit_options(True): works - set_inherit_options(False): works Zsh: - set_inherit_options(True): works - set_inherit_options(False): works See Also: cli.CommandLine(..., inherit_options=BOOL, ...) ''' _assert_is_bool(enable, "set_inherit_options", "enable") self.inherit_options = enable def set_option_stacking(self, enable): '''Sets wether short option stacking is allowed. Args: enable (bool): If True, option stacking is allowed. If False, it is not. Notes: This feature defaults to `True`. Implementation status for shells: Bash: - set_option_stacking(True): works - set_option_stacking(False): not implemented Fish: - set_option_stacking(True): works - set_option_stacking(False): works Zsh: - set_option_stacking(True): works - set_option_stacking(False): works ''' _assert_is_bool(enable, "set_option_stacking", "enable") self.option_stacking = enable def set_long_option_argument_separator(self, separator): '''Sets which separators are used for delimiting a long option from its argument. Args: separator (string): If `space`, only the form `--option argument` is allowed. If `equals`, only the form `--option=argument` is allowed. If `both`, both forms are allowed. Notes: This feature defaults to `both`. Implementation status for shells: Bash: - set_long_option_argument_separator('space'): works - set_long_option_argument_separator('equals'): works - set_long_option_argument_separator('both'): works Fish: - set_long_option_argument_separator('space'): not implemented - set_long_option_argument_separator('equals'): not implemented - set_long_option_argument_separator('both'): works Zsh: - set_long_option_argument_separator('space'): works - set_long_option_argument_separator('equals'): works - set_long_option_argument_separator('both'): works ''' if separator not in ('space', 'equals', 'both'): raise AssertionError('Config.set_long_option_argument_separator: ' 'separator: Invalid value: ' 'expected: `space`, `equals` or `both`') self.long_opt_arg_sep = separator def set_vim_modeline(self, enable): '''Sets whether a vim modeline comment shall be appended to the code. The modeline comment looks like this: # vim: ft=zsh ts=2 sts=2 sw=2 et Args: enable (bool): If True, add a vim modline comment; if False, don't add a modline comment. Notes: This feature defaults to `True`. ''' _assert_is_bool(enable, "set_vim_modeline", "enable") self.vim_modeline = enable def set_bash_completions_version(self, version): '''Sets the version of bash-completions. Args: version (tuple): A tuple in form like (2,) or (2, 12). Notes: This defaults to (2,). ''' assert isinstance(version, tuple) self.bash_completions_version = version def set_zsh_compdef(self, enable): '''Sets whether a `#compdef` comment is written at the top of the generated zsh script. The `#compdef` directive is used by Zsh to automatically associate the generated completion file with a command, enabling autoload functionality. If you plan to load the Zsh completion file manually by sourcing it, omitting this line may be necessary. Args: enable (bool): If true, add a `#compdef` line on top of the file; If false, don't add a `#compdef` line. Notes: This feature defaults to `True` ''' _assert_is_bool(enable, "set_zsh_compdef", "enable") self.zsh_compdef = enable def set_keep_comments(self, enable): '''Sets whether comments shall be included in the generated code. Args: enable (bool): If True, comments are included. if False, don't include comments. Notes: This feature defaults to `False`. ''' _assert_is_bool(enable, "set_keep_comments", "enable") self.keep_comments = enable def set_line_length(self, length): '''Sets the maximum line length of the generated code. Args: length (int): The maximum line length. If zero or negative, maximum line length is selected. Notes: This defaults to `80`. ''' assert isinstance(length, int) self.line_length = length if length > 0 else sys.maxsize def set_fish_fast(self, enable): '''Use faster conditions at the cost of correctness.''' _assert_is_bool(enable, "set_fish_fast", "enable") self.fish_fast = enable def set_fish_inline_conditions(self, enable): '''Don't store conditions in an extra variable.''' _assert_is_bool(enable, "set_fish_inline_conditions", "enable") self.fish_inline_conditions = enable def include_file(self, file): '''Add a file which should be included to the generated code.''' assert isinstance(file, str), \ f"Config.include_file: file: expected str, got `{file}`" self.include_files.append(file) def include_many_files(self, files): '''Add files which should be included to the generated code.''' assert is_list_type(files) self.include_files.extend(files) def add_comments(self, comments): '''Add comments to the generated output.''' assert is_list_type(comments) self.comments.extend(comments) def disable_hidden(self, disable): '''Disable hidden options. This disables hidden options completely. ''' _assert_is_bool(disable, "disable_hidden", "disable") self.disabled_hidden = disable def disable_final(self, disable): '''Disable final options. This disables final options completely. ''' _assert_is_bool(disable, "disable_final", "disable") self.disabled_final = disable def disable_groups(self, disable): '''Disable option grouping. This disables the mutually exclusive feature for options completely. ''' _assert_is_bool(disable, "disable_groups", "disable") self.disabled_groups = disable def disable_repeatable(self, disable): '''Disable repeatable options. This disables repeatable options completely. Despite its name, this function actually does the opposite: Instead of making all options non-repeatable, it makes all options repeatable. ''' _assert_is_bool(disable, "disable_repeatable", "disable") self.disabled_repeatable = disable def disable_when(self, disable): '''Disable when feature. This disables conditional options and positionals completely. ''' _assert_is_bool(disable, "disable_when", "disable") self.disabled_when = disable crazy-complete-crazy-complete-bc5afec/crazy_complete/dictionary_source.py000066400000000000000000000231021520061347500273560ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth """ This module provides functions for creating CommandLine objects from dictionaries and vice versa. """ from collections import OrderedDict from .errors import CrazyError, CrazyTypeError from .cli import CommandLine, ExtendedBool from .str_utils import validate_prog from . import compat _INHERIT = ExtendedBool.INHERIT def _validate_keys(dictionary, allowed_keys): for key in dictionary.keys(): if key not in allowed_keys: raise CrazyError(f'Unknown key: {key}') def dictionary_to_commandline(dictionary, prog=None): '''Convert a single dictionary to a cli.CommandLine object.''' _validate_keys(dictionary, ['prog', 'help', 'aliases', 'wraps', 'abbreviate_commands', 'abbreviate_options', 'inherit_options', 'options', 'positionals']) options = dictionary.get('options', []) if not isinstance(options, list): raise CrazyTypeError('options', 'list', options) positionals = dictionary.get('positionals', []) if not isinstance(positionals, list): raise CrazyTypeError('positionals', 'list', positionals) commandline = CommandLine( prog or dictionary['prog'], parent = None, help = dictionary.get('help', None), aliases = dictionary.get('aliases', []), wraps = dictionary.get('wraps', None), abbreviate_commands = dictionary.get('abbreviate_commands', _INHERIT), abbreviate_options = dictionary.get('abbreviate_options', _INHERIT), inherit_options = dictionary.get('inherit_options', _INHERIT)) for option in options: _validate_keys(option, ['option_strings', 'metavar', 'help', 'optional_arg', 'groups', 'repeatable', 'final', 'hidden', 'complete', 'nosort', 'when', 'capture', 'long_opt_arg_sep']) commandline.add_option( option.get('option_strings', None), metavar = option.get('metavar', None), help = option.get('help', None), optional_arg = option.get('optional_arg', False), groups = option.get('groups', None), repeatable = option.get('repeatable', _INHERIT), final = option.get('final', False), hidden = option.get('hidden', False), complete = option.get('complete', None), nosort = option.get('nosort', False), when = option.get('when', None), capture = option.get('capture', None), long_opt_arg_sep = option.get('long_opt_arg_sep', _INHERIT) ) for positional in positionals: _validate_keys(positional, ['number', 'metavar', 'help', 'repeatable', 'complete', 'nosort', 'when', 'capture']) commandline.add_positional( positional.get('number', None), metavar = positional.get('metavar', None), help = positional.get('help', None), repeatable = positional.get('repeatable', False), complete = positional.get('complete', None), nosort = positional.get('nosort', False), when = positional.get('when', None), capture = positional.get('capture', None)) return commandline def _check_prog_in_dictionary(dictionary): if 'prog' not in dictionary: raise CrazyError('Missing `prog` field') if not isinstance(dictionary['prog'], str): raise CrazyTypeError('prog', 'str', dictionary["prog"]) try: validate_prog(dictionary['prog']) except CrazyError as e: raise CrazyError(f'prog: {e}') from e def _get_commandline_by_path(root, path): current = root for i, name in enumerate(path): path_str = ' '.join(path[0:i + 1]) if not current.get_subcommands(): raise CrazyError(f"Missing definition of program `{path_str}`") current = current.get_subcommands().get_subcommand_by_name(name) if not current: raise CrazyError(f"Missing definition of program `{path_str}`") return current def replace_defines_in_documents(documents): '''Replaces defines in a list of documents. Exmaple: prog: '%defines%' my_completer: ['choices', ['foo', 'bar']] --- prog: 'example' options: - option_strings: ['-o'] complete: 'my_completer' ''' defines = None for document in documents: if not isinstance(document, dict): continue if 'prog' in document and document['prog'] == '%defines%': defines = document break if defines is None: return documents documents.remove(defines) defines.pop('prog') def replace_defines(obj): if isinstance(obj, str): return defines.get(obj, obj) if isinstance(obj, list): return [replace_defines(sub) for sub in obj] if isinstance(obj, dict): return {key: replace_defines(val) for key, val in obj.items()} return obj new = [] for document in documents: new.append(replace_defines(document)) return new def dictionaries_to_commandline(dictionaries): '''Convert a list of dictionaries to a cli.CommandLine object.''' dictionaries = replace_defines_in_documents(dictionaries) compat.fix_commandline_dictionaries(dictionaries) root = CommandLine('root') for dictionary in dictionaries: _check_prog_in_dictionary(dictionary) path = dictionary['prog'].split() progname = path.pop() cmdline = _get_commandline_by_path(root, path) subcommands = cmdline.get_subcommands() if not subcommands: subcommands = cmdline.add_subcommands() new_cmdline = dictionary_to_commandline(dictionary, progname) try: subcommands.add_commandline_object(new_cmdline) except CrazyError as e: raise CrazyError(f"Multiple definition of program `{progname}`") from e if not root.get_subcommands(): raise CrazyError("No programs defined") if len(root.get_subcommands().subcommands) > 1: progs = [c.prog for c in root.get_subcommands().subcommands] raise CrazyError(f"Too many main programs defined: {progs}") cmdline = root.get_subcommands().subcommands[0] cmdline.parent = None return cmdline def option_to_dictionary(self): '''Convert a cli.Option object to a dictionary.''' r = OrderedDict() r['option_strings'] = self.option_strings if self.metavar is not None: r['metavar'] = self.metavar if self.help is not None: r['help'] = self.help if self.optional_arg is not False: r['optional_arg'] = self.optional_arg if self.groups is not None: r['groups'] = self.groups if self.repeatable != ExtendedBool.INHERIT: r['repeatable'] = self.repeatable if self.final is True: r['final'] = self.final if self.hidden is True: r['hidden'] = self.hidden if self.complete: r['complete'] = self.complete if self.nosort is True: r['nosort'] = self.nosort if self.when is not None: r['when'] = self.when if self.capture is not None: r['capture'] = self.capture if self.long_opt_arg_sep != ExtendedBool.INHERIT: r['long_opt_arg_sep'] = self.long_opt_arg_sep return r def positional_to_dictionary(self): '''Convert a cli.Positional object to a dictionary.''' r = OrderedDict() r['number'] = self.number if self.metavar is not None: r['metavar'] = self.metavar if self.help is not None: r['help'] = self.help if self.repeatable is not False: r['repeatable'] = self.repeatable if self.complete: r['complete'] = self.complete if self.nosort is True: r['nosort'] = self.nosort if self.when is not None: r['when'] = self.when if self.capture is not None: r['capture'] = self.capture return r def commandline_to_dictionary(commandline): '''Convert a cli.CommandLine object to a dictionary.''' r = OrderedDict() r['prog'] = commandline.get_command_path() if commandline.aliases: r['aliases'] = commandline.aliases if commandline.help is not None: r['help'] = commandline.help if commandline.wraps is not None: r['wraps'] = commandline.wraps if commandline.abbreviate_commands != ExtendedBool.INHERIT: r['abbreviate_commands'] = commandline.abbreviate_commands if commandline.abbreviate_options != ExtendedBool.INHERIT: r['abbreviate_options'] = commandline.abbreviate_options if commandline.inherit_options != ExtendedBool.INHERIT: r['inherit_options'] = commandline.inherit_options if commandline.options: r['options'] = options = [] for option in commandline.options: options.append(option_to_dictionary(option)) if commandline.positionals: r['positionals'] = positionals = [] for positional in commandline.positionals: positionals.append(positional_to_dictionary(positional)) return r def commandline_to_dictionaries(commandline): '''Convert a cli.CommandLine object to dictionaries.''' dictionaries = [] commandline.visit_commandlines( lambda c: dictionaries.append(commandline_to_dictionary(c))) return dictionaries crazy-complete-crazy-complete-bc5afec/crazy_complete/errors.py000066400000000000000000000050431520061347500251510ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains Exception classes for crazy-complete.''' class CrazyError(Exception): '''Exception class for handling predictable or expected errors. This exception is raised in situations where the error condition is anticipated and should be handled gracefully by the program. It is meant to signal issues that are not caused by bugs in the code but by user input. ''' class CrazyTypeError(CrazyError): '''Exception raised for invalid parameter types. Args: name (str): The name of the parameter with the invalid type. expected (str): A description of the expected types for the parameter. value (any): The actual value passed that has the wrong type. ''' def __init__(self, name, expected, value): self.name = name self.expected = expected self.value = value super().__init__(self.__str__()) def __str__(self): s0 = 'Parameter `%s` has an invalid type.' % self.name s1 = 'Expected types: %s. Received: %r (%s)' % ( self.expected, self.value, type(self.value).__name__) return f'{s0} {s1}' class CrazySchemaValidationError(CrazyError): '''Exception raised for errors in the configuration structure. This exception is specifically designed to handle errors encountered when validating YAML or JSON configuration files against a defined schema. It provides detailed error messages, including the line and column numbers where the error occurred, to help locate the issue in the configuration file. Args: message (str): The error message. value_with_trace (ValueWithTrace): The value that caused the error. ''' def __init__(self, message, value_with_trace): self.message = message self.value_with_trace = value_with_trace super().__init__(self.__str__()) def __str__(self): pos_string = self.value_with_trace.get_position_string() if not pos_string: return self.message return f'{pos_string}: {self.message}' class InternalError(Exception): '''Exception raised for internal errors within the program. This exception indicates that an unexpected condition has occurred that typically results from a bug or flaw in the code logic. It represents errors that are not caused by user actions or external factors, but rather by mistakes or inconsistencies in the program's internal state. ''' crazy-complete-crazy-complete-bc5afec/crazy_complete/extended_yaml_parser.py000066400000000000000000000110521520061347500300300ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''YAML Parser with column and line information.''' import yaml from yaml.events import (StreamStartEvent, DocumentStartEvent, DocumentEndEvent, MappingStartEvent, MappingEndEvent, SequenceStartEvent, SequenceEndEvent, ScalarEvent, AliasEvent) from .value_with_trace import ValueWithTrace from .errors import CrazySchemaValidationError _error = CrazySchemaValidationError class ExtendedYAMLParser: '''Parse YAML with the ability to trace the origin of parsed values.''' def __init__(self): self.src = None self.data = [] self.current_stack = [] self.current_key = None def parse(self, stream): """ Parses the given YAML stream in a SAX-like way and reconstructs the structure. Raises: - yaml.parser.ParserError - errors.CrazySchemaValidationError """ self.src = stream self.data = [] self.current_stack = [] self.current_key = None loader = yaml.Loader(stream) try: while True: event = loader.get_event() if event is None: break self.handle_event(event) finally: loader.dispose() return self.data def handle_event(self, event): """ Handle each YAML parsing event and construct the corresponding data structure. """ if isinstance(event, StreamStartEvent): pass elif isinstance(event, DocumentStartEvent): pass elif isinstance(event, DocumentEndEvent): if self.current_stack: self.data.append(self.current_stack.pop()) elif isinstance(event, MappingStartEvent): new_map = ValueWithTrace.from_yaml_event({}, self.src, event) self.add_to_current_structure(self.current_key, new_map) self.current_stack.append(new_map) self.current_key = None elif isinstance(event, SequenceStartEvent): new_seq = ValueWithTrace.from_yaml_event([], self.src, event) self.add_to_current_structure(self.current_key, new_seq) self.current_stack.append(new_seq) self.current_key = None elif isinstance(event, ScalarEvent): value = self.convert_scalar(event.value, event.implicit) value = ValueWithTrace.from_yaml_event(value, self.src, event) if self.current_stack and isinstance(self.current_stack[-1].value, list): self.add_to_current_structure(None, value) else: if self.current_key is None: self.current_key = value else: self.add_to_current_structure(self.current_key, value) self.current_key = None elif isinstance(event, (MappingEndEvent, SequenceEndEvent)): self.current_stack.pop() elif isinstance(event, AliasEvent): value = ValueWithTrace.from_yaml_event(None, self.src, event) raise _error('Anchors not supported', value) def add_to_current_structure(self, key, value): """ Add a value to the current structure (either dict or list). """ if not self.current_stack: # Root-level element self.current_stack.append(value) return current = self.current_stack[-1] if isinstance(current.value, dict): if key is None: raise _error("Missing key for mapping in YAML", value) current.value[key] = value elif isinstance(current.value, list): current.value.append(value) else: raise _error("Invalid structure in YAML", value) def convert_scalar(self, value, implicit): """ Converts a scalar based on its type hints (implicit tuple). """ if implicit[0]: lower = value.lower() if lower in ("null", "~", ""): return None if lower in ("true", "on", "yes"): return True if lower in ("false", "off", "no"): return False try: return int(value) except ValueError: pass try: return float(value) except ValueError: pass return value crazy-complete-crazy-complete-bc5afec/crazy_complete/file_loader.py000066400000000000000000000031041520061347500260760ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth """Functions for loading a python file.""" import os import sys import importlib import tempfile import __main__ def execute_file(file): '''Import file using compile + exec.''' with open(os.devnull, 'w', encoding='utf-8') as null_fh: sys.stdout = null_fh sys.stderr = null_fh try: with open(file, 'r', encoding='utf-8') as fh: source = fh.read() compiled = compile(source, file, 'exec') try: # pylint: disable=exec-used exec(compiled, globals()) except SystemExit: pass finally: sys.stdout = sys.__stdout__ sys.stderr = sys.__stderr__ return __main__ def import_file(file): '''Import file using importlib.''' directory, filename = os.path.split(file) if filename.lower().endswith('.py'): module_name = filename[:-3] else: temp = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', suffix='.py') with open(file, 'r', encoding='utf-8') as fh: temp.file.write(fh.read()) temp.flush() directory, file = os.path.split(temp.name) module_name = file[:-3] if not directory: directory = '.' if directory not in sys.path: sys.path.append(directory) return importlib.import_module(module_name) crazy-complete-crazy-complete-bc5afec/crazy_complete/fish.py000066400000000000000000000273451520061347500245770ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for generating a Fish auto completion file.''' from . import config as config_ from . import utils from . import algo from . import fish_complete from . import fish_helpers from . import generation from .output import Output from .fish_utils import FishCompleteCommand, VariableManager from .fish_conditions import ( Conditions, Not, HasHiddenOption, HasOption, PositionalNum, PositionalContains ) class FishCompletionDefinition: '''Class holding a completion definition.''' # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments # pylint: disable=too-few-public-methods def __init__( self, ctxt, short_options=None, # List of short options long_options=None, # List of long options old_options=None, # List of old-style options positional=None, # Positional number description=None, # Description requires_argument=False, # Option requires an argument keep_order=False, # Do not sort completion suggestions completion_obj=None): if short_options is None: short_options = [] if long_options is None: long_options = [] if old_options is None: old_options = [] self.ctxt = ctxt self.short_options = short_options self.long_options = long_options self.old_options = old_options self.positional = positional self.description = description self.requires_argument = requires_argument self.keep_order = keep_order self.completion_obj = completion_obj self.conditions = Conditions() def get_complete_cmd(self, ctxt): '''Return a `complete` command for current definition.''' cmd = FishCompleteCommand() cmd.set_command('$prog', raw=True) cmd.add_old_options(self.old_options) cmd.add_long_options(self.long_options) # The Fish manual for complete says that if short option stacking is # not desired, we should add the short options as old options. if not ctxt.config.option_stacking: cmd.add_old_options(self.short_options) else: cmd.add_short_options(self.short_options) if self.description is not None: cmd.set_description(self.description) if self.requires_argument: cmd.flags.add('r') if self.keep_order: cmd.flags.add('k') cmd.parse_args(self.completion_obj.get_args()) if ctxt.config.fish_fast: cmd.set_condition(self.conditions.unsafe_code(ctxt), raw=True) else: cmd.set_condition(self.conditions.query_code(ctxt), raw=True) return cmd def _get_positional_contains(option): commandlines = option.parent.get_parents(include_self=True) del commandlines[0] conditions = [] for commandline in commandlines: cmds = utils.get_all_command_variations(commandline) num = commandline.parent.get_subcommands().get_positional_num() conditions.append(PositionalContains(num, cmds)) return conditions class FishCompletionGenerator: '''Class for generating completions.''' # pylint: disable=too-many-instance-attributes def __init__(self, ctxt, commandline, parent=None): self.ctxt = ctxt self.commandline = commandline self.parent = parent self.children = [] self.completer = fish_complete.FishCompleter() self.subcommands = self.commandline.get_subcommands() self.lines = [] self.conditions = VariableManager('C') self.complete_definitions = [] for option in self.commandline.get_options(): self.complete_definitions.append( self._complete_option(option)) for positional in self.commandline.get_positionals(): self.complete_definitions.append( self._complete_positional(positional)) if self.subcommands: self.complete_definitions.append(self._complete_subcommands()) for sub in self.subcommands.subcommands: self.children.append(FishCompletionGenerator(ctxt, sub, self)) for definition in self.complete_definitions: if isinstance(definition.completion_obj, fish_complete.FishCompleteCommandArg): cmdline = self while cmdline: cmdline._fix_command_arg(definition.positional) cmdline = cmdline.parent for definition in self.complete_definitions: cmd = definition.get_complete_cmd(self.ctxt) if not self.ctxt.config.fish_inline_conditions: if cmd.condition is not None: cmd.set_condition(self.conditions.add(cmd.condition.string), raw=True) self.lines.append(cmd.get()) def _complete(self, context, *args): return self.completer.complete(context, [], *args) def _fix_command_arg(self, positional): for definition in self.complete_definitions: if not (definition.short_options or definition.long_options or definition.old_options): continue definition.conditions.add(Not(PositionalNum('>=', positional))) def visit(self, callback): '''Execute callback for generator objects.''' callback(self) for child in self.children: callback(child) def get_all(self): '''Return all generator objects.''' r = [] self.visit(r.append) return r def _complete_option(self, option): context = self.ctxt.get_option_context(self.commandline, option) if option.complete: completion_obj = self._complete(context, *option.complete) else: completion_obj = self._complete(context, 'none') definition = FishCompletionDefinition( self.ctxt, short_options = option.get_short_option_strings(), long_options = option.get_long_option_strings(), old_options = option.get_old_option_strings(), requires_argument = option.has_required_arg(), keep_order = option.nosort, description = option.help, completion_obj = completion_obj) not_has_options = [] final_options = self.commandline.get_final_option_strings() not_has_options.extend(final_options) conflicting_options = option.get_conflicting_option_strings() not_has_options.extend(conflicting_options) if not option.repeatable: not_has_options.extend(option.option_strings) if not_has_options: not_has_options = algo.uniq(not_has_options) definition.conditions.add(Not(HasOption(not_has_options))) if option.hidden: definition.conditions.add(HasHiddenOption(option.option_strings)) # If we don't inherit options, add a condition to the option that # ensures that we're in the right (sub)command. if not self.commandline.inherit_options and self.subcommands: positional = self.subcommands.get_positional_num() definition.conditions.add(PositionalNum('==', positional)) definition.conditions.extend(_get_positional_contains(option)) definition.conditions.add_when(option.when_parsed) return definition def _complete_positional(self, positional): context = self.ctxt.get_option_context(self.commandline, positional) if positional.complete: completion_obj = self._complete(context, *positional.complete) else: completion_obj = self._complete(context, 'none') definition = FishCompletionDefinition( self.ctxt, positional = positional.get_positional_num(), requires_argument = True, keep_order = positional.nosort, description = positional.help, completion_obj = completion_obj ) definition.conditions.add_when(positional.when_parsed) definition.conditions.extend(_get_positional_contains(positional)) positional_num = positional.get_positional_num() operator = '>=' if positional.repeatable else '==' definition.conditions.add(PositionalNum(operator, positional_num)) return definition def _complete_subcommands(self): positional = self.subcommands items = positional.get_choices() context = self.ctxt.get_option_context(self.commandline, positional) completion_obj = self._complete(context, 'choices', items) definition = FishCompletionDefinition( self.ctxt, description = 'Commands', completion_obj = completion_obj ) positional_num = positional.get_positional_num() definition.conditions.add(PositionalNum('==', positional_num)) definition.conditions.extend(_get_positional_contains(positional)) return definition def get_command_comment(self): '''Return a comment for the current command.''' return '# command %s' % self.commandline.get_command_path() def get_options_for_query(self): '''Return the options for the query command.''' return 'set -l opts "%s"' % ( utils.get_query_option_strings(self.commandline)) def generate_completion(commandline, config=None): '''Code for generating a Fish auto completion file.''' if config is None: config = config_.Config() commandline = generation.enhance_commandline(commandline, config) helpers = fish_helpers.FishHelpers(config, commandline.prog) ctxt = generation.GenerationContext(config, helpers) result = FishCompletionGenerator(ctxt, commandline) if helpers.is_used('query'): types = utils.get_defined_option_types(commandline) if types.short: ctxt.helpers.use_function('query', 'short_options') if types.long: ctxt.helpers.use_function('query', 'long_options') if types.old: ctxt.helpers.use_function('query', 'old_options') output = Output(config, helpers) output.add_generation_notice() output.add_comments() output.add_included_files() output.add_helper_functions_code() with output.add_as_block() as block: block.add("set -l prog '%s'" % commandline.prog) if helpers.is_used('query'): block.add("set -l query '%s'" % helpers.use_function('query')) block.add('') block.add('# Delete existing completions') block.add('complete -c $prog -e') block.add('') block.add('# Generally disable file completion') block.add('complete -c $prog -x') if result.commandline.wraps: with output.add_as_block() as block: block.add('# Wrap command') block.add('complete -c $prog -w %s' % result.commandline.wraps) for generator in result.get_all(): if generator.lines: with output.add_as_block() as block: block.add(generator.get_command_comment()) block.add(generator.get_options_for_query()) block.extend(generator.conditions.get_lines()) block.extend(generator.lines) if commandline.aliases: with output.add_as_block() as block: for alias in commandline.aliases: block.add('complete -c %s -e' % alias) block.add('complete -c %s -w %s' % (alias, commandline.prog)) output.add_vim_modeline('fish') return output.get() crazy-complete-crazy-complete-bc5afec/crazy_complete/fish_complete.py000066400000000000000000000452301520061347500264600ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains code for completing arguments in Fish.''' from . import shell from .pattern import bash_glob_to_regex from .type_utils import is_dict_type from .str_utils import indent, join_with_wrap from .utils import get_query_option_strings, get_defined_option_types CHOICES_INLINE_THRESHOLD = 80 # The `choices` command can in Fish be expressed inline in a complete command # like this: # complete -c program -a 'foo bar baz' # or: # complete -c program -a 'foo\t"Foo value" bar\t"Bar value" baz\t"Baz value"' # # This variable defines how big this string can get before a function # is used instead. class FishCompletionBase: '''Base class for Fish completions.''' def __init__(self, ctxt): self.ctxt = ctxt def get_args(self): '''Return a list of arguments to be appended to the `complete` command in Fish. The returned arguments should be in raw form, without any escaping. Escaping will be handled at a later stage. ''' raise NotImplementedError def get_code(self): '''Return the code that can be used for completing an argument.''' raise NotImplementedError def get_function(self): '''Return a function that runs the code.''' func = self.ctxt.helpers.add_dynamic_func(self.ctxt, self.get_code()) return func class FishCompleteNone(FishCompletionBase): '''Class for completing an argument without a completer.''' def get_args(self): return ['-f'] def get_code(self): return '' def get_function(self): return 'true' class FishCompletionCommand(FishCompletionBase): '''Class for executing a command.''' def __init__(self, ctxt, args): super().__init__(ctxt) self.args = args def get_code(self): return shell.join_quoted(self.args) def get_args(self): command = shell.join_quoted(self.args) return ['-f', '-a', '(%s)' % command] def get_function(self): if len(self.args) == 1: return self.args[0] func = self.ctxt.helpers.add_dynamic_func(self.ctxt, self.get_code()) return func class FishCompletionRawCommand(FishCompletionBase): '''Class for executing a command (without any escaping)''' def __init__(self, ctxt, command): super().__init__(ctxt) self.command = command def get_code(self): return self.command def get_args(self): return ['-f', '-a', '(%s)' % self.command] class FishCompleteChoices(FishCompletionBase): '''Class for completing choices.''' def __init__(self, ctxt, choices): super().__init__(ctxt) self.choices = choices def _get_inline_for_list(self): return ' '.join(shell.quote(str(c)) for c in self.choices) def _get_inline_for_dict(self): def str0(s): return str(s) if s is not None else '' stringified = {str(item): str0(desc) for item, desc in self.choices.items()} q = shell.quote r = ['%s\\t%s' % (q(item), q(desc)) for item, desc in stringified.items()] return ' '.join(r) def _get_code_for_list(self): code = "printf '%s\\n' \\\n" quoted = [shell.quote(str(item)) for item in self.choices] line_length = self.ctxt.config.line_length - 2 code += indent(join_with_wrap(' ', ' \\\n', line_length, quoted), 2) return code.rstrip(' \\\n') def _get_code_for_dict(self): code = "printf '%s\\t%s\\n' \\\n" for item, desc in self.choices.items(): if desc is None: desc = '' code += ' %s %s \\\n' % (shell.quote(str(item)), shell.quote(str(desc))) return code.rstrip(' \\\n') def get_args(self): if is_dict_type(self.choices): arg = self._get_inline_for_dict() else: arg = self._get_inline_for_list() if len(arg) <= CHOICES_INLINE_THRESHOLD: return ['-f', '-a', arg] func = self.get_function() return ['-f', '-a', '(%s)' % func] def get_code(self): if is_dict_type(self.choices): return self._get_code_for_dict() return self._get_code_for_list() def _get_extension_regex(extensions, fuzzy): patterns = [] for extension in extensions: pattern = '' for c in extension: if c.isalpha(): pattern += '[%s%s]' % (c.lower(), c.upper()) elif c in ('.', '+'): pattern += f'\\{c}' else: pattern += c if fuzzy: pattern += '.*' patterns.append(pattern) return '|'.join(f'(.*\\.{pattern})' for pattern in patterns) class FishCompleteFile(FishCompletionBase): '''Class for completing files.''' def __init__(self, ctxt, opts): super().__init__(ctxt) fuzzy = False directory = None extensions = None ignore_globs = None self.args = [] if opts: fuzzy = opts.get('fuzzy', False) directory = opts.get('directory', None) extensions = opts.get('extensions', None) ignore_globs = opts.get('ignore_globs', None) if directory: self.args.extend(['-C', directory]) if extensions: ctxt.helpers.use_function('filedir', 'regex') self.args.extend(['-r', _get_extension_regex(extensions, fuzzy)]) if ignore_globs: ctxt.helpers.use_function('filedir', 'regex_ignore') patterns = map(bash_glob_to_regex, ignore_globs) patterns = [f'({p})' for p in patterns] self.args.extend(['-i', '|'.join(patterns)]) def get_args(self): if len(self.args) == 0: return ['-F'] func = self.ctxt.helpers.use_function('filedir') return FishCompletionCommand(self.ctxt, [func] + self.args).get_args() def get_code(self): func = self.ctxt.helpers.use_function('filedir') return FishCompletionCommand(self.ctxt, [func] + self.args).get_code() def get_function(self): func = self.ctxt.helpers.use_function('filedir') return FishCompletionCommand(self.ctxt, [func] + self.args).get_function() class FishCompleteDirectory(FishCompletionCommand): '''Class for completing directories.''' def __init__(self, ctxt, trace, opts): directory = None if opts is None else opts.get('directory', None) # __fish_complete_directories does not respect __fish_stripprefix # which is used inside list/key_value_list/prefix use_filedir = ( 'list' in trace or 'key_value_list' in trace or 'prefix' in trace ) if directory is not None: func = ctxt.helpers.use_function('filedir') super().__init__(ctxt, [func, '-D', '-C', directory]) elif use_filedir: func = ctxt.helpers.use_function('filedir') super().__init__(ctxt, [func, '-D']) else: super().__init__(ctxt, ['__fish_complete_directories']) class FishCompleteValueList(FishCompletionCommand): '''Class for completing a list of values.''' def __init__(self, ctxt, opts): separator = opts.get('separator', ',') duplicates = opts.get('duplicates', False) values = opts['values'] if is_dict_type(values): code = "printf '%s\\t%s\\n' \\\n" for item, desc in values.items(): code += ' %s %s \\\n' % (shell.quote(item), shell.quote(desc)) code = code.rstrip(' \\\n') else: code = "printf '%s\\n' \\\n" for value in values: code += ' %s \\\n' % shell.quote(value) code = code.rstrip(' \\\n') func = ctxt.helpers.add_dynamic_func(ctxt, code) if duplicates: super().__init__(ctxt, ['__fish_complete_list', separator, func]) else: complete_list_func = ctxt.helpers.use_function('list') super().__init__(ctxt, [complete_list_func, separator, func]) class FishCompletKeyValueList(FishCompletionCommand): '''Used for completing a list of key-value pairs.''' # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments def __init__(self, ctxt, trace, completer, pair_separator, value_separator, values): trace.append('key_value_list') args = [] q = shell.quote for key, desc, complete in values: if not complete: func = 'false' elif complete[0] == 'none': func = 'true' else: obj = completer.complete_from_def(ctxt, trace, complete) func = obj.get_function() args.append('%s %s %s' % (q(key), q(desc or ''), q(func))) code = '%s %s %s \\\n%s' % ( ctxt.helpers.use_function('key_value_list'), shell.quote(pair_separator), shell.quote(value_separator), indent(' \\\n'.join(args), 2) ) func = ctxt.helpers.add_dynamic_func(ctxt, code) super().__init__(ctxt, [func]) class FishCompleteCommand(FishCompletionBase): '''Complete a command from $PATH.''' def __init__(self, ctxt, opts): super().__init__(ctxt) code = None path = None append = None prepend = None if opts: path = opts.get('path', None) append = opts.get('path_append', None) prepend = opts.get('path_prepend', None) def mkpath(path): return shell.join_quoted(path.split(':')) if path: code = 'set -lx PATH %s' % mkpath(path) elif append and prepend: code = 'set -lx PATH %s $PATH %s' % (mkpath(prepend), mkpath(append)) elif append: code = 'set -lx -a PATH %s' % mkpath(append) elif prepend: code = 'set -lx PATH %s $PATH' % mkpath(prepend) if not code: self.code = "__fish_complete_command" else: self.code = f'{code}\n__fish_complete_command' def get_args(self): if '\n' in self.code: func = self.ctxt.helpers.add_dynamic_func(self.ctxt, self.code) return ['-f', '-a', '(%s)' % func] return ['-f', '-a', '(%s)' % self.code] def get_code(self): return self.code class FishCompleteCombine(FishCompletionBase): '''Used for combining multiple complete commands.''' def __init__(self, ctxt, trace, completer, commands): super().__init__(ctxt) self.code = [] trace.append('combine') for command in commands: obj = completer.complete_from_def(ctxt, trace, command) self.code.append(obj.get_code()) def get_code(self): return '\n'.join(self.code) def get_args(self): code_is_singleline = not any('\n' in code for code in self.code) if code_is_singleline: return ['-f', '-a', '(%s)' % '; '.join(self.code)] code = '\n'.join(self.code) func = self.ctxt.helpers.add_dynamic_func(self.ctxt, code) return ['-f', '-a', '(%s)' % func] class FishCompleteCommandArg(FishCompletionBase): '''Complete an argument of a command''' def __init__(self, ctxt): super().__init__(ctxt) query = ctxt.helpers.use_function('query', 'positionals_positions') types = get_defined_option_types(ctxt.option.parent.get_root_commandline()) if types.short: ctxt.helpers.use_function('query', 'short_options') if types.long: ctxt.helpers.use_function('query', 'long_options') if types.old: ctxt.helpers.use_function('query', 'old_options') opts = get_query_option_strings(ctxt.option.parent, with_parent_options=True) opts = shell.quote(opts) command_pos = ctxt.option.get_positional_num() - 1 r = 'set -l opts %s\n' % shell.quote(opts) r += 'set -l pos (%s "$opts" positional_pos %d)\n' % (query, command_pos) r += 'set -l cmdline (commandline -poc | string escape) (commandline -ct)\n' r += 'complete -C -- "$cmdline[$pos..]"' self.code = r def get_code(self): return self.code def get_args(self): func = self.ctxt.helpers.add_dynamic_func(self.ctxt, self.code) return ['-f', '-a', '(%s)' % func] def get_function(self): func = self.ctxt.helpers.add_dynamic_func(self.ctxt, self.code) return func class FishCompleter(shell.ShellCompleter): '''Code generator for completing arguments in Fish.''' # pylint: disable=missing-function-docstring # pylint: disable=too-many-public-methods # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments def none(self, ctxt, _trace, *_): return FishCompleteNone(ctxt) def integer(self, ctxt, _trace, options=None): suffixes = [] if options: if 'suffixes' in options: for suffix, description in options['suffixes'].items(): suffixes.append(f'{suffix}:{description}') if not suffixes: return FishCompleteNone(ctxt) func = ctxt.helpers.use_function('number') return FishCompletionCommand(ctxt, [func, *suffixes]) def float(self, ctxt, trace, options=None): return self.integer(ctxt, trace, options) def choices(self, ctxt, _trace, choices): return FishCompleteChoices(ctxt, choices) def command(self, ctxt, _trace, opts=None): return FishCompleteCommand(ctxt, opts) def directory(self, ctxt, trace, opts=None): return FishCompleteDirectory(ctxt, trace, opts) def file(self, ctxt, _trace, opts=None): return FishCompleteFile(ctxt, opts) def mime_file(self, ctxt, _trace, pattern): func = ctxt.helpers.use_function('mime_file') return FishCompletionCommand(ctxt, [func, pattern]) def group(self, ctxt, _trace): return FishCompletionCommand(ctxt, ["__fish_complete_groups"]) def hostname(self, ctxt, _trace): return FishCompletionCommand(ctxt, ["__fish_print_hostnames"]) def pid(self, ctxt, _trace): return FishCompletionCommand(ctxt, ["__fish_complete_pids"]) def process(self, ctxt, _trace): return FishCompletionCommand(ctxt, ["__fish_complete_proc"]) def range(self, ctxt, _trace, start, stop, step=1): if step == 1: return FishCompletionCommand(ctxt, ['seq', str(start), str(stop)]) return FishCompletionCommand(ctxt, ['seq', str(start), str(step), str(stop)]) def service(self, ctxt, _trace): return FishCompletionCommand(ctxt, ["__fish_systemctl_services"]) def user(self, ctxt, _trace): return FishCompletionCommand(ctxt, ["__fish_complete_users"]) def variable(self, ctxt, _trace): return FishCompletionCommand(ctxt, ["set", "-n"]) def environment(self, ctxt, _trace): return FishCompletionCommand(ctxt, ["set", "-n", "-x"]) def exec(self, ctxt, _trace, command): return FishCompletionRawCommand(ctxt, command) def exec_fast(self, ctxt, _trace, command): return FishCompletionRawCommand(ctxt, command) def exec_internal(self, ctxt, _trace, command): return FishCompletionRawCommand(ctxt, command) def value_list(self, ctxt, _trace, opts): return FishCompleteValueList(ctxt, opts) def key_value_list(self, ctxt, trace, pair_separator, value_separator, values): return FishCompletKeyValueList(ctxt, trace, self, pair_separator, value_separator, values) def combine(self, ctxt, trace, commands): return FishCompleteCombine(ctxt, trace, self, commands) def list(self, ctxt, trace, command, opts=None): separator = opts.get('separator', ',') if opts else ',' duplicates = opts.get('duplicates', False) if opts else False trace.append('list') obj = self.complete_from_def(ctxt, trace, command) func = obj.get_function() list_func = ctxt.helpers.use_function('list') if not duplicates: return FishCompletionCommand(ctxt, [list_func, separator, func]) return FishCompletionCommand(ctxt, [list_func, '-d', separator, func]) def history(self, ctxt, _trace, pattern): func = ctxt.helpers.use_function('history') return FishCompletionCommand(ctxt, [func, pattern]) def commandline_string(self, ctxt, _trace): func = ctxt.helpers.use_function('commandline_string') return FishCompletionCommand(ctxt, [func]) def command_arg(self, ctxt, _trace): return FishCompleteCommandArg(ctxt) def date(self, ctxt, _trace, _format): return FishCompleteNone(ctxt) def date_format(self, ctxt, _trace): func = ctxt.helpers.use_function('date_format') return FishCompletionCommand(ctxt, [func]) def uid(self, ctxt, _trace): return FishCompletionCommand(ctxt, ['__fish_complete_user_ids']) def gid(self, ctxt, _trace): return FishCompletionCommand(ctxt, ['__fish_complete_group_ids']) def filesystem_type(self, ctxt, _trace): return FishCompletionCommand(ctxt, ['__fish_print_filesystems']) def prefix(self, ctxt, trace, prefix, command): obj = self.complete_from_def(ctxt, trace, command) func = obj.get_function() prefix_func = ctxt.helpers.use_function('prefix') return FishCompletionCommand(ctxt, [prefix_func, prefix, func]) def ip_address(self, ctxt, _trace, type_='all'): func = '__fish_print_addresses' if type_ == 'ipv6': cmd = f"{func} | string match -e ':'" return FishCompletionRawCommand(ctxt, cmd) if type_ == 'ipv4': cmd = f"{func} | string match -e '.'" return FishCompletionRawCommand(ctxt, cmd) return FishCompletionCommand(ctxt, [func]) # ========================================================================= # Bonus # ========================================================================= def net_interface(self, ctxt, _trace): return FishCompletionCommand(ctxt, ['__fish_print_interfaces']) def mountpoint(self, ctxt, _trace): return FishCompletionCommand(ctxt, ['__fish_print_mounted']) def timezone(self, ctxt, _trace): func = ctxt.helpers.use_function('timezone_list') return FishCompletionCommand(ctxt, [func]) def alsa_card(self, ctxt, _trace): func = ctxt.helpers.use_function('alsa_list_cards') return FishCompletionCommand(ctxt, [func]) def alsa_device(self, ctxt, _trace): func = ctxt.helpers.use_function('alsa_list_devices') return FishCompletionCommand(ctxt, [func]) crazy-complete-crazy-complete-bc5afec/crazy_complete/fish_conditions.py000066400000000000000000000207701520061347500270230ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Conditions for Fish.''' import re from . import cli from . import when from . import shell from .errors import InternalError from .type_utils import is_list_type def escape_in_double(string): '''Escape special characters in a double quoted string.''' return re.sub(r'(["$`\\])', r'\\\1', string) def make_condition_command(prefix, args): '''Make a condition command. In our completion scripts, we use the variables $query and $opts. We generate code like this: complete -n "$query '$opts' has_option --foo" This function generates such a condition command. We have to ensure proper escaping inside the double quotes. ''' args_quoted = [shell.quote(arg) for arg in args] args_quoted = [escape_in_double(arg) for arg in args_quoted] return '%s %s' % (prefix, ' '.join(args_quoted)) def make_query(args): '''Make a query command.''' return make_condition_command("$query '$opts'", args) class Condition: '''Base class for conditions.''' def query_code(self, ctxt): '''Returns code using the `query` function.''' raise NotImplementedError def unsafe_code(self, ctxt): '''Returns code using Fish's internal functions.''' raise NotImplementedError class Not(Condition): '''Logical not.''' def __init__(self, conditional): self.conditional = conditional def query_code(self, ctxt): return 'not %s' % self.conditional.query_code(ctxt) def unsafe_code(self, ctxt): return 'not %s' % self.conditional.unsafe_code(ctxt) class HasOption(Condition): '''Checks if an option is present on command line.''' def __init__(self, option_strings): if not is_list_type(option_strings): raise InternalError('option_strings: Invalid type') self.option_strings = option_strings def query_code(self, ctxt): ctxt.helpers.use_function('query', 'has_option') return make_query(['has_option'] + self.option_strings) def unsafe_code(self, _ctxt): args = [] for opt in self.option_strings: if cli.is_long_option_string(opt): args += ['-l', opt.lstrip('-')] elif cli.is_short_option_string(opt): args += ['-s', opt[1]] else: args += ['-o', opt.lstrip('-')] return make_condition_command('__fish_seen_argument', args) class HasHiddenOption(Condition): '''Checks if an incomplete option is present on command line.''' def __init__(self, option_strings): if not is_list_type(option_strings): raise InternalError('option_strings: Invalid type') self.option_strings = option_strings def query_code(self, ctxt): ctxt.helpers.use_function('query', 'has_option') ctxt.helpers.use_function('query', 'with_incomplete') options = self.option_strings return make_query(['has_option', 'WITH_INCOMPLETE'] + options) def unsafe_code(self, ctxt): return self.query_code(ctxt) class OptionIs(Condition): '''Checks if an option has a specific value.''' def __init__(self, option_strings, values): if not is_list_type(option_strings): raise InternalError('option_strings: Invalid type') if not is_list_type(values): raise InternalError('values: Invalid type') self.option_strings = option_strings self.values = values def query_code(self, ctxt): ctxt.helpers.use_function('query', 'option_is') options = self.option_strings values = self.values return make_query(['option_is', *options, '--', *values]) def unsafe_code(self, ctxt): return self.query_code(ctxt) class PositionalNum(Condition): '''Check the number of positionals.''' TEST_OPERATORS = { '==': '-eq', '!=': '-ne', '<=': '-le', '>=': '-ge', '<': '-lt', '>': '-gt', } def __init__(self, operator, number): if operator not in ('==', '!=', '<', '<=', '>', '>='): raise InternalError(f'operator: Invalid value: {operator}') if not isinstance(number, int): raise InternalError(f'number: Invalid type: {number}') self.operator = operator self.number = number def query_code(self, ctxt): ctxt.helpers.use_function('query', 'num_of_positionals') op = self.TEST_OPERATORS[self.operator] return make_query(['num_of_positionals', op, str(self.number - 1)]) def unsafe_code(self, _ctxt): op = self.TEST_OPERATORS[self.operator] number = self.number r = "test (__fish_number_of_cmd_args_wo_opts) %s %d" % (op, number) return r class PositionalContains(Condition): '''Checks if a positional contains a specific value.''' def __init__(self, number, values): if not isinstance(number, int): raise InternalError(f'number: Invalid type: {number}') if not is_list_type(values): raise InternalError(f'values: Invalid type: {values}') self.number = number self.values = values def query_code(self, ctxt): ctxt.helpers.use_function('query', 'positional_contains') values = self.values number = str(self.number) return make_query(['positional_contains', number, *values]) def unsafe_code(self, _ctxt): values = self.values return make_condition_command('__fish_seen_subcommand_from', values) def replace_commands(tokens): '''Replace when objects by own condition objects.''' r = [] for obj in tokens: if isinstance(obj, when.OptionIs): r.append(OptionIs(obj.options, obj.values)) elif isinstance(obj, when.HasOption): r.append(HasOption(obj.options)) elif isinstance(obj, str): r.append(obj) else: raise AssertionError("Not reached") return r class Conditions(Condition): '''Holds a list of conditions.''' def __init__(self): self.conditions = [] self.when = None def _optimized_conditions(self): positional_contains = [] positional_num = [] has_option = [] other = [] for condition in self.conditions: if isinstance(condition, PositionalContains): positional_contains.append(condition) elif isinstance(condition, PositionalNum): positional_num.append(condition) elif isinstance(condition, (HasHiddenOption, HasOption)): has_option.append(condition) else: other.append(condition) # positional_num is fastest, followed by positional_contains r = [] r.extend(positional_num) r.extend(positional_contains) r.extend(has_option) r.extend(other) return r def _get_tokens(self): r = [] for condition in self._optimized_conditions(): r.append(condition) r.append('&&') if self.when: if r and when.needs_braces(self.when): r += ['(', *self.when, ')'] else: r += self.when if r and r[-1] == '&&': r.pop(-1) return r def query_code(self, ctxt): r = [] for obj in self._get_tokens(): if obj == '(': r.append('begin') elif obj == ')': r.append(';end') elif obj in ('!', '&&', '||'): r.append(obj) else: r.append(obj.query_code(ctxt)) if not r: return None return '"%s"' % ' '.join(r) def unsafe_code(self, ctxt): r = [] for obj in self._get_tokens(): if obj == '(': r.append('begin') elif obj == ')': r.append(';end') elif obj in ('!', '&&', '||'): r.append(obj) else: r.append(obj.unsafe_code(ctxt)) if not r: return None return '"%s"' % ' '.join(r) def add(self, condition): '''Add a condition.''' self.conditions.append(condition) def extend(self, conditions): '''Add many conditions.''' self.conditions.extend(conditions) def add_when(self, tokens): '''Add when objects.''' if not tokens: return self.when = replace_commands(tokens) crazy-complete-crazy-complete-bc5afec/crazy_complete/fish_helpers.py000066400000000000000000000560431520061347500263160ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains helper functions for Fish.''' from .helpers import GeneralHelpers, FishFunction _QUERY = FishFunction('query', r''' # =========================================================================== # # This function implements the parsing of options and positionals in the # Fish shell. # # Usage: query [ARGS...] # # The first argument is a comma-separated list of options that the parser # should know about. # # Short options (-o), long options (--option), and old-style options (-option) # are supported. # # If an option takes an argument, it is suffixed by '='. # If an option takes an optional argument, it is suffixed by '=?'. # # For example: # query '-f,--flag,-old-style,--with-arg=,--with-optional=?' [...] # # Here, -f, --flag and -old-style don't take options, --with-arg requires an # argument and --with-optional takes an optional argument. # # COMMANDS # positional_contains # Checks if the positional argument number NUM is one of WORDS. # NUM counts from one. # # has_option [WITH_INCOMPLETE] # Checks if an option given in OPTIONS is passed on commandline. # If an option requires an argument, this command returns true only if the # option includes an argument. If 'WITH_INCOMPLETE' is specified, it also # returns true for options missing their arguments. # # option_is -- # Checks if any option in OPTIONS has a value of VALUES. # # num_of_positionals [ ] # If no arguments are provided, print the count of positional arguments. # If two arguments are provided, the first argument should be one of # the comparison operators: '-lt', '-le', '-eq', '-ne', '-gt', '-ge'. # Returns 0 if the count of positional arguments matches the # specified NUMBER according to the comparison operator, otherwise # returns 1. # # =========================================================================== set -l positionals #ifdef positionals_positions set -l positionals_positions #endif set -l having_options set -l option_values #ifdef DEBUG switch (count $argv) case 0 echo '%FUNCNAME%: missing OPTIONS argument' >&2 return 1 case 1 echo '%FUNCNAME%: missing COMMAND' >&2 return 1 end #endif set -l options $argv[1] set -e argv[1] set -l cmd $argv[1] set -e argv[1] set -l my_cache_key "$(commandline -b) $options" if test "$__QUERY_CACHE_KEY" = "$my_cache_key" set positionals $__QUERY_CACHE_POSITIONALS #ifdef positionals_positions set positionals_positions $__QUERY_CACHE_POSITIONALS_POSITIONS #endif set having_options $__QUERY_CACHE_HAVING_OPTIONS set option_values $__QUERY_CACHE_OPTION_VALUES else # ========================================================================= # Parsing of OPTIONS argument # ========================================================================= #ifdef short_options set -l short_opts_with_arg set -l short_opts_without_arg set -l short_opts_with_optional_arg #endif #ifdef long_options set -l long_opts_with_arg set -l long_opts_without_arg set -l long_opts_with_optional_arg #endif #ifdef old_options set -l old_opts_with_arg set -l old_opts_without_arg set -l old_opts_with_optional_arg #endif set -l option if test -n "$options" for option in (string split -- ' ' $options) if false true #ifdef long_options else if string match -qr -- '^--.+=$' $option set -a long_opts_with_arg (string replace -- '=' '' $option) else if string match -qr -- '^--.+=\?$' $option set -a long_opts_with_optional_arg (string replace -- '=?' '' $option) else if string match -qr -- '^--.+$' $option set -a long_opts_without_arg $option #endif #ifdef short_options else if string match -qr -- '^-.=$' $option set -a short_opts_with_arg (string replace -- '=' '' $option) else if string match -qr -- '^-.=\?$' $option set -a short_opts_with_optional_arg (string replace -- '=?' '' $option) else if string match -qr -- '^-.$' $option set -a short_opts_without_arg $option #endif #ifdef old_options else if string match -qr -- '^-..+=$' $option set -a old_opts_with_arg (string replace -- '=' '' $option) else if string match -qr -- '^-..+=\?$' $option set -a old_opts_with_optional_arg (string replace -- '=?' '' $option) else if string match -qr -- '^-..+$' $option set -a old_opts_without_arg $option #endif end end end # ========================================================================= # Parsing of options and positionals # ========================================================================= set -l cmdline (commandline -poc) set -l cmdline_count (count $cmdline) set -l argi 2 # cmdline[1] is command name while test $argi -le $cmdline_count set -l arg "$cmdline[$argi]" set -l have_trailing_arg (test $argi -lt $cmdline_count && echo true || echo false) switch $arg case '-' set -a positionals - #ifdef positionals_positions set -s positionals_positions $argi #endif case '--' set -a positionals $cmdline[$(math $argi + 1)..] #ifdef positionals_positions set -a positionals_positions (seq (math $argi + 1) $cmdline_count) #endif break case '--*=*' set -l split (string split -m 1 -- '=' $arg) set -a having_options $split[1] set -a option_values "$split[2]" case '--*' #ifdef long_options if contains -- $arg $long_opts_with_arg if $have_trailing_arg set -a having_options $arg set -a option_values $cmdline[(math $argi + 1)] set argi (math $argi + 1) end else set -a having_options $arg set -a option_values '' end #endif case '-*' set -l end_of_parsing false #ifdef old_options if string match -q -- '*=*' $arg set -l split (string split -m 1 -- '=' $arg) if contains -- $split[1] $old_opts_with_arg $old_opts_with_optional_arg set -a having_options $split[1] set -a option_values "$split[2]" set end_of_parsing true end else if contains -- $arg $old_opts_with_arg set end_of_parsing true if $have_trailing_arg set -a having_options $arg set -a option_values $cmdline[(math $argi + 1)] set argi (math $argi + 1) end else if contains -- $arg $old_opts_without_arg $old_opts_with_optional_arg set -a having_options $arg set -a option_values '' set end_of_parsing true end #endif #ifdef short_options set -l arg_length (string length -- $arg) set -l i 2 while not $end_of_parsing; and test $i -le $arg_length set -l option "-$(string sub -s $i -l 1 -- $arg)" set -l trailing_chars "$(string sub -s (math $i + 1) -- $arg)" if contains -- $option $short_opts_without_arg set -a having_options $option set -a option_values '' else if contains -- $option $short_opts_with_arg set end_of_parsing true if test -n "$trailing_chars" set -a having_options $option set -a option_values $trailing_chars else if $have_trailing_arg set -a having_options $option set -a option_values $cmdline[(math $argi + 1)] set argi (math $argi + 1) end else if contains -- $option $short_opts_with_optional_arg set end_of_parsing true set -a having_options $option set -a option_values "$trailing_chars" # may be empty end set i (math $i + 1) end #endif case '*' set -a positionals $arg #ifdef positionals_positions set -a positionals_positions $argi #endif end set argi (math $argi + 1) end set -g __QUERY_CACHE_POSITIONALS $positionals #ifdef positionals_positions set -g __QUERY_CACHE_POSITIONALS_POSITIONS $positionals_positions #endif set -g __QUERY_CACHE_HAVING_OPTIONS $having_options set -g __QUERY_CACHE_OPTION_VALUES $option_values set -g __QUERY_CACHE_KEY $my_cache_key end # =========================================================================== # Commands # =========================================================================== switch $cmd #ifdef positional_contains case 'positional_contains' #ifdef DEBUG if test (count $argv) -eq 0 echo '%FUNCNAME%: positional_contains: argv[3]: missing number' >&2 return 1 end #endif set -l positional_num $argv[1] set -e argv[1] contains -- $positionals[$positional_num] $argv && return 0 || return 1 #endif #ifdef has_option case 'has_option' #ifdef with_incomplete set -l with_incomplete false if test $argv[1] = 'WITH_INCOMPLETE' set with_incomplete true set -e argv[1] end #endif for option in $having_options contains -- $option $argv && return 0 end #ifdef with_incomplete if $with_incomplete set -l tokens (commandline -po) set -e tokens[1] for option in $argv if test 2 -eq (string length -- $option) set option (string sub -l 1 -s 2 -- $option) string match -rq -- "^-[A-z0-9]*$option\$" $tokens[-2] && return 0 string match -rq -- "^-[A-z0-9]*$option.*\$" $tokens[-1] && return 0 else string match -q -r -- "^$option\$" $tokens[-2] && return 0 string match -q -r -- "^$option(=.*)?\$" $tokens[-1] && return 0 contains -- $tokens[-1] $argv && return 0 end end end #endif return 1 #endif #ifdef num_of_positionals case 'num_of_positionals' switch (count $argv) case 0 count $positionals #ifdef DEBUG case 1 echo '%FUNCNAME%: num_of_positionals: $argv[1]: missing operand' >&2 return 1 #endif case 2 if contains -- $argv[1] -lt -le -eq -ne -gt -ge; test (count $positionals) $argv[1] $argv[2] && return 0 || return 1 #ifdef DEBUG else echo '%FUNCNAME%: num_of_positionals: $argv[1]: unknown operator' >&2 return 1 #endif end #ifdef DEBUG case '*' echo '%FUNCNAME%: num_of_positionals: too many arguments' >&2 return 1 #endif end #endif #ifdef option_is case 'option_is' set -l eof_string (contains -i -- -- $argv || math (count $argv) + 1) set -l options $argv[1..$(math $eof_string - 1)] set -l values $argv[$eof_string..] #ifdef DEBUG if test (count $options) -eq 0 echo '%FUNCNAME%: missing options' >&2 return 1 end if test (count $values) -eq 0 echo '%FUNCNAME%: missing values' >&2 return 1 end #endif set -l i (count $having_options) while test $i -ge 1 if contains -- $having_options[$i] $options if contains -- $option_values[$i] $values return 0 end end set i (math $i - 1) end return 1 #endif #ifdef positionals_positions case 'positional_pos' echo $positionals_positions[$argv[1]] #endif #ifdef DEBUG case '*' echo '%FUNCNAME%: argv[2]: invalid command' >&2 return 1 #endif end ''') _GET_COMPLETING_ARG = FishFunction('get_completing_arg', r''' set -l arg (commandline -ct | string unescape) switch $arg case '--*=' '--*=*' set arg (string replace -r -- '^-[^=]*=' '' $arg) case '-*' set -l prog (commandline -po)[1] set -l progdef (complete -c $prog) set -l full_opt (string match -r -- '^-[a-zA-Z0-9]+=' $arg) set -l opt (string sub -s 2 -e -1 -- $full_opt) set -l optdefs (string match -re -- " -(o|-old-option) $opt( |\$)" $progdef) set -l optdefs (string match -re -- " -(x|r|a|-(exclusive|require-parameter|arguments))( |\$)" $optdefs) if test (count $optdefs) -gt 0 set arg (string replace -m 1 -- $full_opt '' $arg) else set -l i 2 while test $i -lt (string length -- $arg) set -l opt (string sub -s $i -l 1 -- $arg) set -l optdefs (string match -re -- " -(s|-short-option) $opt( |\$)" $progdef) set -l optdefs (string match -re -- " -(x|r|a|-(exclusive|require-parameter|arguments))( |\$)" $optdefs) if test (count $optdefs) -gt 0 set arg (string sub -s (math $i + 1) -- "$arg") break end set i (math $i + 1) end end end if test -n "$__fish_stripprefix" string replace -r -- $__fish_stripprefix '' "$arg" else printf '%s\n' "$arg" end ''') _FILEDIR = FishFunction('filedir', r''' # Function for completing files or directories # # Options: # -d|--description=DESC The description for completed entries # -D|--directories Only complete directories # -C|--cd=DIR List contents in DIR #ifdef regex # -r|--regex=PATTERN Only list files matching pattern #endif #ifdef regex_ignore # -i|--ignore=PATTERN Ignore files matching pattern #endif argparse --max-args 0 'd/description=' 'D/directories' 'C/cd=' \ #ifdef regex 'r/regex=' \ #endif #ifdef regex_ignore 'i/ignore=' \ #endif -- $argv || return 1 set -l comp (get_completing_arg) set -l desc set -l files if set -q _flag_cd[1] pushd $_flag_cd 2>/dev/null || return 1 set files (complete -C"'' $comp") popd else set files (complete -C"'' $comp") end if set -q _flag_description[1] set desc $_flag_description else if set -g _flag_directories set desc 'Directory' end if set -q files[1] if set -q _flag_directories[1] set files (printf '%s\n' $files | string match -r '.*/$') end #ifdef regex if set -q _flag_regex[1] set files (printf '%s\n' $files | string match -rg "(.*/\$)|($_flag_regex[1])\$") end #endif #ifdef regex_ignore if set -q _flag_ignore[1] set files (printf '%s\n' $files | string match -rv "(^|.*/)($_flag_ignore[1])\$") end #endif printf '%s\n' $files\t"$desc" end ''', ['get_completing_arg']) _LIST = FishFunction('list', r''' set -l duplicates false if test $argv[1] = '-d' set duplicates true set -e argv[1] end set -l separator $argv[1] set -l func $argv[2] set -l comp (get_completing_arg) if test -z "$comp" eval $func return end set -l i set -l value set -l values set -l descriptions set -g __fish_stripprefix "^.*"(string escape --style=regex -- $separator) for value in (eval $func) set -l split (string split -- \t $value) set -a values $split[1] set -a descriptions "$split[2]" end set -e __fish_stripprefix set -l having_values (string split -- $separator $comp) set -l remaining_values_idxs if $duplicates set remaining_values_idxs (seq 1 (count $values)) else set i 1 for value in $values if not contains -- $value $having_values set -a remaining_values_idxs $i end set i (math $i + 1) end end switch $comp case "*$separator" for i in $remaining_values_idxs printf '%s%s\t%s\n' $comp $values[$i] "$descriptions[$i]" end case "*$separator*" set comp (string split -r -m 1 -- $separator $comp)[1] for i in $remaining_values_idxs printf '%s%s%s\t%s\n' "$comp" $separator $values[$i] "$descriptions[$i]" end case '*' for i in $remaining_values_idxs printf '%s\t%s\n' $values[$i] "$descriptions[$i]" end end ''', ['get_completing_arg']) _PREFIX = FishFunction('prefix', r''' set -l comp (get_completing_arg) set -l prefix_escaped (string escape --style=regex -- $argv[1]) if string match -qr -- $prefix_escaped $comp set -g __fish_stripprefix "^"$prefix_escaped printf "%s\n" $argv[1](eval $argv[2]) set -e __fish_stripprefix else printf "%s\n" $argv[1] end ''', ['get_completing_arg']) _KEY_VALUE_LIST = FishFunction('key_value_list', r''' set -l sep1 $argv[1] set -l sep2 $argv[2] set -l value set -l keys set -l descriptions set -l functions set -l i for i in (seq 3 3 (count $argv)) set -a keys $argv[$i] set -a descriptions $argv[(math $i + 1)] set -a functions $argv[(math $i + 2)] end set -l comp (get_completing_arg) if test -z "$comp" || test (string sub -s -1 -l 1 -- $comp) = $sep1 for i in (seq 1 (count $keys)) if test "$functions[$i]" = false printf '%s%s\t%s\n' "$comp" $keys[$i] "$descriptions[$i]" else printf '%s%s%s\t%s\n' "$comp" $keys[$i] $sep2 "$descriptions[$i]" end end return end function %PREFIX%__call_func_for_key -S set -l i for i in (seq 1 (count $keys)) if test $keys[$i] = $argv[1] set -g __fish_stripprefix "^.*"(string escape --style=regex -- $sep2) $functions[$i] set -e __fish_stripprefix return end end end set -l pair (string split -- $sep1 $comp)[-1] set -l split (string split -- $sep2 $pair) switch $pair case "*$sep2*" set -l value_len (string length -- $split[2]) if test $value_len -gt 0 set comp (string sub -e -$value_len -- $comp) end for value in (%PREFIX%__call_func_for_key $split[1]) printf '%s%s\n' $comp $value end case '*' set -l key_len (string length -- $split[1]) set comp (string sub -e -$key_len -- $comp) for i in (seq 1 (count $keys)) if test "$functions[$i]" = false printf '%s%s\t%s\n' "$comp" $keys[$i] "$descriptions[$i]" else printf '%s%s%s\t%s\n' "$comp" $keys[$i] $sep2 "$descriptions[$i]" end end end ''', ['get_completing_arg']) _HISTORY = FishFunction('history', r''' builtin history | command grep -E -o -- $argv[1] ''') _MIME_FILE = FishFunction('mime_file', r''' set -l i_opt set -l comp (get_completing_arg) if command file -i /dev/null &>/dev/null set i_opt '-i' else if command file -I /dev/null &>/dev/null set i_opt '-I' else complete -C"'' $comp" return end set -l line command file -L $i_opt -- "$comp"* 2>/dev/null | while read line set -l split (string split -m 1 -r ':' "$line") if string match -q -- '*inode/directory*' $split[2] printf '%s/\n' "$split[1]" else if begin; echo "$split[2]" | command grep -q -E -- $argv[1]; end printf '%s\n' "$split[1]" end end ''', ['get_completing_arg']) _SUBSTRACT_PREFIX_SUFFIX = FishFunction('subtract_prefix_suffix', r''' set -l s1 $argv[1] set -l s2 $argv[2] set -l len1 (string length -- $s1) set -l len2 (string length -- $s2) set -l maxk $len1 if test $len2 -lt $maxk set maxk $len2 end set -l k $maxk while test $k -gt 0 set -l start (math $len1 - $k + 1) set -l suf (string sub -s $start -l $k -- $s1) set -l pre (string sub -s 1 -l $k -- $s2) if test "$suf" = "$pre" if test $len1 -eq $k echo '' else string sub -l (math $len1 - $k) -- $s1 end return end set k (math $k - 1) end echo $s1 ''') _COMMANDLINE_STRING = FishFunction('commandline_string', r''' set -l line set -l comp (get_completing_arg) complete -C "$comp" | while read line set -l split (string split -m 1 -- \t $line) set -l comp2 (subtract_prefix_suffix "$comp" "$split[1]") printf '%s\t%s\n' "$comp2$split[1]" "$split[2]" end''', ['subtract_prefix_suffix', 'get_completing_arg']) _DATE_FORMAT = FishFunction('date_format', r''' set -l comp (get_completing_arg) if test "$(string sub -s -1 -l 1 -- $comp)" = '%' printf '%s%s\t%s\n' \ "$comp" 'a' 'abbreviated day name' \ "$comp" 'A' 'full day name' \ "$comp" 'b' 'abbreviated month name' \ "$comp" 'h' 'abbreviated month name' \ "$comp" 'B' 'full month name' \ "$comp" 'c' 'preferred locale date and time' \ "$comp" 'C' '2-digit century' \ "$comp" 'd' 'day of month (01-31)' \ "$comp" 'D' 'American format month/day/year (%m/%d/%y)' \ "$comp" 'e' 'day of month ( 1-31)' \ "$comp" 'F' 'ISO 8601 year-month-date (%Y-%m-%d)' \ "$comp" 'G' '4-digit ISO 8601 week-based year' \ "$comp" 'g' '2-digit ISO 8601 week-based year' \ "$comp" 'H' 'hour (00-23)' \ "$comp" 'I' 'hour (01-12)' \ "$comp" 'j' 'day of year (001-366)' \ "$comp" 'k' 'hour ( 0-23)' \ "$comp" 'l' 'hour ( 1-12)' \ "$comp" 'm' 'month (01-12)' \ "$comp" 'M' 'minute (00-59)' \ "$comp" 'n' 'newline' \ "$comp" 'p' 'locale dependent AM/PM' \ "$comp" 'r' 'locale dependent a.m. or p.m. time (%I:%M:%S %p)' \ "$comp" 'R' '24-hour notation time (%H:%M)' \ "$comp" 's' 'seconds since the epoch' \ "$comp" 'S' 'seconds (00-60)' \ "$comp" 't' 'tab' \ "$comp" 'T' '24-hour notation with seconds (%H:%M:%S)' \ "$comp" 'u' 'day of week (1-7, 1=Monday)' \ "$comp" 'U' 'week number of current year, Sunday based (00-53)' \ "$comp" 'V' 'ISO 8601 week number of current year, week 1 has 4 days in current year (01-53)' \ "$comp" 'w' 'day of week (0-6, 0=Sunday)' \ "$comp" 'W' 'week number of current year, Monday based (00-53)' \ "$comp" 'x' 'locale dependent date representation without time' \ "$comp" 'X' 'locale dependent time representation without date' \ "$comp" 'y' '2-digit year (00-99)' \ "$comp" 'Y' 'full year' \ "$comp" 'z' 'UTC offset' \ "$comp" 'Z' 'timezone name' \ "$comp" '%' 'literal %' end ''', ['get_completing_arg']) _NUMBER = FishFunction('number', r''' set -l comp (get_completing_arg) set -l number (string match -r -- '^[0-9,.]*' $comp) if test -z "$number" return end set -l suffix_and_desc for suffix_and_desc in $argv set suffix_and_desc (string split -m 1 -- ':' $suffix_and_desc) printf '%s%s\t%s\n' $number $suffix_and_desc[1] $suffix_and_desc[2] end ''', ['get_completing_arg']) # ============================================================================= # Bonus # ============================================================================= _TIMEZONE_LIST = FishFunction('timezone_list', r''' if ! command timedatectl list-timezones 2>/dev/null command find /usr/share/zoneinfo -type f |\ command sed 's|/usr/share/zoneinfo/||g' |\ command grep -E -v '^(posix|right)' end ''') _ALSA_LIST_CARDS = FishFunction('alsa_list_cards', r''' set -l card for card in (command aplay -l | string match -r '^card [0-9]+: [^,]+') set card (string replace 'card ' '' $card) set -l split (string split ': ' $card) if set -q split[2] printf "%s\t%s\n" $split[1] $split[2] end end ''') _ALSA_LIST_DEVICES = FishFunction('alsa_list_devices', r''' set -l card for card in (command aplay -l | string match -r '^card [0-9]+: [^,]+') set card (string replace 'card ' '' $card) set -l split (string split ': ' $card) if set -q split[2] printf "hw:%s\t%s\n" $split[1] $split[2] end end ''') class FishHelpers(GeneralHelpers): '''Class holding helper functions for Fish.''' def __init__(self, config, function_prefix): super().__init__(config, function_prefix, FishFunction) self.add_function(_QUERY) self.add_function(_GET_COMPLETING_ARG) self.add_function(_FILEDIR) self.add_function(_LIST) self.add_function(_PREFIX) self.add_function(_KEY_VALUE_LIST) self.add_function(_HISTORY) self.add_function(_DATE_FORMAT) self.add_function(_NUMBER) self.add_function(_MIME_FILE) self.add_function(_SUBSTRACT_PREFIX_SUFFIX) self.add_function(_COMMANDLINE_STRING) self.add_function(_TIMEZONE_LIST) self.add_function(_ALSA_LIST_CARDS) self.add_function(_ALSA_LIST_DEVICES) crazy-complete-crazy-complete-bc5afec/crazy_complete/fish_utils.py000066400000000000000000000157511520061347500260150ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Fish utility functions and classes.''' from .errors import InternalError from . import shell class FishString: '''A utility class for command-line strings that may require escaping. When building command-line commands, it's important to ensure that strings are properly escaped to prevent shell injection vulnerabilities or syntax errors. However, in some cases, the string may already be escaped, and applying escaping again would result in incorrect behavior. The `FishString` class provides an abstraction to manage both escaped and unescaped strings. Attributes: string (str): The string that represents the command or command argument. raw (bool): A flag that indicates whether the string is already escaped. If `True`, the string is assumed to be already escaped and will not be escaped again. Args: string (str): The string to be used in the command. raw (bool, optional): If `True`, the string will be treated as already escaped. Defaults to `False`. Methods: escape(): Returns the escaped version of the string, unless the string is already escaped (`raw=True`). __str__(): Returns the raw string `string` for direct use or display. ''' def __init__(self, string, raw=False): '''Initializes a FishString instance. Args: string (str): The string to be used for command-line purposes. raw (bool, optional): If True, indicates that the string `s` is already escaped and should not be escaped again. Defaults to False. ''' assert isinstance(string, str) assert isinstance(raw, bool) self.string = string self.raw = raw def escape(self): '''Escapes the string unless it's marked as already escaped. If the `raw` attribute is set to `True`, the method returns the string without any changes. Otherwise, it applies shell escaping to ensure the string is safe to use in a command-line context. Returns: str: The escaped string, or the raw string if it's already escaped. ''' if self.raw: return self.string return shell.quote(self.string) def __str__(self): return self.string def __repr__(self): return repr(self.string) def make_fish_string(s, raw): '''Make a Fish string.''' if s is not None: return FishString(s, raw) return None class FishCompleteCommand: '''Class for creating Fish's `complete` command.''' # pylint: disable=too-many-instance-attributes def __init__(self): self.command = None self.description = None self.condition = None self.arguments = None self.short_options = [] self.long_options = [] self.old_options = [] self.flags = set() def set_command(self, command, raw=False): '''Set the command (-c|--command).''' self.command = FishString(command, raw) def set_description(self, description, raw=False): '''Set the description (-d|--description).''' self.description = make_fish_string(description, raw) def add_short_options(self, opts, raw=False): '''Add short options (-s|--short-option).''' self.short_options.extend(FishString(o.lstrip('-'), raw) for o in opts) def add_long_options(self, opts, raw=False): '''Add long options (-l|--long-option).''' self.long_options.extend(FishString(o.lstrip('-'), raw) for o in opts) def add_old_options(self, opts, raw=False): '''Add old options (-o|--old-option).''' self.old_options.extend(FishString(o.lstrip('-'), raw) for o in opts) def set_condition(self, condition, raw=False): '''Set the condition (-n|--condition).''' self.condition = make_fish_string(condition, raw) def set_arguments(self, arguments, raw=False): '''Set the arguments (-a|--arguments).''' self.arguments = make_fish_string(arguments, raw) def add_flag(self, flag): '''Set a flag.''' self.flags.add(flag) def parse_args(self, args): '''Parse args (an iterable) and apply it to the instance.''' args = list(args) while len(args) > 0: arg = args.pop(0) if arg == '-f': self.add_flag('f') elif arg == '-F': self.add_flag('F') elif arg == '-a': try: self.set_arguments(args.pop(0)) except IndexError as e: raise InternalError('Option `-a` requires an argument') from e else: raise InternalError(f'Unknown option for `complete`: {arg}') def get(self): '''Return the `complete` command.''' r = ['complete'] if self.command is not None: r.extend(['-c', self.command]) if self.condition is not None: r.extend(['-n', self.condition]) for o in self.short_options: r.extend(['-s', o]) for o in self.long_options: r.extend(['-l', o]) for o in self.old_options: r.extend(['-o', o]) if self.description is not None: r.extend(['-d', self.description]) # -r -f is the same as -x if 'r' in self.flags and 'f' in self.flags: self.flags.add('x') # -x implies -r -f if 'x' in self.flags: self.flags.discard('r') self.flags.discard('f') if self.flags: r.append('-%s' % ''.join(sorted(self.flags))) if self.arguments is not None: r.extend(['-a', self.arguments]) # Pylint doesn't understand that .escape is only called when the # value is not a regular Python string. # pylint: disable=no-member return ' '.join(v if isinstance(v, str) else v.escape() for v in r) class VariableManager: ''' Manages the creation of unique shell variables for a given name prefix. ''' def __init__(self, variable_name): self.variable_name = variable_name self.value_to_variable = {} self.counter = 0 def add(self, value): '''Add a value and get its associated shell variable.''' if value in self.value_to_variable: return '$%s' % self.value_to_variable[value] var = '%s%03d' % (self.variable_name, self.counter) self.value_to_variable[value] = var self.counter += 1 return '$%s' % var def get_lines(self): '''Generate shell code to define all stored variables.''' r = [] for value, variable in self.value_to_variable.items(): r.append('set -l %s %s' % (variable, value)) return r crazy-complete-crazy-complete-bc5afec/crazy_complete/generation.py000066400000000000000000000111241520061347500257650ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Functions that are used in the generation process.''' from .errors import CrazyError from . import completion_validator from . import cli from . import config as _config from . import when class GenerationContext: '''Holds global configuration and helpers used during code generation.''' # pylint: disable=too-few-public-methods def __init__(self, config, helpers): self.config = config self.helpers = helpers def get_option_context(self, commandline, option): '''Return OptionGenerationContext based on current context.''' return OptionGenerationContext( self.config, self.helpers, commandline, option ) class OptionGenerationContext(GenerationContext): '''Extends GenerationContext; holds commandline and option objects.''' # pylint: disable=too-few-public-methods def __init__(self, config, helpers, commandline, option): super().__init__(config, helpers) self.commandline = commandline self.option = option def _apply_config(commandline, config): '''Applies configuration settings to a command line object. If a setting in the CommandLine or Option object is set to ExtendedBool.INHERIT, it will be overridden by the corresponding setting from the config object. Args: commandline (CommandLine): The command line object to apply the configuration to. config (Config): The configuration object containing the settings to apply. Returns: None ''' assert isinstance(commandline, cli.CommandLine) assert isinstance(config, _config.Config) if commandline.abbreviate_commands == cli.ExtendedBool.INHERIT: commandline.abbreviate_commands = config.abbreviate_commands if commandline.abbreviate_options == cli.ExtendedBool.INHERIT: commandline.abbreviate_options = config.abbreviate_options if commandline.inherit_options == cli.ExtendedBool.INHERIT: commandline.inherit_options = config.inherit_options for option in commandline.options: if option.repeatable == cli.ExtendedBool.INHERIT: option.repeatable = config.repeatable_options if option.long_opt_arg_sep == cli.ExtendedBool.INHERIT: option.long_opt_arg_sep = config.long_opt_arg_sep if config.disabled_hidden: option.hidden = False if config.disabled_final: option.final = False if config.disabled_groups: option.groups = [] if config.disabled_repeatable: option.repeatable = True if config.disabled_when: option.when = None for positional in commandline.positionals: if config.disabled_when: positional.when = None def _add_parsed_when(commandline): for option in commandline.options: if option.when: try: option.when_parsed = when.parse_when(option.when) except CrazyError as e: raise CrazyError('%s: %s: when: %s: %s' % ( commandline.get_command_path(), option.get_option_strings_key('|'), option.when, e)) from e else: option.when_parsed = None for positional in commandline.positionals: if positional.when: try: positional.when_parsed = when.parse_when(positional.when) except CrazyError as e: raise CrazyError('%s: %s: %s: when: %s: %s' % ( commandline.get_command_path(), positional.number, positional.metavar, positional.when, e)) from e else: positional.when_parsed = None def enhance_commandline(commandline, config): '''Enhance commandline. - Make a copy of commandline - Apply configuration to it - Add `when_parsed` attribute ''' commandline = commandline.copy() commandline.visit_commandlines(lambda c: _apply_config(c, config)) commandline.visit_commandlines(_add_parsed_when) completion_validator.validate_commandlines(commandline) return commandline def visit_commandlines(completion_class, ctxt, commandline): '''Visit commandlines with a completer class.''' result = [] def _call_generator(commandline): result.append(completion_class(ctxt, commandline)) commandline.visit_commandlines(_call_generator) return result crazy-complete-crazy-complete-bc5afec/crazy_complete/help_converter.py000066400000000000000000000074341520061347500266620ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains functions for generating a YAML file from help texts.''' from . import help_parser from . import yaml_source from .str_utils import indent def fix_description(s): '''Fix description.''' s = s.strip() # Replace hyphens followed by a newline ('-\n') with a simple hyphen. # This prevents words like "Some-word-with-\nhyphens" from being split # incorrectly due to the newline. Instead, it will correctly join as # "Some-word-with-hyphens". s = s.replace('-\n', '-') s = s.replace('\n', ' ') return s def strip_metavar(s): '''Strip metavar.''' if s and s[0] == '[' and s[-1] == ']': s = s[1:-1] if s and s[0] == '=': s = s[1:] if s and s[0] == '<' and s[-1] == '>': s = s[1:-1] return s def complete_for_metavar(string): '''Return completion for metavar.''' string = string.strip('<>') lower = string.lower() equals = { 'f': ['file'], 'n': ['integer'], 'duration': ['integer'], 'bits': ['integer'], 'cols': ['integer'], 'column': ['integer'], 'digits': ['integer'], 'size': ['integer'], 'bytes': ['integer'], 'pid': ['pid'], 'user': ['user'], 'group': ['group'], 'program': ['command'], 'command': ['command'], 'cmd': ['cmd'], 'signal': ['signal'], 'sig': ['signal'], } endswith = { 'file': ['file'], 'filename': ['file'], 'dir': ['directory'], 'directory': ['directory'], 'seconds': ['integer'], 'length': ['integer'], 'width': ['integer'], 'num': ['integer'], 'number': ['integer'], } if lower in equals: return equals[lower] for suffix, complete in endswith.items(): if lower.endswith(suffix): return complete if string.startswith('{') and string.endswith('}'): string = string.strip('{}') for sep in ['|', ',']: if sep in string: return ['choices', [item.strip() for item in string.split(sep)]] return ['none'] def from_file_to_yaml(file): '''Parse help text in a file and return YAML.''' with open(file, 'r', encoding='utf-8') as fh: content = fh.read() prog = help_parser.get_program_name_from_help(content) char_stream = help_parser.CharStream(content) parsed = help_parser.parse(char_stream) output = [] output.append(f'prog: "{prog}"\nhelp: ""\noptions:') for obj in parsed: if isinstance(obj, help_parser.Unparsed): output.append(f"# {obj.text.rstrip()}") elif isinstance(obj, help_parser.OptionsWithDescription): option_dict = { 'option_strings': [], 'metavar': None, 'optional_arg': False, 'help': fix_description(obj.description or '') } for option in obj.options: option_dict['option_strings'].append(option.option) if option.optional: option_dict['optional_arg'] = True if option.metavar: option_dict['metavar'] = strip_metavar(option.metavar) option_dict['complete'] = complete_for_metavar(option_dict['metavar']) output.append( indent(yaml_source.option_to_yaml(option_dict), 2) ) output.append('') return '\n'.join(output) crazy-complete-crazy-complete-bc5afec/crazy_complete/help_parser.py000066400000000000000000000212371520061347500261440ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth """This module contains functions to parse the --help output of a program.""" import re from collections import namedtuple # Characters that should not be considered option chars OPTION_BREAK_CHARS = [' ', '\t', '\n', ',', '|', '=', '['] # Characters that delimit options OPTION_DELIMITER_CHARS = [',', '|'] Unparsed = namedtuple('Unparsed', ['text']) OptionWithMetavar = namedtuple('OptionWithMetavar', ['option', 'metavar', 'optional']) OptionsWithDescription = namedtuple('OptionsWithDescription', ['options', 'description']) class CharStream: """A utility class for sequentially reading characters from a string.""" def __init__(self, string, pos = 0): self.string = string self.len = len(string) self.pos = pos def peek(self, relative_pos = 0): """ Returns the character at the current position plus an optional relative offset without advancing the stream. Returns None if the position is out of bounds. """ try: return self.string[self.pos + relative_pos] except IndexError: return None def peek_str(self, length): """ Returns a substring of the specified length starting from the current position without advancing the stream. """ return self.string[self.pos:self.pos + length] def get(self): """Returns the current character and advances the position by one.""" c = self.string[self.pos] self.pos += 1 return c def is_space(self): """Checks if the current character is a space or a tab.""" return self.peek() in (' ', '\t') def is_end(self): """Checks if the current position has reached the end of the string.""" return self.pos >= self.len def copy(self): """Creates and returns a new CharStream object at the current position.""" return CharStream(self.string, self.pos) def __repr__(self): line = '' i = self.pos while i < self.len: if self.string[i] == '\n': break line += self.string[i] i += 1 return f"CharStream({line!r})" def eat_line(stream): '''Read the remainling line and return it (including the newline character).''' content = '' while not stream.is_end(): char = stream.get() content += char if char == '\n': break return content def eat_space(stream): '''Read spaces and tabs and return it.''' content = '' while stream.is_space(): content += stream.get() return content def parse_option_string(stream): '''Read an option string and return it. All chars except OPTION_BREAK_CHARS are considered valid option chars. Example option strings: --help, -h If the resulting option string is '-' or '--', it is not considered an option. ''' option = '' p = stream.copy() eat_space(p) if p.peek() != '-': return None while not p.is_end() and p.peek() not in OPTION_BREAK_CHARS: option += p.get() if option in ('-', '--'): return None stream.pos = p.pos return option def parse_bracket(stream): '''Read and return a bracketed expression. Bracketed expressions are: [foo bar] (foo bar) {foo bar} ''' content = stream.peek() try: closing = {'<':'>', '[':']', '(':')', '{':'}'}[content] except KeyError: return None stream.get() while not stream.is_end(): char = stream.get() content += char if char == closing: break return content def parse_quoted_string(stream): '''Read and return a string. Strings are: 'foo bar' "foo bar" Since it is unlikely that we encounter escape sequences in a description string of an option, we don't process any escape sequences. ''' quote = stream.peek() if quote not in ('"', "'"): return None stream.get() content = quote while not stream.is_end(): char = stream.get() content += char if char == quote: break return content def parse_metavar(stream): '''Read and return a metavar. Everything until a tab, space or newline is considered a metavar. Special cases: - Bracketed expressions (e.g., '') and quoted strings (e.g., '"foo bar"') are handled, and the spaces within them are preserved. - The function supports metavars enclosed by `<`, `[`, `(`, `{`, as well as single (`'`) and double (`"`) quotes. Metavars are: foo_bar 'foo bar' "foo bar" ''' metavar = '' while not stream.is_end() and stream.peek() not in (' ', '\t', '\n'): if stream.peek() in ('<', '[', '(', '{'): metavar += parse_bracket(stream) elif stream.peek() in ('"', "'"): metavar += parse_quoted_string(stream) elif stream.peek() in OPTION_DELIMITER_CHARS: break else: metavar += stream.get() return metavar def parse_trailing_description_line(stream): '''Reads and returns a trailing description line. A line is considered a trailing description line if it meets the following criteria: - It starts with whitespace (indicating continuation from a previous line). - It does not begin with a hyphen ('-'), which would indicate the start of a new option. ''' p = stream.copy() if not p.is_space(): return None space = eat_space(p) if p.peek() == '-' and len(space) < 10: return None content = eat_line(p) stream.pos = p.pos return content def parse_description(stream): '''Reads and returns the description of an option.''' eat_space(stream) content = eat_line(stream) while True: line = parse_trailing_description_line(stream) if line: content += line else: break return content def parse_option_with_metavar(stream): '''Read and return an option with its metavar (if any). Valid inputs are: --option=METAVAR --option[=METAVAR] (in this case, 'optional' is set to True) --option METAVAR Invalid inputs are: --option METAVAR (notice two spaces) ''' opt = parse_option_string(stream) metavar = None optional = False if opt: if stream.peek_str(2) == '[=': optional = True metavar = parse_metavar(stream) elif stream.peek() == '=': stream.get() metavar = parse_metavar(stream) # Two spaces after --option means the description follows elif stream.peek_str(2).isspace(): return OptionWithMetavar(opt, metavar, optional) # An option delimiter cannot be a metavar elif parse_option_delimiter(stream.copy()): return OptionWithMetavar(opt, metavar, optional) elif not stream.is_end() and stream.is_space(): stream.get() return OptionWithMetavar(opt, parse_metavar(stream), optional) return OptionWithMetavar(opt, metavar, optional) return None def parse_option_delimiter(stream): '''Parse an option delimiter and return True if it was found, False otherwise.''' p = stream.copy() eat_space(p) if p.get() in (',', '|'): stream.pos = p.pos return True return False def parse_options_with_description(stream): '''Parse options with description.''' options = [] description = None while not stream.is_end(): option = parse_option_with_metavar(stream) if option: options.append(option) else: break if not parse_option_delimiter(stream): break if not options: return None # if s.peek() == '\n' or s.peek_str(2) in (' ', ' \n'): if stream.peek() in (' ', '\t', '\n'): description = parse_description(stream) return OptionsWithDescription(options, description) def parse(stream): """Parses the stream and returns a list of options with descriptions or unparsed lines.""" r = [] while not stream.is_end(): options = parse_options_with_description(stream) if options: r.append(options) else: line = eat_line(stream) r.append(Unparsed(line)) return r def get_program_name_from_help(string): """Extracts the program name from the help string.""" m = re.match('usage:[\n\t ]+([^\n\t ]+)', string, re.I) if m: return m[1] return string.split()[0] crazy-complete-crazy-complete-bc5afec/crazy_complete/helpers.py000066400000000000000000000200731520061347500252770ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Classes for including functions in the generation process.''' import re from . import cli from . import shell from . import preprocessor from .str_utils import (indent, strip_comments, strip_double_empty_lines) # pylint: disable=too-few-public-methods def replace_function_name(search, replace, s): '''Safely replace a function name in code''' pattern = f'\\b{search}\\b' return re.sub(pattern, replace, s) class FunctionBase: '''Base class for functions.''' def __init__(self, funcname, code, dependencies=None): self.funcname = funcname self.code = code.strip() self.dependencies = dependencies if dependencies else [] def get_code(self): '''Return the whole function.''' raise NotImplementedError class ShellFunction(FunctionBase): '''Class for generating Bash and Zsh functions.''' def get_code(self, funcname=None, defines=frozenset()): if funcname is None: funcname = self.funcname code = self.code.replace('%FUNCNAME%', funcname) code = preprocessor.preprocess(code, defines) code = strip_double_empty_lines(code) r = '%s() {\n' % funcname r += '%s\n' % indent(code, 2).rstrip() r += '}' return r class FishFunction(FunctionBase): '''Class for generating Fish functions.''' def get_code(self, funcname=None, defines=frozenset()): if funcname is None: funcname = self.funcname code = self.code.replace('%FUNCNAME%', funcname) code = preprocessor.preprocess(code, defines) code = strip_double_empty_lines(code) r = 'function %s\n' % funcname r += '%s\n' % indent(code, 2).rstrip() r += 'end' return r def make_completion_funcname_for_context(ctxt, prefix): '''TODO.''' commandlines = ctxt.commandline.get_parents(include_self=True) prognames = [p.prog for p in commandlines] prognames[0] = prefix funcname = shell.make_identifier('_'.join(prognames)) if isinstance(ctxt.option, cli.Option): return '%s__%s' % (funcname, ctxt.option.option_strings[0]) if isinstance(ctxt.option, cli.Positional): if ctxt.option.metavar: identifier = shell.make_identifier(ctxt.option.metavar) else: identifier = 'arg%d' % ctxt.option.get_positional_num() return '%s__%s' % (funcname, identifier) raise AssertionError('Should not be reached') class GeneralHelpers: '''Class for including functions in the generation process.''' def __init__(self, config, prog, function_class): self.config = config self.function_prefix = config.function_prefix.replace('$PROG', prog) self.function_class = function_class # Dynamic functions self.dynamic_functions_code_to_funcname = {} # Builtin functins self.functions = {} self.used_functions = {} # funcname:set(defines) self.global_defines = set() # Set debug mode if self.config.debug: self.define('DEBUG') # ========================================================================= # Dynamic functions # ========================================================================= def add_dynamic_func(self, ctxt, code): '''Add dynamically generated function.''' try: return self.dynamic_functions_code_to_funcname[code] except KeyError: funcname = self.get_dynamic_funcname(ctxt) self.dynamic_functions_code_to_funcname[code] = funcname return funcname def get_dynamic_funcname(self, ctxt): '''Return a unique function name for `ctxt`.''' funcname = make_completion_funcname_for_context(ctxt, self.function_prefix) num = 0 funcname_plus_num = funcname while funcname_plus_num in self.dynamic_functions_code_to_funcname.values(): funcname_plus_num = '%s%d' % (funcname, num) num += 1 return funcname_plus_num def get_all_dynamic_functions(self): '''Return a list of code for all dynamically defined functions.''' r = [] for code, funcname in self.dynamic_functions_code_to_funcname.items(): r.append(self.function_class(funcname, code).get_code()) return r # ========================================================================= # Builtin functions # ========================================================================= def add_function(self, function): '''Add a function to the function repository.''' assert isinstance(function, FunctionBase) self.functions[function.funcname] = function def define(self, name): '''Define a macro on global scope.''' self.global_defines.add(name) def use_function(self, function_name, *defines): '''Use a function from the function repository.''' if function_name not in self.functions: raise KeyError('No such function: %r' % function_name) if function_name not in self.used_functions: self.used_functions[function_name] = set(defines) else: self.used_functions[function_name].update(defines) # Add dependencies for function in self.functions[function_name].dependencies: self.use_function(function) return self.get_real_function_name(function_name) def get_real_function_name(self, function_name): '''Return the function name with its prefix.''' return '%s__%s' % (self.function_prefix, function_name) def is_used(self, function_name): '''Check if a function is used.''' return function_name in self.used_functions def get_used_functions_code(self): '''Return a list of code for all used functions.''' r = [] for funcname, defines in self.used_functions.items(): function = self.functions[funcname] realname = self.get_real_function_name(funcname) code = function.get_code(realname, self.global_defines | defines) for dep_func in function.dependencies: real_func = self.get_real_function_name(dep_func) code = replace_function_name(dep_func, real_func, code) code = code.replace('%PREFIX%', self.function_prefix) if not self.config.keep_comments: code = strip_comments(code) code = strip_double_empty_lines(code) r.append(code) r.extend(self.get_all_dynamic_functions()) return r # ========================================================================= # Other methods # ========================================================================= def make_completion_funcname(self, commandline, suffix=''): '''Generates a function name for auto-completing a program or subcommand. Args: cmdline (CommandLine): The CommandLine instance representing the program or subcommand. suffix (str): The suffix that shall be appended to the result. Returns: str: The generated function name for auto-completion. This function is used to generate a unique function name for auto-completing a program or subcommand in the specified shell. It concatenates the names of all parent commandlines, including the current commandline, and converts them into a valid function name format. Example: For a program with the name 'my_program' and a subcommand with the name 'subcommand', the generated function name is '_my_program_subcommand'. ''' assert isinstance(commandline, cli.CommandLine) commandlines = commandline.get_parents(include_self=True) prognames = [cmdline.prog for cmdline in commandlines] prognames[0] = self.function_prefix identifier = shell.make_identifier('_'.join(prognames)) return f'{identifier}{suffix}' crazy-complete-crazy-complete-bc5afec/crazy_complete/json_source.py000066400000000000000000000013741520061347500261710ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth """ This module provides functions for creating CommandLine objects from JSON and vice versa. """ import json from . import dictionary_source def load_from_file(file): '''Load a JSON file and turn it into a cli.CommandLine object.''' with open(file, 'r', encoding='utf-8') as fh: dictionaries = json.load(fh) return dictionary_source.dictionaries_to_commandline(dictionaries) def commandline_to_json(commandline): '''Convert a cli.CommandLine object into JSON.''' dictionaries = dictionary_source.commandline_to_dictionaries(commandline) json_string = json.dumps(dictionaries, indent=None) return json_string crazy-complete-crazy-complete-bc5afec/crazy_complete/manual.py000066400000000000000000003511631520061347500251210ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Built-int Manual.''' from .errors import CrazyError from .str_utils import indent, join_with_wrap # flake8: noqa: E501 # pylint: disable=line-too-long # pylint: disable=too-many-lines COMMANDS = [{'also': {'alsa_device': 'For completing an ALSA device'}, 'category': 'bonus', 'command': 'alsa_card', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--alsa-card"]\n' ' complete: ["alsa_card"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--alsa-card\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33malsa_card\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --alsa-card=\n0 1\n', 'short': 'Complete an ALSA card'}, {'also': {'alsa_card': 'For completing an ALSA card'}, 'category': 'bonus', 'command': 'alsa_device', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--alsa-device"]\n' ' complete: ["alsa_device"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--alsa-device\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33malsa_device\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --alsa-device=\nhw:0 hw:1\n', 'short': 'Complete an ALSA device'}, {'also': None, 'category': 'bonus', 'command': 'charset', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--charset"]\n' ' complete: ["charset"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--charset\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mcharset\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --charset=A\n' 'ANSI_X3.110-1983 ANSI_X3.4-1968 ARMSCII-8 ASMO_449\n', 'short': 'Complete a charset'}, {'also': None, 'category': 'basic', 'command': 'choices', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--choices-1"]\n' ' complete: ["choices", ["Item 1", "Item 2"]]\n' '\n' ' - option_strings: ["--choices-2"]\n' ' complete: ["choices", {"Item 1": "Description 1", "Item ' '2": "Description 2"}]\n' '\n' ' - option_strings: ["--choices-keep-order"]\n' ' complete: ["choices", ["zebra", "cat", "monkey"]]\n' ' nosort: true\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--choices-1\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mchoices\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mItem\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33m1\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mItem\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33m2\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--choices-2\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mchoices\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mItem\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33m1\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mDescription\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33m1\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mItem\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33m2\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mDescription\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33m2\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m}]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--choices-keep-order\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mchoices\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mzebra\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mcat\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mmonkey\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mnosort\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00mtrue\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'Items can be a list or a dictionary.\n' ' \n' 'If a dictionary is supplied, the keys are used as items and the ' 'values are used\n' 'as description.\n', 'long_colored': 'Items can be a list or a dictionary.\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'If a dictionary is supplied, the keys are used as items and ' 'the values are used\x1b[37m\x1b[39;49;00m\n' 'as description.\x1b[37m\x1b[39;49;00m\n', 'notes': ['If the completion suggestions should appear in their original ' 'order, set `nosort` to `true`'], 'output': '~ > example --choices-2=\n' 'Item 1 (Description 1) Item 2 (Description 2)\n', 'short': 'Complete from a set of words'}, {'also': None, 'category': 'meta', 'command': 'combine', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--combine"]\n' ' complete: ["combine", [["user"], ["pid"]]]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--combine\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mcombine\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m[[\x1b[33m"\x1b[39;49;00m\x1b[33muser\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m],\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mpid\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]]]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'With `combine` multiple completers can be combined into one.\n' '\n' 'It takes a list of completers as its argument.\n', 'long_colored': 'With \x1b[33m`combine`\x1b[39;49;00m multiple completers ' 'can be combined into one.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'It takes a list of completers as its ' 'argument.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --user-list=avahi,daemon,\n' '1439404 3488332 3571716 3607235 ' '4134206\n' 'alpm avahi bin braph ' 'daemon\n' 'root rtkit systemd-coredump systemd-journal-remote ' 'systemd-network\n' '[...]\n', 'short': 'Combine multiple completers'}, {'also': {'command_arg': 'For completing arguments of a command', 'commandline_string': 'For completing a command line as a string'}, 'category': 'basic', 'command': 'command', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--command"]\n' ' complete: ["command"]\n' ' - option_strings: ["--command-sbin"]\n' ' complete: ["command", {"path_append": ' '"/sbin:/usr/sbin"}]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--command\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mcommand\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--command-sbin\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mcommand\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mpath_append\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33m/sbin:/usr/sbin\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m}]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'This completer provides completion suggestions for executable ' "commands available in the system's `$PATH`.\n" ' \n' '`$PATH` can be modified using these options:\n' ' \n' '`{"path": ":..."}`: Overrides the default `$PATH` ' 'entirely.\n' ' \n' '`{"path_append": ":..."}`: Appends to the default ' '`$PATH`.\n' ' \n' '`{"path_prepend": ":..."}`: Prepends to the default ' '`$PATH`.\n', 'long_colored': 'This completer provides completion suggestions for ' "executable commands available in the system's " '\x1b[33m`$PATH`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' ' \n' '\x1b[33m`$PATH`\x1b[39;49;00m can be modified using these ' 'options:\x1b[37m\x1b[39;49;00m\n' ' \n' '\x1b[33m`{"path": ":..."}`\x1b[39;49;00m: ' 'Overrides the default \x1b[33m`$PATH`\x1b[39;49;00m ' 'entirely.\x1b[37m\x1b[39;49;00m\n' ' \n' '\x1b[33m`{"path_append": ":..."}`\x1b[39;49;00m: ' 'Appends to the default ' '\x1b[33m`$PATH`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' ' \n' '\x1b[33m`{"path_prepend": ' '":..."}`\x1b[39;49;00m: Prepends to the default ' '\x1b[33m`$PATH`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n', 'notes': ['`path_append` and `path_prepend` can be used together, but both ' 'are mutually exclusive with `path`.'], 'output': '~ > example --command=bas\n' 'base32 base64 basename basenc bash bashbug\n', 'short': 'Complete a command'}, {'also': {'command': 'For completing a command', 'commandline_string': 'For completing a command line as a string'}, 'category': 'basic', 'command': 'command_arg', 'definition': 'prog: "example"\n' 'positionals:\n' ' - number: 1\n' ' complete: ["command"]\n' '\n' ' - number: 2\n' ' complete: ["command_arg"]\n' ' repeatable: true\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94mpositionals\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94mnumber\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m1\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mcommand\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94mnumber\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m2\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mcommand_arg\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mrepeatable\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00mtrue\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': ['This completer can only be used in combination with a previously ' 'defined `command` completer.', 'This completer requires `repeatable: true`.'], 'output': '~ > example sudo bas\n' 'base32 base64 basename basenc bash bashbug\n', 'short': 'Complete arguments of a command'}, {'also': None, 'category': 'basic', 'command': 'commandline_string', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--commandline"]\n' ' complete: ["commandline_string"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--commandline\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mcommandline_string\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': "~ > example --commandline='sudo ba\n" 'base32 base64 basename basenc bash bashbug\n', 'short': 'Complete a command line as a string'}, {'also': {'date_format': 'For completing a date format string'}, 'category': 'basic', 'command': 'date', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--date"]\n' ' complete: ["date", \'%Y-%m-%d\']\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--date\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mdate\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' "\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m%Y-%m-%d\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n", 'implemented': ['Zsh'], 'long': 'The argument is the date format as described in `strftime(3)`.\n', 'long_colored': 'The argument is the date format as described in ' '\x1b[33m`strftime(3)`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --date=\n' '\n' ' November \n' 'Mo Tu We Th Fr Sa Su \n' ' 1 2 3 4 5 6 \n' ' 7 8 9 10 11 12 13\n' '14 15 16 17 18 19 20\n' '21 22 23 24 25 26 27\n' '28 29 30 \n', 'short': 'Complete a date string'}, {'also': {'date': 'For completing a date'}, 'category': 'basic', 'command': 'date_format', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--date-format"]\n' ' complete: ["date_format"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--date-format\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mdate_format\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': ['Fish', 'Zsh'], 'long': None, 'long_colored': None, 'notes': [], 'output': "~ > example --date-format '%\n" 'a -- abbreviated day name\n' 'A -- full day name\n' 'B -- full month name\n' 'c -- preferred locale date and time\n' 'C -- 2-digit century\n' 'd -- day of month (01-31)\n' 'D -- American format month/day/year (%m/%d/%y)\n' 'e -- day of month ( 1-31)\n' '[...]\n', 'short': 'Complete a date format string'}, {'also': {'directory_list': 'For completing a comma-separated list of ' 'directories'}, 'category': 'basic', 'command': 'directory', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--directory"]\n' ' complete: ["directory"]\n' ' - option_strings: ["--directory-tmp"]\n' ' complete: ["directory", {"directory": "/tmp"}]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--directory\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mdirectory\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--directory-tmp\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mdirectory\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mdirectory\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33m/tmp\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m}]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'You can restrict completion to a specific directory by adding ' '`{"directory": ...}`.\n', 'long_colored': 'You can restrict completion to a specific directory by ' 'adding \x1b[33m`{"directory": ' '...}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --directory=\ndir1/ dir2/\n', 'short': 'Complete a directory'}, {'also': {'directory': 'For completing a directory', 'file_list': 'For completing a comma-separated list of files', 'list': 'For completion a comma-separated list of any completer'}, 'category': 'basic', 'command': 'directory_list', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--directory-list"]\n' ' complete: ["directory_list"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--directory-list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mdirectory_list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': "This is an alias for `['list', ['directory']]`.\n" '\n' 'You can restrict completion to a specific directory by adding ' '`{"directory": ...}`.\n' ' \n' 'The separator can be changed by adding `{"separator": ...}`\n' ' \n' 'By default, duplicate values are not offered for completion. This ' 'can be changed by adding `{"duplicates": true}`.\n', 'long_colored': "This is an alias for \x1b[33m`['list', " "['directory']]`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n" '\x1b[37m\x1b[39;49;00m\n' 'You can restrict completion to a specific directory by ' 'adding \x1b[33m`{"directory": ' '...}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'The separator can be changed by adding ' '\x1b[33m`{"separator": ' '...}`\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'By default, duplicate values are not offered for ' 'completion. This can be changed by adding ' '\x1b[33m`{"duplicates": ' 'true}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --directory-list=directory1,directory2,\n' 'directory3 directory4\n', 'short': 'Complete a comma-separated list of directories'}, {'also': None, 'category': 'basic', 'command': 'environment', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--environment"]\n' ' complete: ["environment"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--environment\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33menvironment\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --environment=X\n' 'XDG_RUNTIME_DIR XDG_SEAT XDG_SESSION_CLASS XDG_SESSION_ID\n' 'XDG_SESSION_TYPE XDG_VTNR\n', 'short': 'Complete a shell environment variable name'}, {'also': {'exec_fast': 'Faster implementation of exec'}, 'category': 'custom', 'command': 'exec', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--exec"]\n' ' complete: ["exec", "printf \'%s\\\\t%s\\\\n\' \'Item 1\' ' '\'Description 1\' \'Item 2\' \'Description 2\'"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--exec\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mexec\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mprintf\x1b[39;49;00m\x1b[31m ' "\x1b[39;49;00m\x1b[33m'%s\x1b[39;49;00m\x1b[33m\\\\\x1b[39;49;00m\x1b[33mt%s\x1b[39;49;00m\x1b[33m\\\\\x1b[39;49;00m\x1b[33mn'\x1b[39;49;00m\x1b[31m " "\x1b[39;49;00m\x1b[33m'Item\x1b[39;49;00m\x1b[31m " "\x1b[39;49;00m\x1b[33m1'\x1b[39;49;00m\x1b[31m " "\x1b[39;49;00m\x1b[33m'Description\x1b[39;49;00m\x1b[31m " "\x1b[39;49;00m\x1b[33m1'\x1b[39;49;00m\x1b[31m " "\x1b[39;49;00m\x1b[33m'Item\x1b[39;49;00m\x1b[31m " "\x1b[39;49;00m\x1b[33m2'\x1b[39;49;00m\x1b[31m " "\x1b[39;49;00m\x1b[33m'Description\x1b[39;49;00m\x1b[31m " '\x1b[39;49;00m\x1b[33m2\'\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'The output must be in form of:\n' '\n' '```\n' '\\t\\n\n' '\\t\\n\n' '[...]\n' '```\n' '\n' 'An item and its description are delimited by a tabulator.\n' ' \n' 'These pairs are delimited by a newline.\n', 'long_colored': 'The output must be in form of:\x1b[37m\x1b[39;49;00m\n' '\x1b[33m\x1b[39;49;00m\n' '\x1b[33m```\x1b[39;49;00m\n' '\x1b[33m\\t\\n\x1b[39;49;00m\n' '\x1b[33m\\t\\n\x1b[39;49;00m\n' '\x1b[33m[...]\x1b[39;49;00m\n' '\x1b[33m```\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'An item and its description are delimited by a ' 'tabulator.\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'These pairs are delimited by a ' 'newline.\x1b[37m\x1b[39;49;00m\n', 'notes': ['Functions can be put inside a file and included with ' '`--include-file`'], 'output': '~ > example --exec=\n' 'Item 1 (Description 1) Item 2 (Description 2)\n', 'short': 'Complete by the output of a command or function'}, {'also': None, 'category': 'custom', 'command': 'exec_fast', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--exec-fast"]\n' ' complete: ["exec_fast", "printf \'%s\\\\t%s\\\\n\' 1 one ' '2 two"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--exec-fast\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mexec_fast\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mprintf\x1b[39;49;00m\x1b[31m ' "\x1b[39;49;00m\x1b[33m'%s\x1b[39;49;00m\x1b[33m\\\\\x1b[39;49;00m\x1b[33mt%s\x1b[39;49;00m\x1b[33m\\\\\x1b[39;49;00m\x1b[33mn'\x1b[39;49;00m\x1b[31m " '\x1b[39;49;00m\x1b[33m1\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mone\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33m2\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mtwo\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'Faster version of exec for handling large amounts of data.\n' ' \n' 'This implementation requires that the items of the parsed output do ' 'not include\n' 'special shell characters or whitespace.\n', 'long_colored': 'Faster version of exec for handling large amounts of ' 'data.\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'This implementation requires that the items of the parsed ' 'output do not include\x1b[37m\x1b[39;49;00m\n' 'special shell characters or ' 'whitespace.\x1b[37m\x1b[39;49;00m\n', 'notes': ['Functions can be put inside a file and included with ' '`--include-file`'], 'output': '~ > example --exec-internal=\n1 -- one\n2 -- one\n', 'short': 'Complete by the output of a command or function (fast and unsafe)'}, {'also': None, 'category': 'custom', 'command': 'exec_internal', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--exec-internal"]\n' ' complete: ["exec_internal", "my_completion_func"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--exec-internal\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mexec_internal\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mmy_completion_func\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'Execute a function that internally modifies the completion state.\n' '\n' 'This is useful if a more advanced completion is needed.\n' '\n' 'For **Bash**, it might look like:\n' '\n' '```sh\n' 'my_completion_func() {\n' ' COMPREPLY=( $(compgen -W "read write append" -- "$cur") )\n' '}\n' '```\n' '\n' 'For **Zsh**, it might look like:\n' '\n' '```sh\n' 'my_completion_func() {\n' ' local items=(\n' " read:'Read data from a file'\n" " write:'Write data from a file'\n" " append:'Append data to a file'\n" ' )\n' '\n' " _describe 'my items' items\n" '}\n' '```\n' '\n' 'For **Fish**, it might look like:\n' '\n' '```sh\n' 'function my_completion_func\n' " printf '%s\\t%s\\n' \\\n" " read 'Read data from a file' \\\n" " write 'Write data from a file' \\\n" " append 'Append data to a file'\n" 'end\n' '```\n', 'long_colored': 'Execute a function that internally modifies the completion ' 'state.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'This is useful if a more advanced completion is ' 'needed.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'For **Bash**, it might look like:\x1b[37m\x1b[39;49;00m\n' '\x1b[33m\x1b[39;49;00m\n' '\x1b[33m```\x1b[39;49;00m\x1b[33msh\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' 'my_completion_func()\x1b[37m ' '\x1b[39;49;00m{\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[31mCOMPREPLY\x1b[39;49;00m=(\x1b[37m ' '\x1b[39;49;00m\x1b[34m$(\x1b[39;49;00m\x1b[36mcompgen\x1b[39;49;00m\x1b[37m ' '\x1b[39;49;00m-W\x1b[37m \x1b[39;49;00m\x1b[33m"read write ' 'append"\x1b[39;49;00m\x1b[37m \x1b[39;49;00m--\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[31m$cur\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[34m)\x1b[39;49;00m\x1b[37m ' '\x1b[39;49;00m)\x1b[37m\x1b[39;49;00m\n' '}\x1b[37m\x1b[39;49;00m\n' '\x1b[33m```\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'For **Zsh**, it might look like:\x1b[37m\x1b[39;49;00m\n' '\x1b[33m\x1b[39;49;00m\n' '\x1b[33m```\x1b[39;49;00m\x1b[33msh\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' 'my_completion_func()\x1b[37m ' '\x1b[39;49;00m{\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[36mlocal\x1b[39;49;00m\x1b[37m ' '\x1b[39;49;00m\x1b[31mitems\x1b[39;49;00m=(\x1b[37m\x1b[39;49;00m\n' "\x1b[37m \x1b[39;49;00mread:\x1b[33m'Read data from " "a file'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n" "\x1b[37m \x1b[39;49;00mwrite:\x1b[33m'Write data " "from a file'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n" "\x1b[37m \x1b[39;49;00mappend:\x1b[33m'Append data " "to a file'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n" '\x1b[37m \x1b[39;49;00m)\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m_describe\x1b[37m ' "\x1b[39;49;00m\x1b[33m'my items'\x1b[39;49;00m\x1b[37m " '\x1b[39;49;00mitems\x1b[37m\x1b[39;49;00m\n' '}\x1b[37m\x1b[39;49;00m\n' '\x1b[33m```\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'For **Fish**, it might look like:\x1b[37m\x1b[39;49;00m\n' '\x1b[33m\x1b[39;49;00m\n' '\x1b[33m```\x1b[39;49;00m\x1b[33msh\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[34mfunction\x1b[39;49;00m\x1b[37m ' '\x1b[39;49;00mmy_completion_func\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[36mprintf\x1b[39;49;00m\x1b[37m ' "\x1b[39;49;00m\x1b[33m'%s\\t%s\\n'\x1b[39;49;00m\x1b[37m " '\x1b[39;49;00m\x1b[33m\\\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[36mread\x1b[39;49;00m\x1b[37m ' "\x1b[39;49;00m\x1b[33m'Read data from a " "file'\x1b[39;49;00m\x1b[37m " '\x1b[39;49;00m\x1b[33m\\\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00mwrite\x1b[37m ' "\x1b[39;49;00m\x1b[33m'Write data from a " "file'\x1b[39;49;00m\x1b[37m " '\x1b[39;49;00m\x1b[33m\\\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00mappend\x1b[37m ' "\x1b[39;49;00m\x1b[33m'Append data to a " "file'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n" 'end\x1b[37m\x1b[39;49;00m\n' '\x1b[33m```\x1b[39;49;00m\n', 'notes': ['Functions can be put inside a file and included with ' '`--include-file`'], 'output': '~ > example --exec-internal=\n' 'append -- Append data to a file\n' 'read -- Read data from a file\n' 'write -- Write data from a file\n', 'short': "Complete by a function that uses the shell's internal completion " 'mechanisms'}, {'also': {'file_list': 'For completing a comma-separated list of files', 'mime_file': "For completing a file based on it's MIME-type"}, 'category': 'basic', 'command': 'file', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--file"]\n' ' complete: ["file"]\n' '\n' ' - option_strings: ["--file-tmp"]\n' ' complete: ["file", {"directory": "/tmp"}]\n' '\n' ' - option_strings: ["--file-ext"]\n' ' complete: ["file", {"extensions": ["c", "cpp"]}]\n' '\n' ' - option_strings: ["--file-ignore"]\n' ' complete: ["file", {"ignore_globs": ["*.[tT][xX][tT]", ' '"*.c++"]}]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--file\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mfile\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--file-tmp\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mfile\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mdirectory\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33m/tmp\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m}]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--file-ext\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mfile\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mextensions\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mc\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mcpp\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]}]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--file-ignore\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mfile\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mignore_globs\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m*.[tT][xX][tT]\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33m*.c++\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]}]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'You can restrict completion to a specific directory by adding ' '`{"directory": ...}`.\n' ' \n' 'You can restrict completion to specific extensions by adding ' '`{"extensions": [...]}`.\n' ' \n' 'You can make matching extensions *fuzzy* by adding `{"fuzzy": ' 'true}`.\n' 'Fuzzy means that the files do not have to end with the exact ' 'extension. For example `foo.txt.1`.\n' '\n' 'You can ignore files by adding a list of Bash globs using ' '`{"ignore_globs": [...]}`\n' '\n' '**NOTE:** Restricting completion to specific file extensions only ' 'makes sense if the program being completed actually expects files ' 'of those types.\n' 'On Unix-like systems, file extensions generally have no inherent ' 'meaning -- they are purely conventional and not required for ' 'determining file types.\n', 'long_colored': 'You can restrict completion to a specific directory by ' 'adding \x1b[33m`{"directory": ' '...}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'You can restrict completion to specific extensions by ' 'adding \x1b[33m`{"extensions": ' '[...]}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'You can make matching extensions *fuzzy* by adding ' '\x1b[33m`{"fuzzy": ' 'true}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' 'Fuzzy means that the files do not have to end with the ' 'exact extension. For example ' '\x1b[33m`foo.txt.1`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'You can ignore files by adding a list of Bash globs using ' '\x1b[33m`{"ignore_globs": ' '[...]}`\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\n' '**NOTE:** Restricting completion to specific file ' 'extensions only makes sense if the program being completed ' 'actually expects files of those ' 'types.\x1b[37m\x1b[39;49;00m\n' 'On Unix-like systems, file extensions generally have no ' 'inherent meaning -- they are purely conventional and not ' 'required for determining file ' 'types.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --file=\n' 'dir1/ dir2/ file1 file2\n' '~ > example --file-ext=\n' 'dir1/ dir2/ file.c file.cpp\n', 'short': 'Complete a file'}, {'also': {'directory_list': 'For completing a comma-separated list of ' 'directories', 'file': 'For completing a file', 'list': 'For completing a comma-separted list using any completer'}, 'category': 'basic', 'command': 'file_list', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--file-list"]\n' ' complete: ["file_list"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--file-list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mfile_list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': "This is an alias for `['list', ['file']]`.\n" '\n' 'You can restrict completion to a specific directory by adding ' '`{"directory": ...}`.\n' '\n' 'You can restrict completion to specific extensions by adding ' '`{"extensions": [...]}`.\n' '\n' 'You can make matching extensions *fuzzy* by adding `{"fuzzy": ' 'true}`.\n' 'Fuzzy means that the files do not have to end with the exact ' 'extension. For example `foo.txt.1`.\n' '\n' 'You can ignore files by adding a list of Bash globs using ' '`{"ignore_globs": [...]}`\n' '\n' 'By default, duplicate values are not offered for completion. This ' 'can be changed by adding `{"duplicates": true}`.\n' '\n' 'The separator can be changed by adding `{"separator": ...}`\n', 'long_colored': "This is an alias for \x1b[33m`['list', " "['file']]`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n" '\x1b[37m\x1b[39;49;00m\n' 'You can restrict completion to a specific directory by ' 'adding \x1b[33m`{"directory": ' '...}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'You can restrict completion to specific extensions by ' 'adding \x1b[33m`{"extensions": ' '[...]}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'You can make matching extensions *fuzzy* by adding ' '\x1b[33m`{"fuzzy": ' 'true}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' 'Fuzzy means that the files do not have to end with the ' 'exact extension. For example ' '\x1b[33m`foo.txt.1`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'You can ignore files by adding a list of Bash globs using ' '\x1b[33m`{"ignore_globs": ' '[...]}`\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'By default, duplicate values are not offered for ' 'completion. This can be changed by adding ' '\x1b[33m`{"duplicates": ' 'true}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'The separator can be changed by adding ' '\x1b[33m`{"separator": ' '...}`\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --file-list=file1,file2,\nfile3 file4\n', 'short': 'Complete a comma-separated list of files'}, {'also': None, 'category': 'basic', 'command': 'filesystem_type', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--filesystem-type"]\n' ' complete: ["filesystem_type"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--filesystem-type\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mfilesystem_type\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --filesystem-type=\n' 'adfs autofs bdev bfs binder binfmt_misc bpf\n' 'cgroup cgroup2 configfs cramfs debugfs devpts ' 'devtmpfs\n' '[...]\n', 'short': 'Complete a filesystem type'}, {'also': {'integer': 'For completing an integer'}, 'category': 'basic', 'command': 'float', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--time"]\n' ' complete: ["float", {"suffixes": {"s": "seconds", "m": ' '"minutes", "h": "hours"}}]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--time\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mfloat\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33msuffixes\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33ms\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mseconds\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mm\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mminutes\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mh\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mhours\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m}}]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'A min value can be specified by using `{"min": }`.\n' '\n' 'A max value can be specified by using `{"max": }`.\n' '\n' 'A list of suffixes can be specified by using `{"suffixes": ' '{"": ""}`. If not ' 'supplied, the `help` attribute of the option is used.\n', 'long_colored': 'A min value can be specified by using \x1b[33m`{"min": ' '}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'A max value can be specified by using \x1b[33m`{"max": ' '}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'A list of suffixes can be specified by using ' '\x1b[33m`{"suffixes": {"": ""}`\x1b[39;49;00m. If not supplied, the ' '\x1b[33m`help`\x1b[39;49;00m attribute of the option is ' 'used.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --time=3.0\n' 's -- seconds m -- minutes h -- hours\n', 'short': 'Complete a floating point number'}, {'also': {'group': 'For completing a group name'}, 'category': 'basic', 'command': 'gid', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--gid"]\n' ' complete: ["gid"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--gid\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mgid\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --gid=\n' '0 -- root\n' '1000 -- braph\n' '102 -- polkitd\n' '108 -- vboxusers\n' '11 -- ftp\n' '12 -- mail\n' '133 -- rtkit\n' '19 -- log\n' '[...]\n', 'short': 'Complete a group id'}, {'also': {'gid': 'For completing a group id'}, 'category': 'basic', 'command': 'group', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--group"]\n' ' complete: ["group"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--group\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mgroup\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --group=\n' 'adm audio avahi\n' 'bin braph colord\n' 'daemon dbus dhcpcd\n' 'disk floppy ftp\n' 'games git groups\n' '[...]\n', 'short': 'Complete a group'}, {'also': None, 'category': 'basic', 'command': 'history', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--history"]\n' ' complete: ["history", \'[a-zA-Z0-9]+@[a-zA-Z0-9]+\']\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--history\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mhistory\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' "\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m[a-zA-Z0-9]+@[a-zA-Z0-9]+\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n", 'implemented': None, 'long': 'The argument is an extended regular expression passed to `grep ' '-E`.\n', 'long_colored': 'The argument is an extended regular expression passed to ' '\x1b[33m`grep -E`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --history=\nfoo@bar mymail@myprovider\n', 'short': "Complete based on a shell's history"}, {'also': None, 'category': 'basic', 'command': 'hostname', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--hostname"]\n' ' complete: ["hostname"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--hostname\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mhostname\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --hostname=\nlocalhost\n', 'short': 'Complete a hostname'}, {'also': {'float': 'For completing a floating point number', 'range': 'For completing a range of integers'}, 'category': 'basic', 'command': 'integer', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--time"]\n' ' complete: ["integer", {"suffixes": {"s": "seconds", "m": ' '"minutes", "h": "hours"}}]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--time\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33minteger\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33msuffixes\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33ms\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mseconds\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mm\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mminutes\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mh\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mhours\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m}}]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'A min value can be specified by using `{"min": }`.\n' '\n' 'A max value can be specified by using `{"max": }`.\n' '\n' 'A list of suffixes can be specified by using `{"suffixes": ' '{"": ""}`. If not ' 'supplied, the `help` attribute of the option is used.\n', 'long_colored': 'A min value can be specified by using \x1b[33m`{"min": ' '}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'A max value can be specified by using \x1b[33m`{"max": ' '}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'A list of suffixes can be specified by using ' '\x1b[33m`{"suffixes": {"": ""}`\x1b[39;49;00m. If not supplied, the ' '\x1b[33m`help`\x1b[39;49;00m attribute of the option is ' 'used.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --integer=3\n' 's -- seconds m -- minutes h -- hours\n', 'short': 'Complete an integer'}, {'also': {'net_interface': 'For completing a network interface'}, 'category': 'basic', 'command': 'ip_address', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--ip-address"]\n' ' complete: ["ip_address"]\n' ' - option_strings: ["--ip-address-v4"]\n' ' complete: ["ip_address", "ipv4"]\n' ' - option_strings: ["--ip-address-v6"]\n' ' complete: ["ip_address", "ipv6"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--ip-address\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mip_address\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--ip-address-v4\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mip_address\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mipv4\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--ip-address-v6\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mip_address\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mipv6\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'By default, both IPv4 and IPv6 addresses are completed.\n' '\n' 'To complete only IPv4 addresses, pass "ipv4" as argument.\n' '\n' 'To complete only IPv6 addresses, pass "ipv6" as argument.\n', 'long_colored': 'By default, both IPv4 and IPv6 addresses are ' 'completed.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'To complete only IPv4 addresses, pass "ipv4" as ' 'argument.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'To complete only IPv6 addresses, pass "ipv6" as ' 'argument.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --ip-address=\n' '::1 10.0.0.71 127.0.0.1 fe80::f567:7a1a:3c98:808d\n' '\n' '~ > example --ip-address-v4=\n' '10.0.0.71 127.0.0.1\n' '\n' '~ > example --ip-address-v6=\n' '::1 fe80::f567:7a1a:3c98:808d\n', 'short': 'Complete a bound ip address'}, {'also': {'list': 'For completing a comma-separated list of any completer', 'value_list': 'For completing a comma-separated list of values'}, 'category': 'meta', 'command': 'key_value_list', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--key-value-list"]\n' ' complete: ["key_value_list", ",", "=", [\n' " ['flag', 'An option flag', null],\n" " ['nodesc', null, null],\n" " ['nocomp', 'An option with arg but without completer', " "['none']],\n" " ['user', 'Takes a username', ['user']],\n" " ['check', 'Specify file name conversions', ['choices', " '{\n' ' \'relaxed\': "convert to lowercase before lookup",\n' ' \'strict\': "no conversion"\n' ' }]]\n' ' ]]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--key-value-list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mkey_value_list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33m,\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33m=\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m[\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' "\x1b[39;49;00m[\x1b[33m'\x1b[39;49;00m\x1b[33mflag\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " "\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33mAn\x1b[39;49;00m\x1b[31m " '\x1b[39;49;00m\x1b[33moption\x1b[39;49;00m\x1b[31m ' "\x1b[39;49;00m\x1b[33mflag\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " '\x1b[39;49;00m\x1b[31mnull\x1b[39;49;00m],\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' "\x1b[39;49;00m[\x1b[33m'\x1b[39;49;00m\x1b[33mnodesc\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " '\x1b[39;49;00m\x1b[31mnull\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[31mnull\x1b[39;49;00m],\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' "\x1b[39;49;00m[\x1b[33m'\x1b[39;49;00m\x1b[33mnocomp\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " "\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33mAn\x1b[39;49;00m\x1b[31m " '\x1b[39;49;00m\x1b[33moption\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mwith\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33marg\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mbut\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mwithout\x1b[39;49;00m\x1b[31m ' "\x1b[39;49;00m\x1b[33mcompleter\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " "\x1b[39;49;00m[\x1b[33m'\x1b[39;49;00m\x1b[33mnone\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m]],\x1b[37m\x1b[39;49;00m\n" '\x1b[37m ' "\x1b[39;49;00m[\x1b[33m'\x1b[39;49;00m\x1b[33muser\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " "\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33mTakes\x1b[39;49;00m\x1b[31m " '\x1b[39;49;00m\x1b[33ma\x1b[39;49;00m\x1b[31m ' "\x1b[39;49;00m\x1b[33musername\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " "\x1b[39;49;00m[\x1b[33m'\x1b[39;49;00m\x1b[33muser\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m]],\x1b[37m\x1b[39;49;00m\n" '\x1b[37m ' "\x1b[39;49;00m[\x1b[33m'\x1b[39;49;00m\x1b[33mcheck\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " "\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33mSpecify\x1b[39;49;00m\x1b[31m " '\x1b[39;49;00m\x1b[33mfile\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mname\x1b[39;49;00m\x1b[31m ' "\x1b[39;49;00m\x1b[33mconversions\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " "\x1b[39;49;00m[\x1b[33m'\x1b[39;49;00m\x1b[33mchoices\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m,\x1b[37m " '\x1b[39;49;00m{\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' "\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33mrelaxed\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m:\x1b[37m " '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mconvert\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mto\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mlowercase\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mbefore\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mlookup\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' "\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33mstrict\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m:\x1b[37m " '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mno\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33mconversion\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m}]]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m]]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'The first argument is the separator used for delimiting the ' 'key-value pairs.\n' '\n' 'The second argument is the separator used for delimiting the value ' 'from the key.\n' '\n' 'The third argument is a list of key-description-completer ' 'definitions, like:\n' '\n' ' `[ [, , ], ... ]`\n' '\n' 'If a key does not take an argument, use `null` as completer.\n' '\n' 'If a key does take an argument but cannot be completed, use ' "`['none']` as completer.\n", 'long_colored': 'The first argument is the separator used for delimiting the ' 'key-value pairs.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'The second argument is the separator used for delimiting ' 'the value from the key.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'The third argument is a list of key-description-completer ' 'definitions, like:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' ' \x1b[33m`[ [, , ], ... ' ']`\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'If a key does not take an argument, use ' '\x1b[33m`null`\x1b[39;49;00m as ' 'completer.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'If a key does take an argument but cannot be completed, use ' "\x1b[33m`['none']`\x1b[39;49;00m as " 'completer.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --key-value-list flag,user=\n' 'bin braph\n' 'colord dbus\n' 'dhcpcd git\n', 'short': 'Complete a comma-separated list of key=value pairs'}, {'also': {'directory_list': 'For completing a comma-separated list of ' 'directories', 'file_list': 'For completing a comma-separated list of files', 'key_value_list': 'For completing a comma-separated list of ' 'key=value pairs', 'value_list': 'For completing a comma-separated list of values'}, 'category': 'meta', 'command': 'list', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--user-list"]\n' ' complete: ["list", ["user"]]\n' ' - option_strings: ["--option-list"]\n' ' complete: ["list", ["choices", ["setuid", "async", ' '"block"]], {"separator": ":"}]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--user-list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mlist\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33muser\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--option-list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mlist\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mchoices\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33msetuid\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33masync\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mblock\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]],\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mseparator\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33m:\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m}]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'The separator can be changed by adding `{"separator": ...}`.\n' '\n' 'By default, duplicate values are not offered for completion. This ' 'can be changed by adding `{"duplicates": true}`.\n', 'long_colored': 'The separator can be changed by adding ' '\x1b[33m`{"separator": ' '...}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'By default, duplicate values are not offered for ' 'completion. This can be changed by adding ' '\x1b[33m`{"duplicates": ' 'true}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --user-list=avahi,daemon,\n' 'bin braph\n' 'colord dbus\n' 'dhcpcd git\n', 'short': 'Complete a comma-separated list of any completer'}, {'also': None, 'category': 'bonus', 'command': 'locale', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--locale"]\n' ' complete: ["locale"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--locale\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mlocale\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --locale=\n' 'C C.UTF-8 de_DE de_DE@euro de_DE.iso88591 ' 'de_DE.iso885915@euro\n' 'de_DE.UTF-8 deutsch en_US en_US.iso88591 en_US.UTF-8 german ' 'POSIX\n', 'short': 'Complete a locale'}, {'also': None, 'category': 'bonus', 'command': 'login_shell', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--login-shell"]\n' ' complete: ["login_shell"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--login-shell\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mlogin_shell\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --login-shell=\n' '/bin/bash /bin/sh /usr/bin/fish /usr/bin/sh\n' '[...]\n', 'short': 'Complete a login shell'}, {'also': None, 'category': 'basic', 'command': 'mime_file', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--image"]\n' ' complete: ["mime_file", \'image/\']\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--image\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mmime_file\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' "\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33mimage/\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n", 'implemented': None, 'long': 'This completer takes an extended regex passed to `grep -E` to ' 'filter the results.\n', 'long_colored': 'This completer takes an extended regex passed to ' '\x1b[33m`grep -E`\x1b[39;49;00m to filter the ' 'results.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --image=\ndir1/ dir2/ img.png img.jpg\n', 'short': "Complete a file based on it's MIME-type"}, {'also': None, 'category': 'bonus', 'command': 'mountpoint', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--mountpoint"]\n' ' complete: ["mountpoint"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--mountpoint\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mmountpoint\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --mountpoint=\n' '/ /boot /home /proc /run /sys /tmp\n' '[...]\n', 'short': 'Complete a mountpoint'}, {'also': {'ip_address': 'For completing an ip address'}, 'category': 'bonus', 'command': 'net_interface', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--net-interface"]\n' ' complete: ["net_interface"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--net-interface\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mnet_interface\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --net-interface=\n' 'eno1 enp1s0 lo wlo1 wlp2s0\n' '[...]\n', 'short': 'Complete a network interface'}, {'also': None, 'category': 'meta', 'command': 'none', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--none"]\n' ' complete: ["none"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--none\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mnone\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'Disables autocompletion for an option but still marks it as ' 'requiring an argument.\n' '\n' 'Without specifying `complete`, the option would not take an ' 'argument.\n', 'long_colored': 'Disables autocompletion for an option but still marks it as ' 'requiring an argument.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'Without specifying \x1b[33m`complete`\x1b[39;49;00m, the ' 'option would not take an argument.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --none=\n\n', 'short': 'No completion, but specifies that an argument is required'}, {'also': {'process': 'For completing a process name'}, 'category': 'basic', 'command': 'pid', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--pid"]\n' ' complete: ["pid"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--pid\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mpid\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --pid=\n' '1 13 166 19 254 31 45\n' '1006 133315 166441 19042 26 32 46\n' '10150 1392 166442 195962 27 33 4609\n', 'short': 'Complete a PID'}, {'also': None, 'category': 'meta', 'command': 'prefix', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--prefix"]\n' ' complete: ["prefix", "input:", [\'file\']]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--prefix\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mprefix\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33minput:\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' "\x1b[39;49;00m[\x1b[33m'\x1b[39;49;00m\x1b[33mfile\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m]]\x1b[37m\x1b[39;49;00m\n", 'implemented': None, 'long': 'The first argument is the prefix that should be used.\n' '\n' 'The second argument is a completer.\n', 'long_colored': 'The first argument is the prefix that should be ' 'used.\x1b[37m\x1b[39;49;00m\n' '\x1b[37m\x1b[39;49;00m\n' 'The second argument is a completer.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --prefix=\n' '~ > example --prefix=input:\n' '\n' '~ > example --prefix=input:\n' '~ > example --prefix=input:file1.txt\n', 'short': 'Prefix completion by a string'}, {'also': {'pid': 'For completing a PID'}, 'category': 'basic', 'command': 'process', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--process"]\n' ' complete: ["process"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--process\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mprocess\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --process=s\n' 'scsi_eh_0 scsi_eh_1 scsi_eh_2 scsi_eh_3 ' 'scsi_eh_4\n' 'scsi_eh_5 sh sudo syndaemon ' 'systemd\n' 'systemd-journald systemd-logind systemd-udevd\n', 'short': 'Complete a process name'}, {'also': None, 'category': 'basic', 'command': 'range', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--range-1"]\n' ' complete: ["range", 1, 9]\n' ' - option_strings: ["--range-2"]\n' ' complete: ["range", 1, 9, 2]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--range-1\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mrange\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[31m1\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[31m9\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--range-2\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mrange\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[31m1\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[31m9\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[31m2\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --range-1=\n' '1 2 3 4 5 6 7 8 9\n' '~ > example --range-2=\n' '1 3 5 7 9\n', 'short': 'Complete a range of integers'}, {'also': None, 'category': 'basic', 'command': 'service', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--service"]\n' ' complete: ["service"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--service\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mservice\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --service=\nTODO\n[...]\n', 'short': 'Complete a SystemD service'}, {'also': None, 'category': 'basic', 'command': 'signal', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--signal"]\n' ' complete: ["signal"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--signal\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33msignal\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --signal=\n' 'ABRT -- Process abort signal\n' 'ALRM -- Alarm clock\n' 'BUS -- Access to an undefined portion of a memory object\n' 'CHLD -- Child process terminated, stopped, or continued\n' 'CONT -- Continue executing, if stopped\n' 'FPE -- Erroneous arithmetic operation\n' 'HUP -- Hangup\n' 'ILL -- Illegal instruction\n' 'INT -- Terminal interrupt signal\n' '[...]\n', 'short': 'Complete signal names'}, {'also': None, 'category': 'bonus', 'command': 'timezone', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--timezone"]\n' ' complete: ["timezone"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--timezone\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mtimezone\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --timezone=Europe/B\n' 'Belfast Belgrade Berlin Bratislava\n' 'Brussels Bucharest Budapest Busingen\n', 'short': 'Complete a timezone'}, {'also': {'user': 'For completing a user name'}, 'category': 'basic', 'command': 'uid', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--uid"]\n' ' complete: ["uid"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--uid\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33muid\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --uid=\n' '0 -- root\n' '1000 -- braph\n' '102 -- polkitd\n' '133 -- rtkit\n' '14 -- ftp\n' '1 -- bin\n' '2 -- daemon\n' '33 -- http\n' '65534 -- nobody\n' '[...]\n', 'short': 'Complete a user id'}, {'also': {'uid': 'For completing a user id'}, 'category': 'basic', 'command': 'user', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--user"]\n' ' complete: ["user"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--user\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33muser\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --user=\n' 'avahi bin braph\n' 'colord daemon dbus\n' 'dhcpcd ftp git\n' '[...]\n', 'short': 'Complete a username'}, {'also': {'key_value_list': 'For completing a comma-separated list of ' 'key=value pairs', 'list': 'For completing a comma-separated list of any completer'}, 'category': 'basic', 'command': 'value_list', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--value-list-1"]\n' ' complete: ["value_list", {"values": ["exec", "noexec"]}]\n' ' - option_strings: ["--value-list-2"]\n' ' complete: ["value_list", {"values": {"one": "Description ' '1", "two": "Description 2"}}]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--value-list-1\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mvalue_list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mvalues\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mexec\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mnoexec\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]}]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--value-list-2\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mvalue_list\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mvalues\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m{\x1b[33m"\x1b[39;49;00m\x1b[33mone\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mDescription\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33m1\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m,\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mtwo\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mDescription\x1b[39;49;00m\x1b[31m ' '\x1b[39;49;00m\x1b[33m2\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m}}]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': 'Complete one or more items from a list of items. Similar to `mount ' '-o`.\n' ' \n' 'Arguments are supplied by adding `{"values": ...}`.\n' ' \n' 'A separator can be supplied by adding `{"separator": ...}` (the ' 'default is `","`).\n' ' \n' 'By default, duplicate values are not offered for completion. This ' 'can be changed by adding `{"duplicates": true}`.\n', 'long_colored': 'Complete one or more items from a list of items. Similar to ' '\x1b[33m`mount -o`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'Arguments are supplied by adding \x1b[33m`{"values": ' '...}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'A separator can be supplied by adding ' '\x1b[33m`{"separator": ...}`\x1b[39;49;00m (the default is ' '\x1b[33m`","`\x1b[39;49;00m).\x1b[37m\x1b[39;49;00m\n' ' \x1b[37m\x1b[39;49;00m\n' 'By default, duplicate values are not offered for ' 'completion. This can be changed by adding ' '\x1b[33m`{"duplicates": ' 'true}`\x1b[39;49;00m.\x1b[37m\x1b[39;49;00m\n', 'notes': [], 'output': '~ > example --value-list-1=\n' 'exec noexec\n' '~ > example --value-list-1=exec,\n' 'noexec\n' '~ > example --value-list-2=\n' 'one -- Description 1\n' 'two -- Description 2\n', 'short': 'Complete a comma-separated list of values'}, {'also': {'environment': 'For completing an environment variable'}, 'category': 'basic', 'command': 'variable', 'definition': 'prog: "example"\n' 'options:\n' ' - option_strings: ["--variable"]\n' ' complete: ["variable"]\n', 'definition_colored': '\x1b[94mprog\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[33mexample\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n' '\x1b[94moptions\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n' '\x1b[37m \x1b[39;49;00m-\x1b[37m ' '\x1b[39;49;00m\x1b[94moption_strings\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33m--variable\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n' '\x1b[37m ' '\x1b[39;49;00m\x1b[94mcomplete\x1b[39;49;00m:\x1b[37m ' '\x1b[39;49;00m[\x1b[33m"\x1b[39;49;00m\x1b[33mvariable\x1b[39;49;00m\x1b[33m"\x1b[39;49;00m]\x1b[37m\x1b[39;49;00m\n', 'implemented': None, 'long': None, 'long_colored': None, 'notes': [], 'output': '~ > example --variable=HO\nHOME HOSTNAME HOSTTYPE\n', 'short': 'Complete a shell variable name'}] class TerminalFormatter: '''Format string for terminals.''' def __init__(self, use_colors, tab_width): self.use_colors = use_colors self.tab_width = tab_width self.tab = ' ' * tab_width def bold(self, s): '''Make bold string.''' if not self.use_colors: return s return f"\033[1m{s}\033[0m" def underline(self, s): '''Make underline string.''' if not self.use_colors: return s return f"\033[4m{s}\033[0m" def _make_command_section(command, formatter): r = formatter.bold('COMMAND') r += f'\n{formatter.tab}{command["command"]}' return r def _make_description_section(command, formatter): r = formatter.bold('DESCRIPTION') r += f'\n{formatter.tab}{command["short"]}' if command['long']: r += '\n\n' if formatter.use_colors: r += indent(command['long_colored'].strip(), formatter.tab_width) else: r += indent(command['long'].strip(), formatter.tab_width) return r def _make_example_section(command, formatter): r = formatter.bold('EXAMPLE') r += '\n' if formatter.use_colors: r += indent(command['definition_colored'].strip(), formatter.tab_width) else: r += indent(command['definition'].strip(), formatter.tab_width) return r def _make_output_section(command, formatter): r = formatter.bold('OUTPUT') r += '\n' r += indent(command['output'].strip(), formatter.tab_width) return r def _make_notes_sectiong(notes, formatter): if not notes: return '' r = [formatter.bold('NOTES')] for note in notes: note = f'{formatter.tab}- {note}' r.append(note) return '\n'.join(r) def _make_see_also_section(command, formatter): if not command['also']: return '' r = formatter.bold('SEE ALSO') r += '\n' for also_cmd, also_desc in command['also'].items(): r += f'{formatter.tab}{formatter.bold(also_cmd)}: {also_desc}\n' return r.rstrip() def print_help_for_command(name, use_colors): '''Print a manual like help for command `name`.''' command = None for cmd in COMMANDS: if cmd['command'] == name: command = cmd break if not command: raise CrazyError("Command not found: %s" % name) formatter = TerminalFormatter(use_colors, 4) notes = list(command['notes']) if command['implemented']: r = 'This completer is currently only implemented in ' r += command['implemented'][0] if len(command['implemented']) > 1: r += ' and ' r += command['implemented'][1] notes.append(r) r = [ _make_command_section(command, formatter), _make_description_section(command, formatter), _make_example_section(command, formatter), _make_output_section(command, formatter), _make_notes_sectiong(notes, formatter), _make_see_also_section(command, formatter), ] print() print('\n\n'.join(l for l in r if l)) print() def print_help_topic(topic, use_colors): '''Print help for `topic`.''' try: print_help_for_command(topic, use_colors) except CrazyError: commands = [cmd['command'] for cmd in COMMANDS] print('Topic not found') print('') print('Available completers:') print(indent(join_with_wrap(' ', '\n', 40, commands), 4)) crazy-complete-crazy-complete-bc5afec/crazy_complete/messages.py000066400000000000000000000144611520061347500254500ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Translations.''' import os import locale # flake8: noqa: E501 # pylint: disable=line-too-long # pylint: disable=missing-function-docstring def normalize_lang(lang_code): '''Normalize locale code to short language like `de`.''' if not lang_code: return 'en' return lang_code.split('_')[0].split('.')[0].lower() def get_lang(): '''Get system language.''' lang = ( os.environ.get("LANGUAGE") or os.environ.get("LC_ALL") or os.environ.get("LC_MESSAGES") or os.environ.get("LANG") ) if not lang: locale.setlocale(locale.LC_ALL, "") lang, _ = locale.getlocale() if not lang: lang = 'en' return normalize_lang(lang) LANG = get_lang() def command_arg_without_command(): if LANG == 'de': return 'Vervollständiger `command_arg` benötigt einen vorher definierten `command` Vervollständiger' return 'Completer `command_arg` requires a previously defined `command` completer' def completer_not_allowed_in(completer1, completer2): if LANG == 'de': return f'Vervollständiger `{completer1}` nicht in `{completer2}` erlaubt' return f'Completer `{completer1}` not allowed in `{completer2}`' def completer_not_allowed_in_option(completer): if LANG == 'de': return f'Vervollständiger `{completer}` nicht erlaubt in einer Option' return f'Completer `{completer}` not allowed inside an option' def completer_requires_repeatable(completer): if LANG == 'de': return f'Vervollständiger `{completer}` benötigt `repeatable=True`' return f'Completer `{completer}` requires `repeatable=true`' def dict_cannot_be_empty(): if LANG == 'de': return 'Dictionary darf nicht leer sein' return 'Dictionary cannot be empty' def integer_cannot_be_zero(): if LANG == 'de': return 'Ganzzahl darf nicht Null sein' return 'Integer cannot be zero' def integer_must_be_greater_than_zero(): if LANG == 'de': return 'Ganzzahl muss größer als Null sein' return 'Integer must be greater than zero' def invalid_type_expected_types(types): if LANG == 'de': return f'Ungültiger Typ. Erwartet: {types}' return f'Invalid type. Expected: {types}' def invalid_value(): if LANG == 'de': return 'Ungültiger Wert' return 'Invalid value' def invalid_value_expected_values(values): if LANG == 'de': return f'Ungültiger Wert. Erwartet: {values}' return f'Invalid value. Expected: {values}' def list_cannot_be_empty(): if LANG == 'de': return 'Liste darf nicht leer sein' return 'List cannot be empty' def list_must_contain_at_least_two_items(): if LANG == 'de': return 'Liste muss mindestens zwei Elemente enthalten' return 'List must contain at least two items' def list_must_contain_exact_three_items(): if LANG == 'de': return 'Liste muss exakt 3 Elemente enthalten' return 'List must contain exactly three items' def missing_arg(): if LANG == 'de': return 'Fehlendes benötigtes Argument' return 'Missing required argument' def missing_definition_of_program(program): if LANG == 'de': return f'Fehlende Definition des Programms `{program}`' return f'Missing definition of program `{program}`' def multiple_definition_of_program(program): if LANG == 'de': return f'Mehrfache Definition des Programms `{program}`' return f'Multiple definition of program `{program}`' def mutually_exclusive_parameters(parameters): if LANG == 'de': return f'Parameter schließen sich gegenseitig aus: {parameters}' return f'Parameters are mutually exclusive: {parameters}' def no_programs_defined(): if LANG == 'de': return 'Keine Programme definiert' return 'No programs defined' def not_a_variable_name(): if LANG == 'de': return 'Ungültiger Variablenname' return 'Not a valid variable name' def not_an_absolute_path(): if LANG == 'de': return 'Kein absoluter Pfad' return 'Not an absolute path' def not_an_extended_regex(): if LANG == 'de': return 'Ungültiger erweiterter regulärer Ausdruck' return 'Not an extended regular expression' def parameter_not_allowed_in_subcommand(parameter): if LANG == 'de': return f'Parameter `{parameter}` ist nicht erlaub einem Unterkommando' return f'Parameter `{parameter}` not allowed in subcommand' def parameter_requires_parameter(parameter1, parameter2): if LANG == 'de': return f'Parameter `{parameter1}` benötigt `{parameter2}`' return f'Parameter `{parameter1}` requires `{parameter2}`' def positional_argument_after_repeatable(): if LANG == 'de': return 'Positionsargument nach einem wiederholbaren Positionsargument gefunden' return 'Positional argument found after a repeatable positional argument' def repeatable_with_subcommands(): if LANG == 'de': return 'Wiederholbare Positionsargumente dürfen nicht zusammen mit Unterkommandos definiert werden' return 'Repeatable positional arguments cannot be used together with subcommands' def single_character_expected(): if LANG == 'de': return 'Ungültige Länge. Einzelner Buchstabe erwartet' return 'Invalid length. Single character expected' def string_cannot_be_empty(): if LANG == 'de': return 'Zeichenkette darf nicht leer sein' return 'String cannot be empty' def string_cannot_contain_space(): if LANG == 'de': return 'Zeichenkette darf keine Leerzeichen enthalten' return 'String cannot contain spaces' def too_many_arguments(): if LANG == 'de': return 'Zu viele Argumente' return 'Too many arguments' def too_many_programs_defined(): if LANG == 'de': return 'Zu viele Programme definiert' return 'Too many programs defined' def too_many_repeatable_positionals(): if LANG == 'de': return 'Zu viele wiederholbare Positionsargumente' return 'Too many repeatable positionals' def unknown_completer(): if LANG == 'de': return 'Unbekannter Vervollständiger' return 'Unknown completer' def unknown_parameter(): if LANG == 'de': return 'Unbekannter Parameter' return 'Unknown parameter' crazy-complete-crazy-complete-bc5afec/crazy_complete/output.py000066400000000000000000000047501520061347500252010ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Classes for generating file output.''' _GENERATION_NOTICE = '''\ # This script was generated by crazy-complete. # crazy-complete: A tool that creates robust and reliable autocompletion scripts for Bash, Fish and Zsh. # For more information, visit: https://github.com/crazy-complete/crazy-complete''' class Output: '''Class for generating output.''' def __init__(self, config, helpers): self.config = config self.helpers = helpers self.output = [] def add(self, string): '''Add to output.''' self.output.append(string) def extend(self, strings): '''Add many to output.''' self.output.extend(strings) def add_as_block(self): '''Return a helper context manager for adding a multi-line block.''' class _Block: def __init__(self, parent): self.parent = parent self.lines = [] def add(self, line): '''Add a single line to the current block.''' self.lines.append(line) def extend(self, lines): '''Add multiple lines to current block.''' self.lines.extend(lines) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): block_text = "\n".join(self.lines) self.parent.output.append(block_text) return _Block(self) def add_generation_notice(self): '''Add the generation notice.''' self.add(_GENERATION_NOTICE) def add_comments(self): '''Add additioanl comments.''' if self.config.comments: self.add('\n'.join(f'# {c}' for c in self.config.comments)) def add_included_files(self): '''Add included files.''' for file in self.config.include_files: with open(file, 'r', encoding='utf-8') as fh: self.add(fh.read().strip()) def add_helper_functions_code(self): '''Add helper functions.''' for code in self.helpers.get_used_functions_code(): self.add(code) def add_vim_modeline(self, shell): '''Add the vim modeline.''' if self.config.vim_modeline: vim = 'vim' self.add(f'# {vim}: ft={shell} ts=2 sts=2 sw=2 et') def get(self): '''Return the output.''' return '\n\n'.join(filter(None, self.output)) crazy-complete-crazy-complete-bc5afec/crazy_complete/paths.py000066400000000000000000000036731520061347500247630ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth ''' This module provides functions to retrieve file paths for autocompletion scripts for Bash, Fish, and Zsh. ''' import subprocess from . import utils # pylint: disable=broad-exception-caught # pylint: disable=broad-exception-raised def _pkg_config(args): ''' Call the `pkg-config` program with `args` and check the return code. If everything is fine, return the output as string. ''' command = ['pkg-config'] + args try: result = subprocess.run( command, stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False) except FileNotFoundError as exc: raise Exception('Program `pkg-config` not found') from exc if result.returncode == 0: return result.stdout.strip() raise Exception('Command `%s` failed: %s' % ( ' '.join(command), result.stderr.strip())) def get_bash_completion_file(program_name): '''Get the path for a Bash completion file.''' directory = '/usr/share/bash-completion/completions' try: directory = _pkg_config(['--variable=completionsdir', 'bash-completion']) except Exception as e: utils.warn(e) return f'{directory}/{program_name}' def get_fish_completion_file(program_name): '''Get the path for a Fish completion file.''' directory = '/usr/share/fish/vendor_completions.d' try: directory = _pkg_config(['--variable=completionsdir', 'fish']) except Exception as e: utils.warn(e) return f'{directory}/{program_name}.fish' def get_zsh_completion_file(program_name): '''Get the path for a Zsh completion file.''' # There is '/usr/share/zsh/vendor-completions', but that directory is not # in the default $fpath of zsh. directory = '/usr/share/zsh/site-functions' return f'{directory}/_{program_name}' crazy-complete-crazy-complete-bc5afec/crazy_complete/pattern.py000066400000000000000000000307401520061347500253140ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains functions for transforming bash glob patterns.''' from .string_stream import StringStream class PatternBase: '''Base class for glob patterns.''' def to_regex(self): '''Make a regular expression.''' raise NotImplementedError def to_zsh_glob(self): '''Make a Zsh glob.''' raise NotImplementedError class Literal(PatternBase): '''Holds a literal string.''' REGEX_ESCAPE_CHARS = '.?*+|{}[]()' def __init__(self, string): self.string = string def __str__(self): return self.string def __repr__(self): return self.string def to_regex(self): string = self.string for c in Literal.REGEX_ESCAPE_CHARS: string = string.replace(c, f'\\{c}') return string def to_zsh_glob(self): string = '' for c in self.string: if c in ('[', ']', '?', '*', '"', ' ', '\\'): string += f"'{c}'" elif c == "'": string += f'"{c}"' else: string += c return string class Star(PatternBase): '''Represents a star glob (*).''' def to_regex(self): return '.*' def to_zsh_glob(self): return '*' class Question(PatternBase): '''Represents a question glob (?).''' def to_regex(self): return '.' def to_zsh_glob(self): return '?' class CharClass(PatternBase): '''Represents a character class.''' REGEX_ESCAPE_CHARS = r'^\.[]' def __init__(self, chars, negated): self.chars = chars self.negated = negated def to_regex(self): chars = self.chars for c in CharClass.REGEX_ESCAPE_CHARS: chars = chars.replace(c, f'\\{c}') if self.negated: return f'[^{chars}]' return f'[{chars}]' def to_zsh_glob(self): chars = '' for c in self.chars: if c in ('[', ']', '!'): chars += f"'{c}'" else: chars += c if self.negated: return f'[!{chars}]' return f'[{chars}]' class ExtGlob(PatternBase): '''Holds an extglob: @(), *(), ?(), +(), !().''' def __init__(self, type_, alternatives): self.type = type_ self.alternatives = alternatives def to_regex(self): r = [] for alternative in self.alternatives: r.append(alternative.to_regex()) p = '(%s)' % '|'.join(r) if self.type == '@': return p if self.type == '*': return p + '*' if self.type == '+': return p + '+' if self.type == '?': return p + '?' if self.type == '!': raise ValueError('Negated glob !(...) not supported in regex') raise AssertionError("Not reached") def to_zsh_glob(self): r = [] for alternative in self.alternatives: r.append(alternative.to_zsh_glob()) p = '(%s)' % '|'.join(r) if self.type == '@': return p if self.type in ('*', '+', '?', '!'): raise ValueError(f'Extended glob of form {self.type}(...) not supported in zsh glob') raise AssertionError("Not reached") class PatternList(PatternBase): '''Holds a list of patterns.''' def __init__(self): self.tokens = [] def to_regex(self): return ''.join(t.to_regex() for t in self.tokens) def to_zsh_glob(self): return ''.join(t.to_zsh_glob() for t in self.tokens) def append(self, token): '''Add a token.''' self.tokens.append(token) EXTGLOB_TOKENS = ('@(', '*(', '+(', '?(', '!(', '+(') EXTGLOB_TOKENS_PRE = ('@', '*', '+', '?', '!', '+') class GlobLexer(StringStream): '''Class for splitting a pattern into tokens.''' def __init__(self, pattern): super().__init__(pattern) self.tokens = [] def parse(self): '''Split string into tokens.''' tokens = [] while True: try: tokens.append(self._parse_token()) except IndexError: return tokens def _parse_token(self): c = self.get() if c in EXTGLOB_TOKENS_PRE and self.peek() == '(': return f'{c}{self.get()}' if c in ('[', ']', ')', '?', '!', '*', '|'): return c if c == '"': return Literal(self.parse_shell_double_quote(in_quotes=True)) if c == "'": return Literal(self.parse_shell_single_quote(in_quotes=True)) literal = c while True: c = self.peek() if c is None: break if c in EXTGLOB_TOKENS_PRE and self.peek(1) == '(': break if c in ('*', '?', '[', ']', ')', '|', '"', "'"): break literal += c self.get() return Literal(literal) class GlobParser: '''Parses tokens from GlobLexer.''' def __init__(self, tokens): self.tokens = tokens self.i = 0 def get(self): '''Return a token and advance position.''' token = self.tokens[self.i] self.i += 1 return token def peek(self): '''Return current token without advancing the position.''' try: return self.tokens[self.i] except IndexError: return None def have(self): '''Return True if there are any tokens left.''' return self.i < len(self.tokens) def parse(self): '''Parse tokens and return a PatternList object.''' root = PatternList() while self.have(): root.append(self._parse_token()) return root def _parse_char_class(self): chars = '' negated = False try: token = self.get() except IndexError as e: raise ValueError('Unclosed character class') from e if token == '!': negated = True elif token == ']': raise ValueError('Empty character class') else: chars += str(token) while True: try: token = self.get() except IndexError as e: raise ValueError('Unclosed character class') from e if token == ']': return CharClass(chars, negated) chars += str(token) def _parse_ext_glob(self, token): type_ = token[0] alternatives = [PatternList()] while True: token = self.peek() if token == ')': self.get() return ExtGlob(type_, alternatives) if token == '|': self.get() alternatives.append(PatternList()) elif token is None: raise ValueError('Unclosed extglob') else: alternatives[-1].append(self._parse_token()) def _parse_token(self): token = self.get() if token == '[': return self._parse_char_class() if token == '*': return Star() if token == '?': return Question() if token in EXTGLOB_TOKENS: return self._parse_ext_glob(token) return token def bash_glob_to_regex(pattern): '''Transform a bash glob to regex.''' tokens = GlobLexer(pattern).parse() tokens = GlobParser(tokens).parse() return tokens.to_regex() def bash_glob_to_zsh_glob(pattern): '''Transform a bash glob to zsh glob.''' tokens = GlobLexer(pattern).parse() tokens = GlobParser(tokens).parse() return tokens.to_zsh_glob() def test(): '''Tests.''' def do_test(num, bash_pattern, expected_regex, expected_zsh_glob): '''Do a test case.''' try: regex_result = bash_glob_to_regex(bash_pattern) except ValueError: regex_result = ValueError try: zsh_glob_result = bash_glob_to_zsh_glob(bash_pattern) except ValueError: zsh_glob_result = ValueError if regex_result != expected_regex: print('Test %d (regex):' % num) print('Having: %s' % regex_result) print('Expected: %s' % expected_regex) raise AssertionError('Test failed') if zsh_glob_result != expected_zsh_glob: print('Test %d (zsh glob):' % num) print('Having: %s' % zsh_glob_result) print('Expected: %s' % expected_zsh_glob) raise AssertionError('Test failed') tests = [ # Literals (0, r'a', r'a', r'a'), (1, r'abc', r'abc', r'abc'), (3, r'"abc"', r'abc', r'abc'), (4, r"'abc'", r'abc', r'abc'), (5, r"'a'b'c'", r'abc', r'abc'), (6, r'.', r'\.', r'.'), (7, r'+', r'\+', r'+'), (8, r'@', r'@', r'@'), (9, r'"["', r'\[', r"'['"), (10, r"'['", r'\[', r"'['"), (11, r'"]"', r'\]', r"']'"), (12, r"']'", r'\]', r"']'"), (13, r"'*'", r'\*', r"'*'"), (14, r"'?'", r'\?', r"'?'"), # Simple globbing (15, r'*', r'.*', r'*'), (16, r'?', r'.', r'?'), (17, r'???', r'...', r'???'), # Literals + simple globbing (18, r'*abc*', r'.*abc.*', r'*abc*'), (19, r'*.*', r'.*\..*', r'*.*'), (20, r'*+*', r'.*\+.*', r'*+*'), # Character classes (21, r'[abc]', r'[abc]', r'[abc]'), (22, r'[.]', r'[\.]', r'[.]'), (23, r'[+]', r'[+]', r'[+]'), (24, r'[}]', r'[}]', r'[}]'), (25, r'[{]', r'[{]', r'[{]'), (26, r'[*]', r'[*]', r'[*]'), (27, r'[(]', r'[(]', r'[(]'), (28, r'[)]', r'[)]', r'[)]'), (29, r'["["]', r'[\[]', r"['[']"), (30, r'["]"]', r'[\]]', r"[']']"), # Extglob @ (31, r'@(foo)', r'(foo)', r'(foo)'), (32, r'@(foo|bar)', r'(foo|bar)', r'(foo|bar)'), (33, r'@(|foo)', r'(|foo)', r'(|foo)'), # Extglob * (34, r'*(foo|bar)', r'(foo|bar)*', ValueError), (35, r'*(foo)', r'(foo)*', ValueError), (36, r'*(|foo)', r'(|foo)*', ValueError), # Extglob + (37, r'+(foo|bar)', r'(foo|bar)+', ValueError), (38, r'+(foo)', r'(foo)+', ValueError), (39, r'+(|foo)', r'(|foo)+', ValueError), # Extglob ? (40, r'?(foo|bar)', r'(foo|bar)?', ValueError), (41, r'?(foo)', r'(foo)?', ValueError), (42, r'?(|foo)', r'(|foo)?', ValueError), # Extglob ! (43, r'!(foo|bar)', ValueError, ValueError), # Nested (44, r'@([abc]|foo)', r'([abc]|foo)', r'([abc]|foo)'), (45, r'@(@(foo|bar))', r'((foo|bar))', r'((foo|bar))'), (46, r'@(@("foo"|bar))', r'((foo|bar))', r'((foo|bar))'), (47, r'@(?|bar)', r'(.|bar)', r'(?|bar)'), (48, r'@(*baz|bar)', r'(.*baz|bar)', r'(*baz|bar)'), # Syntax errors (70, r'@(foo', ValueError, ValueError), (71, r'[abc', ValueError, ValueError), (71, r'"abc', ValueError, ValueError), (71, r"'abc", ValueError, ValueError), ] for case in tests: do_test(*case) crazy-complete-crazy-complete-bc5afec/crazy_complete/preprocessor.py000066400000000000000000000030671520061347500263670ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Contains code for preprocessing text.''' def preprocess(string, defines): '''Simple preprocessor function with #ifdef, #else, and #endif support.''' defines = set(defines) output = [] stack = [] # stack of booleans: is this block currently active? lines = string.splitlines(keepends=True) for line in lines: stripped_line = line.lstrip() if stripped_line.startswith("#ifdef"): define = stripped_line.split()[1] active = define in defines stack.append(active) elif stripped_line.startswith("#else"): if not stack: raise SyntaxError("#else without #ifdef") # flip only the current (top of stack) stack[-1] = not stack[-1] elif stripped_line.startswith("#endif"): if not stack: raise SyntaxError("#endif without #ifdef") stack.pop() else: # output only if all enclosing blocks are active if not stack or all(stack): output.append(line) if stack: raise SyntaxError("Unclosed #ifdef") return ''.join(output) def _test(): string = """\ Lorem Ipsum #ifdef DEBUG This is debug code. #ifdef FOO This is foo code. #else This is foo else code. #endif #endif Dolor Sit Amet\ """ defines = ["DEBUG"] result = preprocess(string, defines) print(result) if __name__ == '__main__': _test() crazy-complete-crazy-complete-bc5afec/crazy_complete/scheme_validator.py000066400000000000000000000742141520061347500271540ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for validating the structure of a command line definition.''' from types import NoneType from .cli import ExtendedBool, is_extended_bool from .when import parse_when from .errors import CrazyError, CrazySchemaValidationError from .pattern import bash_glob_to_regex, bash_glob_to_zsh_glob from .value_with_trace import ValueWithTrace from .str_utils import ( contains_space, is_empty_or_whitespace, is_valid_option_string, is_valid_variable_name, is_valid_extended_regex, validate_prog) from . import messages as m _error = CrazySchemaValidationError # ============================================================================= # Helper functions for validating # ============================================================================= class Context: '''Class for providing context to validation.''' # pylint: disable=too-few-public-methods TYPE_OPTION = 0 TYPE_POSITIONAL = 1 def __init__(self): self.definition = None self.option = None self.positional = None self.type = None self.trace = None class Arguments: '''Class for accessing arguments.''' def __init__(self, args): self.args = args self.index = 0 def get_required_arg(self, name): '''Return a required argument, else raise an exception.''' if self.index < len(self.args.value): arg = self.args.value[self.index] self.index += 1 return arg raise _error('%s: %s' % (m.missing_arg(), name), self.args) def get_optional_arg(self, default=None): '''Return an optional arg, else return a default.''' if self.index < len(self.args.value): arg = self.args.value[self.index] self.index += 1 return arg return ValueWithTrace(default, '', 1, 1) def require_no_more(self): '''Raise an exception if there are any arguments left.''' if self.index < len(self.args.value): raise _error(m.too_many_arguments(), self.args) def _has_set(dictionary, key): return (key in dictionary.value and dictionary.value[key].value is not None) def _is_true(dictionary, key): return (key in dictionary.value and dictionary.value[key].value is True) def _check_type(value, types, parameter_name=None): if not isinstance(value.value, types): types_strings = [] for t in types: types_strings.append({ str: 'string', int: 'integer', float: 'float', list: 'list', dict: 'dictionary', bool: 'boolean', NoneType: 'none'}[t]) types_string = '|'.join(types_strings) msg = m.invalid_type_expected_types(types_string) if parameter_name is not None: msg = f'{parameter_name}: {msg}' raise _error(msg, value) def _check_dictionary(dictionary, rules): _check_type(dictionary, (dict,)) for key, value in dictionary.value.items(): if key.value not in rules: raise _error('%s: %s' % (m.unknown_parameter(), key.value), key) _check_type(value, rules[key.value][1], key.value) if rules[key.value][2]: rules[key.value][2](value, key.value) for key, rule in rules.items(): if rule[0] is True and key not in dictionary.value: raise _error('%s: %s' % (m.missing_arg(), key), dictionary) def _check_regex(value, parameter): if not is_valid_extended_regex(value.value): raise _error('%s: %s' % (parameter, m.not_an_extended_regex()), value) def _check_char(value, parameter): if len(value.value) != 1: msg = '%s: %s' % (parameter, m.single_character_expected()) raise _error(msg, value) def _check_variable_name(value, parameter): if not is_valid_variable_name(value.value): raise _error('%s: %s' % (parameter, m.not_a_variable_name()), value) def _check_non_empty_string(value, parameter): if is_empty_or_whitespace(value.value): raise _error('%s: %s' % (parameter, m.string_cannot_be_empty()), value) def _check_no_spaces(value, parameter): if contains_space(value.value): msg = '%s: %s' % (parameter, m.string_cannot_contain_space()) raise _error(msg, value) def _check_non_empty_list(value, parameter): if len(value.value) == 0: raise _error('%s: %s' % (parameter, m.list_cannot_be_empty()), value) def _check_non_empty_dict(value, parameter): if len(value.value) == 0: raise _error('%s: %s' % (parameter, m.dict_cannot_be_empty()), value) def _check_extended_bool(value, parameter): if not is_extended_bool(value.value): expected = 'true, false or `%s`' % ExtendedBool.INHERIT msg = '%s: %s' % (parameter, m.invalid_value_expected_values(expected)) raise _error(msg, value) # ============================================================================= # Actual validation code # ============================================================================= def _check_when(value): try: parse_when(value.value) except CrazyError as e: raise _error(f'when: {e}', value) from e def _check_void(_ctxt, arguments): arguments.require_no_more() def _check_none(ctxt, arguments): arguments.require_no_more() if ctxt.trace and ctxt.trace[-1] in ('combine', 'list', 'prefix'): msg = m.completer_not_allowed_in('none', ctxt.trace[-1]) raise _error(msg, arguments.args) def _check_choices(_ctxt, arguments): choices = arguments.get_required_arg('values') _check_type(choices, (list, dict), 'values') arguments.require_no_more() if isinstance(choices.value, dict): for i, (value, desc) in enumerate(choices.value.items()): _check_type(value, (str, int), f'values[{i}]: item') _check_type(desc, (str, int), f'values[{i}]: description') elif isinstance(choices.value, list): for i, value in enumerate(choices.value): _check_type(value, (str, int), f'values[{i}]') def _check_command(_ctxt, arguments): opts = arguments.get_optional_arg({}) _check_type(opts, (dict,), 'options') arguments.require_no_more() _check_dictionary(opts, { 'path': (False, (str,), _check_non_empty_string), 'path_append': (False, (str,), _check_non_empty_string), 'path_prepend': (False, (str,), _check_non_empty_string), }) path = opts.value.get('path', None) append = opts.value.get('path_append', None) prepend = opts.value.get('path_prepend', None) if path and append: msg = m.mutually_exclusive_parameters('path, path_append') raise _error(msg, opts) if path and prepend: msg = m.mutually_exclusive_parameters('path, path_prepend') raise _error(msg, opts) def _check_command_arg(ctxt, arguments): arguments.require_no_more() if ctxt.trace and ctxt.trace[-1] in ('combine', 'list', 'key_value_list'): msg = m.completer_not_allowed_in('command_arg', ctxt.trace[-1]) raise _error(msg, arguments.args) if ctxt.type != Context.TYPE_POSITIONAL: msg = m.completer_not_allowed_in_option('command_arg') raise _error(msg, arguments.args) if (not _has_set(ctxt.positional, 'repeatable') or not ctxt.positional.value['repeatable'].value): msg = m.completer_requires_repeatable('command_arg') raise _error(msg, ctxt.positional) def command_is_previous_to_command_arg(positional): return ( _has_set(positional, 'complete') and positional.value['complete'].value[0] == 'command' and positional.value['number'].value + 1 == ctxt.positional.value['number'].value ) positionals = ctxt.definition.value['positionals'].value if not any(filter(command_is_previous_to_command_arg, positionals)): msg = m.command_arg_without_command() raise _error(msg, ctxt.positional) def _check_filedir(_ctxt, arguments, with_file_opts=False, with_list_opts=False): options = arguments.get_optional_arg({}) _check_type(options, (dict,), 'options') arguments.require_no_more() spec = {'directory': (False, (str,), _check_non_empty_string)} if with_file_opts: spec['extensions'] = (False, (list,), _check_non_empty_list) spec['fuzzy'] = (False, (bool,), None) spec['ignore_globs'] = (False, (list,), _check_non_empty_list) if with_list_opts: spec['separator'] = (False, (str,), _check_char) spec['duplicates'] = (False, (bool,), None) _check_dictionary(options, spec) if _has_set(options, 'extensions'): for i, extension in enumerate(options.value['extensions'].value): _check_type(extension, (str,), f'extensions[{i}]') _check_non_empty_string(extension, f'extensions[{i}]') _check_no_spaces(extension, f'extensions[{i}]') if _has_set(options, 'ignore_globs'): for pattern in options.value['ignore_globs'].value: _check_type(pattern, (str,), 'pattern') _check_non_empty_string(pattern, 'pattern') try: bash_glob_to_regex(pattern.value) bash_glob_to_zsh_glob(pattern.value) except ValueError as e: raise _error('%s: %s' % ('pattern', e), pattern) from e def _check_file(ctxt, arguments): _check_filedir(ctxt, arguments, with_file_opts=True) def _check_directory(ctxt, arguments): _check_filedir(ctxt, arguments) def _check_file_list(ctxt, arguments): _check_filedir(ctxt, arguments, with_file_opts=True, with_list_opts=True) def _check_directory_list(ctxt, arguments): _check_filedir(ctxt, arguments, with_list_opts=True) def _check_mime_file(_ctxt, arguments): pattern = arguments.get_required_arg("pattern") _check_type(pattern, (str,), "pattern") arguments.require_no_more() _check_regex(pattern, 'pattern') def _check_range(_ctxt, arguments): start = arguments.get_required_arg("start") stop = arguments.get_required_arg("stop") step = arguments.get_optional_arg(1) arguments.require_no_more() _check_type(start, (int,), "start") _check_type(stop, (int,), "stop") _check_type(step, (int,), "step") if step.value == 0: msg = '%s: %s' % ('step', m.integer_cannot_be_zero()) raise _error(msg, step) if step.value > 0 and start.value > stop.value: msg = f"start > stop: {start.value} > {stop.value} (step={step.value})" raise _error(msg, step) if step.value < 0 and stop.value > start.value: msg = f"stop > start: {stop.value} > {start.value} (step={step.value})" raise _error(msg, step) def _check_exec(_ctxt, arguments): command = arguments.get_required_arg("command") _check_type(command, (str,), "command") arguments.require_no_more() def _check_value_list(_ctxt, arguments): options = arguments.get_required_arg('options') _check_type(options, (dict,), 'options') arguments.require_no_more() _check_dictionary(options, { 'values': (True, (list, dict), None), 'separator': (False, (str,), _check_char), 'duplicates': (False, (bool,), None), }) values = options.value['values'] if isinstance(values.value, dict): _check_non_empty_dict(values, 'values') for i, (item, desc) in enumerate(values.value.items()): _check_type(item, (str,), f'values[{i}]: item') _check_type(desc, (str,), f'values[{i}]: description') else: _check_non_empty_list(values, 'values') for i, value in enumerate(values.value): _check_type(value, (str,), f'values[{i}]: item') def _check_key_value_list(ctxt, arguments): pair_separator = arguments.get_required_arg('pair_separator') value_separator = arguments.get_required_arg('value_separator') values = arguments.get_required_arg('values') arguments.require_no_more() _check_type(pair_separator, (str,), 'pair_separator') _check_type(value_separator, (str,), 'value_separator') _check_type(values, (list,), 'values') ctxt.trace.append('key_value_list') _check_char(pair_separator, 'pair_separator') _check_char(value_separator, 'value_separator') _check_non_empty_list(values, 'values') for i, compldef in enumerate(values.value): index = f'definition[{i}]' _check_type(compldef, (list,), index) if len(compldef.value) != 3: msg = '%s: %s' % (index, m.list_must_contain_exact_three_items()) raise _error(msg, compldef) key = compldef.value[0] description = compldef.value[1] complete = compldef.value[2] _check_type(key, (str,), index + ': key') _check_type(description, (str, NoneType), index + ': description') _check_type(complete, (list, NoneType), index + ': completer') _check_non_empty_string(key, index + ': key') _check_no_spaces(key, index + ': key') if complete.value is None: continue _check_complete(ctxt, complete) def _check_combine(ctxt, arguments): commands = arguments.get_required_arg('commands') arguments.require_no_more() _check_type(commands, (list,), 'commands') _check_non_empty_list(commands, 'commands') if len(commands.value) == 1: msg = '%s: %s' % ('commands', m.list_must_contain_at_least_two_items()) raise _error(msg, commands) ctxt.trace.append('combine') for i, completer in enumerate(commands.value): _check_type(completer, (list,), f'command[{i}]') _check_complete(ctxt, completer) def _check_list(ctxt, arguments): command = arguments.get_required_arg('command') options = arguments.get_optional_arg({}) arguments.require_no_more() _check_type(command, (list,), 'command') _check_type(options, (dict,), 'options') _check_dictionary(options, { 'separator': (False, (str,), _check_char), 'duplicates': (False, (bool,), None), }) ctxt.trace.append('list') _check_complete(ctxt, command) def _check_prefix(ctxt, arguments): prefix = arguments.get_required_arg('prefix') command = arguments.get_required_arg('command') arguments.require_no_more() _check_type(prefix, (str,), 'prefix') _check_type(command, (list,), 'command') ctxt.trace.append('prefix') _check_complete(ctxt, command) def _check_history(_ctxt, arguments): pattern = arguments.get_required_arg("pattern") _check_type(pattern, (str,), "pattern") arguments.require_no_more() _check_regex(pattern, 'pattern') def _check_date(_ctxt, arguments): format_ = arguments.get_required_arg("format") _check_type(format_, (str,), "format") arguments.require_no_more() _check_non_empty_string(format_, 'format') def _check_ip_address(_ctxt, arguments): type_ = arguments.get_optional_arg('all') arguments.require_no_more() _check_type(type_, (str,), "type") if type_.value not in ('ipv4', 'ipv6', 'all'): allowed = 'ipv4, ipv6, all' msg = '%s: %s' % ('type', m.invalid_value_expected_values(allowed)) raise _error(msg, type_) def _check_integer_float(_ctxt, arguments, types): options = arguments.get_optional_arg({}) arguments.require_no_more() _check_type(options, (dict,), "options") _check_dictionary(options, { 'min': (False, types, None), 'max': (False, types, None), 'help': (False, (str,), _check_non_empty_string), 'suffixes': (False, (dict,), _check_non_empty_dict), }) if _has_set(options, 'min') and _has_set(options, 'max'): min_ = options.value['min'].value max_ = options.value['max'].value if min_ > max_: raise _error(f'min > max ({min_} > {max_})', options) if _has_set(options, 'suffixes'): for suffix, description in options.value['suffixes'].value.items(): _check_type(suffix, (str,), "suffix") _check_type(description, (str,), "description") _check_non_empty_string(suffix, "suffix") _check_non_empty_string(description, "description") def _check_integer(ctxt, arguments): _check_integer_float(ctxt, arguments, (int,)) def _check_float(ctxt, arguments): _check_integer_float(ctxt, arguments, (int, float)) _COMMANDS = { 'alsa_card': _check_void, 'alsa_device': _check_void, 'charset': _check_void, 'choices': _check_choices, 'combine': _check_combine, 'command': _check_command, 'command_arg': _check_command_arg, 'commandline_string': _check_void, 'date': _check_date, 'date_format': _check_void, 'directory': _check_directory, 'directory_list': _check_directory_list, 'environment': _check_void, 'exec': _check_exec, 'exec_fast': _check_exec, 'exec_internal': _check_exec, 'file': _check_file, 'file_list': _check_file_list, 'filesystem_type': _check_void, 'float': _check_float, 'gid': _check_void, 'group': _check_void, 'history': _check_history, 'hostname': _check_void, 'integer': _check_integer, 'ip_address': _check_ip_address, 'key_value_list': _check_key_value_list, 'list': _check_list, 'locale': _check_void, 'login_shell': _check_void, 'mime_file': _check_mime_file, 'mountpoint': _check_void, 'net_interface': _check_void, 'none': _check_none, 'pid': _check_void, 'prefix': _check_prefix, 'process': _check_void, 'range': _check_range, 'service': _check_void, 'signal': _check_void, 'timezone': _check_void, 'uid': _check_void, 'user': _check_void, 'value_list': _check_value_list, 'variable': _check_void, } def _check_complete(ctxt, args): arguments = Arguments(args) cmd = arguments.get_required_arg('command') _check_type(cmd, (str,), 'command') if cmd.value not in _COMMANDS: msg = '%s: %s' % (m.unknown_completer(), cmd.value) raise _error(msg, cmd) try: _COMMANDS[cmd.value](ctxt, arguments) except _error as e: msg = '%s: %s' % (cmd.value, e.message) raise _error(msg, e.value_with_trace) from e def _check_positionals_repeatable0(definition_tree, definition): if not _has_set(definition, 'positionals'): return positionals = definition.value['positionals'].value repeatable_number = None for positional in sorted(positionals, key=lambda p: p.value['number'].value): repeatable = False if _has_set(positional, 'repeatable'): repeatable = positional.value['repeatable'].value positional_number = positional.value['number'].value if repeatable: if repeatable_number is not None and repeatable_number != positional_number: msg = m.too_many_repeatable_positionals() raise _error(msg, positional) repeatable_number = positional_number elif repeatable_number is not None and positional_number > repeatable_number: msg = m.positional_argument_after_repeatable() raise _error(msg, positional) node = definition_tree.get_definition(definition.value['prog'].value) if repeatable_number is not None and len(node.subcommands) > 0: msg = m.repeatable_with_subcommands() raise _error(msg, definition) def _check_positionals_repeatable(definition_tree, definition): try: _check_positionals_repeatable0(definition_tree, definition) except _error as e: prog = definition.value['prog'].value msg = '%s: %s' % (prog, e.message) raise _error(msg, e.value_with_trace) from e def _check_option0(ctxt, option): chkbool = _check_extended_bool _check_dictionary(option, { 'option_strings': (True, (list,), None), 'metavar': (False, (str, NoneType), None), 'help': (False, (str, NoneType), None), 'optional_arg': (False, (bool, NoneType), None), 'group': (False, (str, NoneType), None), 'groups': (False, (list, NoneType), None), 'repeatable': (False, (bool, str, NoneType), chkbool), 'final': (False, (bool, NoneType), None), 'hidden': (False, (bool, NoneType), None), 'nosort': (False, (bool, NoneType), None), 'complete': (False, (list, NoneType), None), 'when': (False, (str, NoneType), None), 'capture': (False, (str, NoneType), None), 'long_opt_arg_sep': (False, (str,), None), }) option_strings = option.value['option_strings'] _check_non_empty_list(option_strings, 'option_strings') for option_string in option_strings.value: _check_type(option_string, (str,), "option_string") if not is_valid_option_string(option_string.value): msg = m.invalid_value() raise _error(msg, option_string) if _has_set(option, 'metavar') and not _has_set(option, 'complete'): msg = m.parameter_requires_parameter('metavar', 'complete') raise _error(msg, option) if _is_true(option, 'optional_arg') and not _has_set(option, 'complete'): msg = m.parameter_requires_parameter('optional_arg', 'complete') raise _error(msg, option) if _is_true(option, 'repeatable') and _is_true(option, 'hidden'): msg = m.mutually_exclusive_parameters('repeatable, hidden') raise _error(msg, option) if _has_set(option, 'group') and _has_set(option, 'groups'): msg = m.mutually_exclusive_parameters('groups, group') raise _error(msg, option) if _has_set(option, 'groups'): for group in option.value['groups'].value: _check_type(group, (str,), "group") if _has_set(option, 'complete'): ctxt.type = Context.TYPE_OPTION ctxt.option = option ctxt.trace = [] _check_complete(ctxt, option.value['complete']) if _has_set(option, 'when'): _check_when(option.value['when']) if _has_set(option, 'capture'): _check_variable_name(option.value['capture'], 'capture') if _has_set(option, 'long_opt_arg_sep'): if option.value['long_opt_arg_sep'] not in ( 'space', 'equals', 'both', ExtendedBool.INHERIT): expected = f'space, equals, both, {ExtendedBool.INHERIT}' msg = m.invalid_value_expected_values(expected) raise _error(msg, option.value['long_opt_arg_sep']) def _check_option(ctxt, option): try: _check_option0(ctxt, option) except _error as e: try: option_strings = option.value['option_strings'].value option_strings = '|'.join([o.value for o in option_strings]) except (KeyError, TypeError): raise e from None msg = '%s: %s' % (option_strings, e.message) raise _error(msg, e.value_with_trace) from e def _check_positional0(ctxt, positional): _check_dictionary(positional, { 'number': (True, (int,), None), 'metavar': (False, (str, NoneType), None), 'help': (False, (str, NoneType), None), 'repeatable': (False, (bool, NoneType), None), 'nosort': (False, (bool, NoneType), None), 'complete': (False, (list, NoneType), None), 'when': (False, (str, NoneType), None), 'capture': (False, (str, NoneType), None), }) if positional.value['number'].value < 1: msg = m.integer_must_be_greater_than_zero() raise _error('%s: %s' % ('number', msg), positional.value['number']) if _has_set(positional, 'complete'): ctxt.type = Context.TYPE_POSITIONAL ctxt.positional = positional ctxt.trace = [] _check_complete(ctxt, positional.value['complete']) if _has_set(positional, 'when'): _check_when(positional.value['when']) if _has_set(positional, 'capture'): _check_variable_name(positional.value['capture'], 'capture') def _check_positional(ctxt, positional): try: _check_positional0(ctxt, positional) except _error as e: metavar = 'unknown' try: metavar = 'positional #%d' % positional.value['number'].value except (KeyError, TypeError): pass try: metavar_ = positional.value['metavar'].value assert isinstance(metavar_, str) metavar = metavar_ except (AssertionError, KeyError): pass msg = '%s: %s' % (metavar, e.message) raise _error(msg, e.value_with_trace) from e def _check_commandline_definition0(ctxt, definition): chkbool = _check_extended_bool _check_dictionary(definition, { 'prog': (True, (str,), None), 'help': (False, (str, NoneType), None), 'aliases': (False, (list, NoneType), None), 'wraps': (False, (str, NoneType), None), 'abbreviate_commands': (False, (bool, str, NoneType), chkbool), 'abbreviate_options': (False, (bool, str, NoneType), chkbool), 'inherit_options': (False, (bool, str, NoneType), chkbool), 'options': (False, (list, NoneType), None), 'positionals': (False, (list, NoneType), None), }) try: validate_prog(definition.value['prog'].value) except CrazyError as e: raise _error(f'prog: {e}', definition.value['prog']) from None if _has_set(definition, 'aliases'): for alias in definition.value['aliases'].value: _check_type(alias, (str,), 'alias') _check_non_empty_string(alias, 'alias') _check_no_spaces(alias, 'alias') if _has_set(definition, 'wraps'): wraps = definition.value['wraps'] _check_non_empty_string(wraps, 'wraps') _check_no_spaces(wraps, 'wraps') if contains_space(definition.value['prog'].value): msg = m.parameter_not_allowed_in_subcommand('wraps') raise _error(msg, definition.value['wraps']) if _has_set(definition, 'options'): for option in definition.value['options'].value: _check_option(ctxt, option) if _has_set(definition, 'positionals'): for positional in definition.value['positionals'].value: _check_positional(ctxt, positional) def _check_commandline_definition(ctxt, definition): try: _check_commandline_definition0(ctxt, definition) except _error as e: try: prog = definition.value['prog'].value except (KeyError, TypeError): prog = 'unknown' msg = '%s: %s' % (prog, e.message) raise _error(msg, e.value_with_trace) from e class DefinitionTree: '''Holds a command-line definition with subcommands.''' def __init__(self, definition): self.definition = definition self.subcommands = {} def add_definition(self, definition): '''Add a definition to subcommands.''' _check_type(definition, (dict,)) if not _has_set(definition, 'prog'): raise _error('%s: %s' % (m.missing_arg(), 'prog'), definition) try: validate_prog(definition.value['prog'].value) except CrazyError as e: raise _error(f'prog: {e}', definition.value['prog']) from None commands = definition.value['prog'].value.split(' ') subcommand = commands.pop(-1) node = self for i, part in enumerate(commands): try: node = node.subcommands[part] except KeyError: prog = ' '.join(commands[0:i+1]) msg = m.missing_definition_of_program(prog) raise _error(msg, definition) from None if subcommand in node.subcommands: prog = definition.value['prog'].value msg = m.multiple_definition_of_program(prog) raise _error(msg, definition) node.subcommands[subcommand] = DefinitionTree(definition) def get_definition(self, prog): '''Get definition by prog.''' commands = prog.split(' ') node = self for part in commands: node = node.subcommands[part] return node def visit(self, callback): '''Call `callback` on each node in the tree.''' callback(self) for subcommand in self.subcommands.values(): callback(subcommand) def get_all(self): '''Return a list of all nodes.''' result = [] self.visit(result.append) return result def get_first_commandline(self): '''Return the first command line in the tree.''' return list(self.subcommands.values())[0] @staticmethod def make_tree(definition_list): '''Make a tree out of a list of definitions.''' root = DefinitionTree(None) for definition in definition_list: root.add_definition(definition) return root def validate(definition_list): '''Validate a list of definitions.''' context = Context() commandline_root = DefinitionTree.make_tree(definition_list) if len(commandline_root.subcommands) == 0: msg = m.no_programs_defined() raise _error(msg, ValueWithTrace(None, '', 1, 1)) if len(commandline_root.subcommands) > 1: value = ValueWithTrace(None, '', 1, 1) progs = list(commandline_root.subcommands.keys()) msg = m.too_many_programs_defined() raise _error('%s: %s' % (msg, progs), value) for cmdline in commandline_root.get_first_commandline().get_all(): context.definition = cmdline.definition _check_commandline_definition(context, cmdline.definition) _check_positionals_repeatable(commandline_root, cmdline.definition) crazy-complete-crazy-complete-bc5afec/crazy_complete/shell.py000066400000000000000000000131111520061347500247370ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Shell utility functions.''' import re from . import utils def make_identifier(string): '''Make `string` a valid shell identifier. This function replaces any dashes '-' with underscores '_', removes any characters that are not letters, digits, or underscores, and ensures that consecutive underscores are replaced with a single underscore. Args: string (str): The input string to be converted into a valid shell identifier. Returns: str: The modified string that is a valid shell identifier. ''' string = string.replace('-', '_') string = re.sub('[^a-zA-Z0-9_]', '', string) string = re.sub('_+', '_', string) if string and string[0] in '0123456789': return '_' + string return string def needs_quote(string): '''Return if string needs quoting.''' return not re.fullmatch('[a-zA-Z0-9_@%+=:,./-]+', string) def quote(string, quote_empty_string=True): '''Quotes a string for safe usage in shell commands or scripts. Args: string (str): The input string to be quoted. quote_empty_string (bool, optional): Determines whether to quote an empty string or not. Defaults to True. Returns: str: The quoted string. ''' if not string and not quote_empty_string: return '' if not needs_quote(string): return string if "'" not in string: return f"'{string}'" if '"' not in string: s = string s = s.replace('\\', '\\\\') s = s.replace('$', '\\$') s = s.replace('`', '\\`') return f'"{s}"' s = string.replace("'", '\'"\'"\'') return f"'{s}'" def join_quoted(arguments, string=' '): '''Joins quoted `arguments` on `string`.''' return string.join(quote(arg) for arg in arguments) class ShellCompleter: '''Base class for argument completion.''' # pylint: disable=missing-function-docstring # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments def complete(self, ctxt, trace, command, *a): if not hasattr(self, command): utils.warn(f"complete: Falling back from `{command}` to `none`") command = 'none' return getattr(self, command)(ctxt, trace, *a) def complete_from_def(self, ctxt, trace, definition): command, *args = definition return self.complete(ctxt, trace, command, *args) def fallback(self, ctxt, trace, from_, to, *a): utils.warn(f"ShellCompleter: Falling back from `{from_}` to `{to}`") return self.complete(ctxt, trace, to, *a) def signal(self, ctxt, trace): signals = { 'ABRT': 'Process abort signal', 'ALRM': 'Alarm clock', 'BUS': 'Access to an undefined portion of a memory object', 'CHLD': 'Child process terminated: stopped: or continued', 'CONT': 'Continue executing: if stopped', 'FPE': 'Erroneous arithmetic operation', 'HUP': 'Hangup', 'ILL': 'Illegal instruction', 'INT': 'Terminal interrupt signal', 'KILL': 'Kill (cannot be caught or ignored)', 'PIPE': 'Write on a pipe with no one to read it', 'QUIT': 'Terminal quit signal', 'SEGV': 'Invalid memory reference', 'STOP': 'Stop executing (cannot be caught or ignored)', 'TERM': 'Termination signal', 'TSTP': 'Terminal stop signal', 'TTIN': 'Background process attempting read', 'TTOU': 'Background process attempting write', 'USR1': 'User-defined signal 1', 'USR2': 'User-defined signal 2', 'POLL': 'Pollable event', 'PROF': 'Profiling timer expired', 'SYS': 'Bad system call', 'TRAP': 'Trace/breakpoint trap', 'XFSZ': 'File size limit exceeded', 'VTALRM': 'Virtual timer expired', 'XCPU': 'CPU time limit exceeded', } return self.complete(ctxt, trace, 'choices', signals) def exec(self, _ctxt, _trace, _command): raise NotImplementedError def list(self, _ctxt, _trace, _command, _opts=None): raise NotImplementedError # ========================================================================= # Aliases # ========================================================================= def file_list(self, ctxt, trace, opts=None): list_opts = { 'separator': opts.pop('separator', ',') if opts else ',', 'duplicates': opts.pop('duplicates', False) if opts else False } return self.list(ctxt, trace, ['file', opts], list_opts) def directory_list(self, ctxt, trace, opts=None): list_opts = { 'separator': opts.pop('separator', ',') if opts else ',', 'duplicates': opts.pop('duplicates', False) if opts else False } return self.list(ctxt, trace, ['directory', opts], list_opts) # ========================================================================= # Bonus # ========================================================================= def login_shell(self, ctxt, trace): return self.exec(ctxt, trace, "command grep -E '^[^#]' /etc/shells") def locale(self, ctxt, trace): return self.exec(ctxt, trace, "command locale -a") def charset(self, ctxt, trace): return self.exec(ctxt, trace, "command locale -m") crazy-complete-crazy-complete-bc5afec/crazy_complete/shell_parser.py000066400000000000000000000075501520061347500263250ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Module for parsing shell-like commands with logical operators.''' from .string_stream import StringStream # pylint: disable=too-few-public-methods class Literal: '''Represents a literal string.''' def __init__(self, string): self.string = string def __str__(self): return self.string def __repr__(self): return f'Literal({self.string!r})' class Command: '''Represents a command.''' def __init__(self): self.args = [] def __repr__(self): return f'Command({self.args!r})' class Lexer(StringStream): '''Lexer class.''' def parse(self): '''Split string into tokens (e.g. logical operators) and literals.''' tokens = [] while True: token = self._parse_token() if token is not None: tokens.append(token) else: return tokens def _parse_token(self): c = self.peek() if c is None: return None if c in ('!', '(', ')'): self.advance(1) return c if c.isspace(): self.advance(1) return self._parse_token() if c == '&': if not self.peek(1) == '&': raise ValueError('Single `&` found') self.advance(2) return '&&' if c == '|': if not self.peek(1) == '|': raise ValueError('Single `|` found') self.advance(2) return '||' return self._parse_literal() def _parse_literal(self): literal = '' while True: c = self.peek() if c is None: return Literal(literal) if c == '"': literal += self.parse_shell_double_quote(in_quotes=False) elif c == "'": literal += self.parse_shell_single_quote(in_quotes=False) elif c.isspace() or c in ('&', '|', '!', '(', ')'): return Literal(literal) else: self.advance(1) literal += c def make_commands(tokens): '''Parse tokens. Input: [Literal("foo"), Literal("bar"), '&&' Literal("baz")] Output: [Command(["foo", "bar"]), '&&', Command(["baz"])] ''' new_tokens = [] current = Command() for token in tokens: if token in ('&&', '||', '!', '(', ')'): if current.args: new_tokens.append(current) current = Command() new_tokens.append(token) else: current.args.append(str(token)) if current.args: new_tokens.append(current) return new_tokens def check_syntax(tokens): '''Checks tokens for syntax errors.''' last = None parentheses = 0 for token in tokens: if token == '(': parentheses += 1 if last not in (None, '&&', '||', '!', '('): raise ValueError("Unexpected `(`") elif token == ')': parentheses -= 1 if parentheses < 0 or last in (None, '(', '&&', '||', '!'): raise ValueError("Unexpected `)`") elif token in ('&&', '||'): if last in (None, '(', '&&', '||', '!'): raise ValueError(f"Unexpected `{token}`") elif token == '!': if last not in (None, '(', '&&', '||', '!'): raise ValueError("Unexpected `!`") last = token if parentheses > 0: raise ValueError("Unclosed `(`") if last is None: raise ValueError("No command found") def parse(string): '''Parse a string and turn it into And/Or/Not/Command objects.''' lex_tokens = Lexer(string).parse() tokens = make_commands(lex_tokens) check_syntax(tokens) return tokens crazy-complete-crazy-complete-bc5afec/crazy_complete/str_utils.py000066400000000000000000000112131520061347500256610ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''String utility functions.''' import re import subprocess from .errors import CrazyError _VALID_OPTION_STRING_RE = re.compile('-[^\\s,]+') _VALID_VARIABLE_RE = re.compile('[a-zA-Z_][a-zA-Z0-9_]*') def contains_space(string): '''Check if string contains space type characters.''' return (' ' in string or '\t' in string or '\n' in string) def is_empty_or_whitespace(string): '''Check if string is empty or whitespace.''' return not string.strip() def is_valid_extended_regex(string): '''Check if string is a valid extended regular expression.''' try: r = subprocess.run( ['grep', '-q', '-E', '--', string], input=b"", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=1, check=False) return r.returncode != 2 except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired): return True def validate_prog(string): '''Validate a program string.''' if is_empty_or_whitespace(string): raise CrazyError('value is empty') if string.startswith(' '): raise CrazyError('begins with space') if string.endswith(' '): raise CrazyError('ends with space') if '\t' in string: raise CrazyError('contains a tabulator') if '\n' in string: raise CrazyError('contains a newline') if ' ' in string: raise CrazyError('contains multiple spaces') def indent(string, num_spaces): '''Indents each line in a string by a specified number of spaces, preserving empty lines. Args: string (str): The input string to be indented. num_spaces (int): The number of spaces to indent each line. Returns: str: The indented string. ''' assert isinstance(string, str), \ f"indent: string: expected str, got {string!r}" assert isinstance(num_spaces, int), \ f"indent: num_spaces: expected int, got {num_spaces!r}" spaces = ' ' * num_spaces lines = string.split('\n') lines = [(spaces + line) if line.strip() else line for line in lines] return '\n'.join(lines) def join_with_wrap(primary_sep, secondary_sep, max_line_length, items, line_prefix=''): ''' Join a list of strings with a primary separator, automatically wrapping to a new line (using the secondary separator) when the current line would exceed max_line_length. Args: primary_sep: String used to separate items on the same line. secondary_sep: String used to separate wrapped lines. max_line_length: Maximum number of characters per line. items: List of strings to join. line_prefix: Prefix each line with a prefix. Returns: str: Joined string with line wrapping ''' lines = [] current_items = [] current_length = len(line_prefix) for item in items: sep_len = len(primary_sep) if current_items else 0 new_length = current_length + sep_len + len(item) if current_items and new_length > max_line_length: # flush current line lines.append(line_prefix + primary_sep.join(current_items)) current_items = [item] current_length = len(line_prefix) + len(item) else: current_items.append(item) current_length = new_length if current_items: lines.append(line_prefix + primary_sep.join(current_items)) return secondary_sep.join(lines) def is_valid_option_string(option_string): '''Check if `option_string` is a valid option string.''' if not _VALID_OPTION_STRING_RE.fullmatch(option_string): return False if option_string == '--': return False return True def is_valid_variable_name(string): '''Check if string is a valid shell variable name.''' return _VALID_VARIABLE_RE.fullmatch(string) def strip_comments(string): '''Strip shell comments from string.''' lines = [] for line in string.split('\n'): if line.lstrip().startswith('#'): continue lines.append(line) return '\n'.join(lines) def strip_double_empty_lines(string): '''Collapse triple newlines into double newlines.''' return string.replace('\n\n\n', '\n\n') def replace_many(string, replacements): ''' Replace multiple substrings in `string` according to a list of (search, replace) tuples. ''' s = string for search, replace in replacements: s = s.replace(search, replace) return s crazy-complete-crazy-complete-bc5afec/crazy_complete/string_stream.py000066400000000000000000000042401520061347500265140ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Module containing code for parsing strings as a stream.''' class StringStream: '''String stream class for reading a string.''' def __init__(self, string): self.s = string self.i = 0 self.length = len(string) def peek(self, seek=0): '''Return character at current position + seek without advancing.''' try: return self.s[self.i + seek] except IndexError: return None def peek_str(self, length=1): '''Return substring of given length from current position.''' return self.s[self.i:length] def get(self): '''Return current character and advance position by one.''' c = self.s[self.i] self.i += 1 return c def have(self): '''Return True if iterator is not at its end.''' return self.i < self.length def advance(self, length): '''Advance the stream by a given length.''' self.i += length def parse_shell_single_quote(self, in_quotes=False): '''Parse a single-quoted shell string.''' if not in_quotes: assert self.get() == "'" string = '' while True: try: c = self.get() except IndexError as e: raise ValueError("Unclosed single quote") from e if c == "'": return string string += c def parse_shell_double_quote(self, in_quotes=False): '''Parse a double-quoted shell string.''' if not in_quotes: assert self.get() == '"' string = '' while True: try: c = self.get() except IndexError as e: raise ValueError("Unclosed double quote") from e if c == '\\': try: string += self.get() except IndexError as e: raise ValueError("Missing character after backslash") from e elif c == '"': return string else: string += c crazy-complete-crazy-complete-bc5afec/crazy_complete/type_utils.py000066400000000000000000000020071520061347500260330ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Type utility functions.''' from types import NoneType from .errors import CrazyTypeError def is_dict_type(obj): '''Return if `obj` is a dictionary type.''' return hasattr(obj, 'items') def is_list_type(obj): '''Return if `obj` is list or tuple.''' return isinstance(obj, (list, tuple)) def validate_type(value, types, parameter_name): '''Check if `value` is one of `types`.''' if not isinstance(value, types): types_strings = [] for t in types: types_strings.append({ str: 'str', int: 'int', float: 'float', list: 'list', tuple: 'tuple', dict: 'dict', bool: 'bool', NoneType: 'None'}[t]) types_string = '|'.join(types_strings) raise CrazyTypeError(parameter_name, types_string, value) crazy-complete-crazy-complete-bc5afec/crazy_complete/utils.py000066400000000000000000000217211520061347500247760ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains utility functions.''' import sys from collections import namedtuple from .type_utils import is_list_type def warn(*a): '''Print a warning.''' print('WARNING:', *a, file=sys.stderr) def print_err(*a): '''Print to STDERR.''' print(*a, file=sys.stderr) class GeneralAbbreviationGenerator: '''A class for generating abbreviations from a list of words. Attributes: min_abbreviated_length (int): The minimum length of an abbreviation. abbreviations (dict): A dictionary mapping each word to its list of abbreviations. min_lengths (dict): A dictionary mapping each word to its minimum abbreviation length. ''' def __init__(self, min_abbreviated_length, words): '''Initialize the GeneralAbbreviationGenerator instance. Args: min_abbreviated_length (int): The minimum length of an abbreviation. words (iterable of str): The list of words from which to generate abbreviations. ''' assert isinstance(min_abbreviated_length, int) assert is_list_type(words) # pylint: disable=cell-var-from-loop self.min_abbreviated_length = min_abbreviated_length self.abbreviations = {} self.min_lengths = {} for word in words: self.abbreviations[word] = [] self.min_lengths[word] = len(word) for length in range(len(word), self.min_abbreviated_length - 1, -1): abbrev = word[0:length] can_abbreviate = True for other_word in filter(lambda w: w != word, words): if other_word.startswith(abbrev): can_abbreviate = False break if can_abbreviate or abbrev == word: self.abbreviations[word].append(abbrev) self.min_lengths[word] = length def get_abbreviations(self, word): '''Get the list of abbreviations for a given word. Args: word (str): The word for which to retrieve abbreviations. Returns: list: A list of abbreviations for the given word. ''' assert isinstance(word, str) return self.abbreviations[word] def get_many_abbreviations(self, words): '''Get the list of abbreviations for multiple words. Args: words (iterable of str): The words for which to retrieve abbreviations. Returns: list: A list of abbreviations for the given words. ''' assert is_list_type(words) r = [] for word in words: r.extend(self.get_abbreviations(word)) return r class OptionAbbreviationGenerator(GeneralAbbreviationGenerator): '''AbbreviationGenerator for abbreviating long and old-style options.''' def __init__(self, words): assert is_list_type(words) words = list(words) for word in words: if word.startswith('--') and len(word) > 3: pass elif word.startswith('-') and len(word) > 2: pass else: raise ValueError('Not a long or old-style option: %r' % word) super().__init__(3, words) class CommandAbbreviationGenerator(GeneralAbbreviationGenerator): '''AbbreviationGenerator for abbreviating commands.''' def __init__(self, words): super().__init__(1, words) class DummyAbbreviationGenerator: '''A dummy abbreviation generator that returns the original word as the abbreviation. This class is used as a placeholder when abbreviation generation is not required. ''' def __init__(self): pass def get_abbreviations(self, word): '''Don't abbreviate, just return the input.''' assert isinstance(word, str) return [word] def get_many_abbreviations(self, words): '''Don't abbreviate, just return the input.''' assert is_list_type(words) return words def get_option_abbreviator(commandline): '''Return an OptionAbbreviationGenerator for options in `commandline`.''' if not commandline.abbreviate_options: return DummyAbbreviationGenerator() options = commandline.get_options(with_parent_options=commandline.inherit_options) option_strings = [] for option in options: option_strings.extend(option.get_long_option_strings()) option_strings.extend(option.get_old_option_strings()) return OptionAbbreviationGenerator(option_strings) def get_all_command_variations(commandline): '''Return all possible names for this command. If `commandline.abbreviate_commands` is True, also return abbreviated forms. ''' if commandline.parent is None: return [commandline.prog] + commandline.aliases if commandline.abbreviate_commands: all_commands = [] for subcommand in commandline.parent.subcommands.subcommands: all_commands.append(subcommand.prog) abbrevs = CommandAbbreviationGenerator(all_commands) else: abbrevs = DummyAbbreviationGenerator() return abbrevs.get_abbreviations(commandline.prog) + commandline.aliases def get_defined_option_types(commandline): '''Return a tuple of defined option types.''' # pylint: disable=too-many-branches short_required = False short_optional = False short_flag = False long_required = False long_optional = False long_flag = False old_required = False old_optional = False old_flag = False positionals = False for cmdline in commandline.get_all_commandlines(): if len(cmdline.get_positionals()) > 0: positionals = True if cmdline.get_subcommands() and len(cmdline.get_subcommands().subcommands) > 0: positionals = True for option in cmdline.options: if option.get_long_option_strings(): if option.has_required_arg(): long_required = True elif option.has_optional_arg(): long_optional = True else: long_flag = True if option.get_old_option_strings(): if option.has_required_arg(): old_required = True elif option.has_optional_arg(): old_optional = True else: old_flag = True if option.get_short_option_strings(): if option.has_required_arg(): short_required = True elif option.has_optional_arg(): short_optional = True else: short_flag = True return namedtuple('Types', ( 'positionals', 'short_required', 'short_optional', 'short_flag', 'short', 'long_required', 'long_optional', 'long_flag', 'long', 'old_required', 'old_optional', 'old_flag', 'old'))( positionals, short_required, short_optional, short_flag, short_required or short_optional or short_flag, long_required, long_optional, long_flag, long_required or long_optional or long_flag, old_required, old_optional, old_flag, old_required or old_optional or old_flag) def get_query_option_strings(commandline, with_parent_options=True): '''Return a string that can be used by {fish,zsh}_query functions. Returns something like: "-f --flag -a= --argument= -o=? --optional=?" ''' r = [] for option in commandline.get_options(with_parent_options=with_parent_options): if option.has_required_arg(): r.extend('%s=' % s for s in option.option_strings) elif option.has_optional_arg(): r.extend('%s=?' % s for s in option.option_strings) else: r.extend(option.option_strings) return ' '.join(r) def is_worth_a_function(commandline): '''Check if a commandline "is worth a function". This means that a commandline has one of: - Subcommands - Positionals (that have meaningful completion) - Options (that aren't --help or --version) ''' if commandline.get_subcommands(): return True for positional in commandline.get_positionals(): if positional.complete[0] == 'none': pass elif positional.complete[0] in ('integer', 'float'): # integer/float without options have no real completion if len(positional.complete) > 1: return True else: return True options = commandline.get_options() options = list(filter(lambda o: '--help' not in o.option_strings and '--version' not in o.option_strings, options)) if len(options) > 0: return True return False crazy-complete-crazy-complete-bc5afec/crazy_complete/value_with_trace.py000066400000000000000000000037641520061347500271720ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Value holder class with line and column information''' class ValueWithTrace: '''Represents a value from a configuration file along with its metadata. This class is used to associate a value from a YAML or JSON file with its source, line number, and column number. It is particularly useful for debugging and error reporting, as it provides precise location information for the value in the original configuration file. Attributes: value (Any): The value extracted from the configuration file. src (str): The source of the configuration file. line (int): The line number of the value in the source. column (int): The column number of the value in the source. ''' def __init__(self, value, source, line, column): self.value = value self.source = source self.line = line self.column = column @staticmethod def from_yaml_event(value, source, event): '''Constructs a `ValueWithTrace` from a YAML event object.''' return ValueWithTrace( value, source, event.start_mark.line + 1, event.start_mark.column + 1) def get_position_string(self): '''Return a string describing the position of the value.''' return f'line {self.line}, column {self.column}' def __hash__(self): return hash(self.value) def __eq__(self, other): return self.value == other def __repr__(self): return f'{self.value!r}' class ValueWithOutTrace: '''Class holding a value without metadata. ''' def __init__(self, value): self.value = value def get_position_string(self): '''Return empty string.''' return '' def __hash__(self): return hash(self.value) def __eq__(self, other): return self.value == other def __repr__(self): return f'{self.value!r}' crazy-complete-crazy-complete-bc5afec/crazy_complete/when.py000066400000000000000000000040061520061347500245740ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Functions for parsing the `when` attribute.''' from . import shell_parser from .errors import CrazyError # pylint: disable=too-few-public-methods class OptionIs: '''Class for holding `option_is`.''' def __init__(self, args): self.options = [] self.values = [] has_end_of_options = False for arg in args: if arg == '--': has_end_of_options = True elif not has_end_of_options: self.options.append(arg) elif has_end_of_options: self.values.append(arg) if not self.options: raise CrazyError('OptionIs: Empty options') if not self.values: raise CrazyError('OptionIs: Empty values') class HasOption: '''Class for holding `has_option`.''' def __init__(self, args): self.options = args if not self.options: raise CrazyError('HasOption: Empty options') def replace_commands(tokens): '''Replaced `shell_parser.Command` objects by condition objects.''' r = [] for token in tokens: if isinstance(token, shell_parser.Command): cmd, *args = token.args if cmd == 'option_is': r.append(OptionIs(args)) elif cmd == 'has_option': r.append(HasOption(args)) else: raise CrazyError(f"Invalid command: {cmd!r}") else: r.append(token) return r def needs_braces(tokens): '''Check if we need braces around our tokens. We don't need braces if: - We only have one token (single command) - We only have two tokens (negated single command) - We don't have an OR inside the tokens ''' return len(tokens) > 2 and '||' in tokens def parse_when(s): '''Parse `when` string and return an object.''' tokens = shell_parser.parse(s) return replace_commands(tokens) crazy-complete-crazy-complete-bc5afec/crazy_complete/yaml_source.py000066400000000000000000000146111520061347500261600ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth """ This module provides functions for creating CommandLine objects from YAML and vice versa. """ import json import yaml from . import dictionary_source from . import scheme_validator from .str_utils import indent from .extended_yaml_parser import ExtendedYAMLParser from .cli import ExtendedBool # pylint: disable=redefined-builtin _INHERIT = ExtendedBool.INHERIT def option_to_yaml(dictionary): '''Convert an option dictionary to YAML.''' option_strings = dictionary['option_strings'] fields = [ # name, default, include-if ('metavar', None, lambda v: v is not None), ('help', None, lambda v: v is not None), ('optional_arg', False, lambda v: v is True), ('groups', None, lambda v: v is not None), ('repeatable', _INHERIT, lambda v: v != _INHERIT), ('final', False, lambda v: v is True), ('hidden', False, lambda v: v is True), ('complete', None, lambda v: v is not None), ('nosort', None, lambda v: v is True), ('when', None, lambda v: v is not None), ('capture', None, lambda v: v is not None), ('long_opt_arg_sep', _INHERIT, lambda v: v != _INHERIT), ] r = [f'- option_strings: {json.dumps(option_strings)}'] for name, default, include_if in fields: value = dictionary.get(name, default) if include_if(value): r.append(f' {name}: {json.dumps(value)}') return '\n'.join(r) def positional_to_yaml(dictionary): '''Convert a positional dictionary to YAML.''' number = dictionary['number'] fields = [ # name, default, include-if ('metavar', None, lambda v: v is not None), ('help', None, lambda v: v is not None), ('repeatable', False, lambda v: v is True), ('complete', None, lambda v: v is not None), ('nosort', None, lambda v: v is True), ('when', None, lambda v: v is not None), ('capture', None, lambda v: v is not None), ] r = [f'- number: {number}'] for name, default, include_if in fields: value = dictionary.get(name, default) if include_if(value): r.append(f' {name}: {json.dumps(value)}') return '\n'.join(r) def to_yaml(dictionary): '''Convert a single dictionary to YAML.''' prog = dictionary['prog'] aliases = dictionary.get('aliases', None) help = dictionary.get('help', None) wraps = dictionary.get('wraps', None) abbreviate_commands = dictionary.get('abbreviate_commands', _INHERIT) abbreviate_options = dictionary.get('abbreviate_options', _INHERIT) inherit_options = dictionary.get('inherit_options', _INHERIT) options = dictionary.get('options', None) positionals = dictionary.get('positionals', None) r = 'prog: %s\n' % json.dumps(prog) if aliases is not None: r += 'aliases: %s\n' % json.dumps(aliases) if help is not None: r += 'help: %s\n' % json.dumps(help) if wraps is not None: r += 'wraps: %s\n' % json.dumps(wraps) if abbreviate_commands != ExtendedBool.INHERIT: r += 'abbreviate_commands: %s\n' % json.dumps(abbreviate_commands) if abbreviate_options != ExtendedBool.INHERIT: r += 'abbreviate_options: %s\n' % json.dumps(abbreviate_options) if inherit_options != ExtendedBool.INHERIT: r += 'inherit_options: %s\n' % json.dumps(inherit_options) if options: r += 'options:\n' for option in options: r += indent(option_to_yaml(option), 2) r += '\n\n' if positionals: r += 'positionals:\n' for positional in positionals: r += indent(positional_to_yaml(positional), 2) r += '\n\n' return r.rstrip() def commandline_to_yaml(commandline): '''Convert a cli.CommandLine object to YAML.''' dictionaries = dictionary_source.commandline_to_dictionaries(commandline) r = [] for dictionary in dictionaries: r.append(to_yaml(dictionary)) return '\n---\n'.join(r) def replace_defines_in_documents(documents): '''Replaces defines in a list of documents. Example: prog: '%defines%' my_completer: ['choices', ['foo', 'bar']] --- prog: 'example' options: - option_strings: ['-o'] complete: 'my_completer' ''' defines = None for document in documents: if not isinstance(document.value, dict): continue if 'prog' in document.value and document.value['prog'].value == '%defines%': defines = document break if defines is None: return documents documents.remove(defines) defines.value.pop('prog') def replace_defines(obj): if isinstance(obj.value, str): return defines.value.get(obj.value, obj) if isinstance(obj.value, list): obj.value = [replace_defines(sub) for sub in obj.value] return obj if isinstance(obj.value, dict): obj.value = {key: replace_defines(val) for key, val in obj.value.items()} return obj return obj new = [] for document in documents: new.append(replace_defines(document)) return new def load_from_file(file): '''Load a YAML file and turn it into a cli.CommandLine object.''' # First, load using normal yaml loader. It gives better error messages # in case of syntax errors. with open(file, 'r', encoding='utf-8') as fh: dictionaries = list(yaml.safe_load_all(fh)) # Then load using extended yaml loader... with open(file, 'r', encoding='utf-8') as fh: content = fh.read() # Validate YAML... parser = ExtendedYAMLParser() parsed = parser.parse(content) parsed = replace_defines_in_documents(parsed) scheme_validator.validate(parsed) # Finally convert the config structure to CommandLine objects return dictionary_source.dictionaries_to_commandline(dictionaries) crazy-complete-crazy-complete-bc5afec/crazy_complete/zsh.py000066400000000000000000000232521520061347500244430ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for generating a Zsh auto completion file.''' from collections import namedtuple, OrderedDict from . import config as config_ from . import generation from . import shell from . import utils from . import zsh_complete from . import zsh_helpers from . import zsh_utils from . import zsh_when from . import zsh_wrapper from .output import Output from .str_utils import indent Arg = namedtuple('Arg', ('option', 'when', 'option_spec')) class ZshQuery: '''Helper class for using the `query` function.''' # pylint: disable=too-few-public-methods def __init__(self, ctxt): self.ctxt = ctxt self.used = False def use(self, command=None): '''Return the function name of query and use a command.''' self.used = True return self.ctxt.helpers.use_function('query', command) class ZshCompletionFunction: '''Class for generating a zsh completion function.''' # pylint: disable=too-many-branches # pylint: disable=too-few-public-methods # pylint: disable=too-many-instance-attributes def __init__(self, ctxt, commandline): self.commandline = commandline self.ctxt = ctxt self.funcname = ctxt.helpers.make_completion_funcname(commandline) self.subcommands = commandline.get_subcommands() self.command_counter = 0 self.completer = zsh_complete.ZshCompleter() self.code = None self.query = ZshQuery(ctxt) self._generate_completion_code() def _complete(self, option, command, *args): context = self.ctxt.get_option_context(self.commandline, option) return self.completer.complete(context, [], command, *args) def _complete_option(self, option): if option.complete: action = self._complete(option, *option.complete).get_action_string() else: action = None option_spec = zsh_utils.make_option_spec( option.option_strings, conflicting_options = option.get_conflicting_option_strings(), description = option.help, complete = option.complete, optional_arg = option.optional_arg, repeatable = option.repeatable, hidden = option.hidden, final = option.final, metavar = option.metavar, action = action, long_opt_arg_sep = option.long_opt_arg_sep ) return Arg(option, option.when, option_spec) def _complete_subcommands(self, positional): choices = positional.get_choices() self.command_counter += 1 spec = zsh_utils.make_positional_spec( positional.get_positional_num(), False, f'command{self.command_counter}', self._complete(positional, 'choices', choices).get_action_string() ) return Arg(positional, None, spec) def _complete_positional(self, positional): spec = zsh_utils.make_positional_spec( positional.get_positional_num(), positional.repeatable, positional.help or positional.metavar or ' ', self._complete(positional, *positional.complete).get_action_string() ) return Arg(positional, positional.when, spec) def _generate_completion_code(self): self.code = OrderedDict() self.code['0-init'] = '' self.code['1-capture'] = '' self.code['2-subcommands'] = '' self.code['3-options'] = '' # We have to call these functions first, because they tell us if # the query function is used. self.code['1-capture'] = self._generate_option_capture() self.code['2-subcommands'] = self._generate_subcommand_call() self.code['3-options'] = self._generate_option_parsing() if self.query.used: r = 'local opts=%s\n' % shell.quote(utils.get_query_option_strings(self.commandline)) r += "local HAVING_OPTIONS=() OPTION_VALUES=() POSITIONALS=() INCOMPLETE_OPTION=''\n" r += '%s init "$opts" "${words[@]}"' % self.query.use() self.code['0-init'] = r def _generate_option_capture(self): local_vars = [] set_cmds = [] for option in self.commandline.options: if option.capture: local_vars.append(option.capture) set_cmds.append('IFS=$"\\n" %s=($(%s get_option %s))' % ( option.capture, self.query.use('get_option'), shell.join_quoted(option.option_strings))) if local_vars: r = 'local %s' % ' '.join(f'{s}=()' for s in local_vars) for cmd in set_cmds: r += '\n%s' % cmd return r return '' def _generate_subcommand_call(self): if not self.subcommands: return '' query = self.query.use('get_positional') positional_num = self.subcommands.get_positional_num() r = 'case "$(%s get_positional %d)" in\n' % (query, positional_num) for subcommand in self.subcommands.subcommands: sub_funcname = self.ctxt.helpers.make_completion_funcname(subcommand) cmds = utils.get_all_command_variations(subcommand) pattern = '|'.join(shell.quote(s) for s in cmds) r += f' {pattern}) {sub_funcname}; return $?;;\n' r += 'esac' return r def _generate_option_parsing(self): args = [] if self.commandline.inherit_options: options = self.commandline.get_options(with_parent_options=True) else: options = self.commandline.get_options() for option in options: args.append(self._complete_option(option)) for cmdline in self.commandline.get_parents(): for option in cmdline.get_positionals(): args.append(self._complete_positional(option)) if cmdline.get_subcommands(): args.append(self._complete_subcommands(cmdline.get_subcommands())) for option in self.commandline.get_positionals(): args.append(self._complete_positional(option)) if self.subcommands: args.append(self._complete_subcommands(self.subcommands)) if self.commandline.wraps: args.append(Arg(None, None, "'::*'")) if not args: return '' args_with_when = [] args_without_when = [] for arg in args: if arg.when is None: args_without_when.append(arg) else: args_with_when.append(arg) r = '' if not args_without_when: r += 'local -a args=()\n' else: r += 'local -a args=(\n' for arg in args_without_when: r += ' %s\n' % arg.option_spec r += ')\n' for arg in args_with_when: when_cmd = zsh_when.generate_when_conditions(self.query, arg.when) r += '%s &&\\\n' % when_cmd r += ' args+=(%s)\n' % arg.option_spec if self.ctxt.config.option_stacking: r += '_arguments -S -s -w "${args[@]}"' else: r += '_arguments -S "${args[@]}"' return r def get_code(self): '''Return the code of the completion function.''' return '%s() {\n%s\n}' % ( self.funcname, indent('\n\n'.join(c for c in self.code.values() if c), 2)) def get_zstyles(commandline, out_list): '''Get zstyle commands for disabling sorting completion suggestions.''' prog = commandline.get_program_name() for option in filter(lambda o: o.nosort, commandline.options): for option_string in option.option_strings: r = f"zstyle ':completion:*:*:{prog}:option{option_string}-*' sort false" out_list.append(r) for positional in filter(lambda p: p.nosort, commandline.positionals): number = positional.number r = f"zstyle ':completion:*:*:{prog}:argument-{number}:*' sort false" out_list.append(r) def generate_completion(commandline, config=None): '''Code for generating a Zsh auto completion file.''' if config is None: config = config_.Config() commandline = generation.enhance_commandline(commandline, config) helpers = zsh_helpers.ZshHelpers(config, commandline.prog) ctxt = generation.GenerationContext(config, helpers) functions = generation.visit_commandlines(ZshCompletionFunction, ctxt, commandline) all_progs = ' '.join([commandline.prog] + commandline.aliases) if helpers.is_used('query'): types = utils.get_defined_option_types(commandline) if types.short: ctxt.helpers.use_function('query', 'short_options') if types.long: ctxt.helpers.use_function('query', 'long_options') if types.old: ctxt.helpers.use_function('query', 'old_options') output = Output(config, helpers) if config.zsh_compdef: output.add('#compdef %s' % all_progs) output.add_generation_notice() output.add_comments() output.add_included_files() completion_func, wrapper_code = zsh_wrapper.generate_wrapper(ctxt, commandline) output.add_helper_functions_code() output.extend(function.get_code() for function in functions) zstyles = [] commandline.visit_commandlines(lambda c: get_zstyles(c, zstyles)) if zstyles: output.add('\n'.join(zstyles)) if wrapper_code: output.add(wrapper_code) if config.zsh_compdef: output.add('%s "$@"' % completion_func) else: output.add('compdef %s %s' % (completion_func, all_progs)) output.add_vim_modeline('zsh') return output.get() crazy-complete-crazy-complete-bc5afec/crazy_complete/zsh_complete.py000066400000000000000000000422301520061347500263300ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains code for completing arguments in Zsh.''' from . import shell from .pattern import bash_glob_to_zsh_glob from .str_utils import join_with_wrap, indent from .zsh_utils import ( escape_colon, escape_square_brackets, make_file_extension_pattern ) from .type_utils import is_dict_type CHOICES_INLINE_THRESHOLD = 80 # The `choices` command can in Zsh be expressed inline in an optspec, like: # (foo bar baz) # or: # (foo:"Foo value" bar:"Bar value" baz:"Baz value) # # This variable defines how big this string can get before a function # is used instead. class ZshCompletionBase: '''Base class for Zsh completions.''' def get_action_string(self): '''Return an action string that can be used in an option spec.''' raise NotImplementedError def get_function(self): '''Return a function that can be used in e.g. `_sequence`.''' raise NotImplementedError class ZshComplFunc(ZshCompletionBase): '''Complete using a function.''' def __init__(self, ctxt, args, needs_braces=False): self.ctxt = ctxt self.args = args self.needs_braces = needs_braces def get_action_string(self): if len(self.args) == 1: cmd = self.args[0] else: cmd = shell.join_quoted(self.args) if self.needs_braces: return shell.quote(f'{{{cmd}}}') return shell.quote(cmd) def get_function(self): if len(self.args) == 1: return self.args[0] code = shell.join_quoted(self.args) funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code) return funcname class ZshCompleteChoices(ZshCompletionBase): '''Complete from a set of words.''' def __init__(self, ctxt, trace, choices): self.ctxt = ctxt self.trace = trace self.choices = choices def _list_action_string(self): items = [str(item) for item in self.choices] quoted = [shell.quote(item) for item in items] action = shell.quote('(%s)' % ' '.join(quoted)) if len(action) <= CHOICES_INLINE_THRESHOLD: return action return self._list_function() def _list_function(self): metavar = shell.quote(self.ctxt.option.metavar or '') quoted = [shell.quote(escape_colon(c)) for c in self.choices] line_length = self.ctxt.config.line_length - 2 code = 'local items=(\n' code += indent(join_with_wrap(' ', '\n', line_length, quoted), 2) code += '\n)\n\n' code += f'_describe -- {metavar} items' funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code) return funcname def _dict_action_string(self): def str0(s): return str(s) if s is not None else '' items = [str0(item) for item in self.choices.keys()] descriptions = [str0(value) for value in self.choices.values()] colon = any(':' in s for s in items + descriptions) quoted = [] for item, desc in zip(items, descriptions): val = shell.quote(item) if desc: val += ':%s' % shell.quote(desc) quoted.append(val) action = shell.quote('((%s))' % ' '.join(quoted)) if not colon and len(action) <= CHOICES_INLINE_THRESHOLD: return action return self._dict_function() def _dict_function(self): metavar = shell.quote(self.ctxt.option.metavar or '') code = 'local items=(\n' for item, desc in self.choices.items(): item = shell.quote(escape_colon(str(item))) if desc: desc = shell.quote(str(desc)) code += f' {item}:{desc}\n' else: code += f' {item}\n' code += ')\n\n' code += f'_describe -- {metavar} items' funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code) return funcname def get_action_string(self): if is_dict_type(self.choices): return self._dict_action_string() return self._list_action_string() def get_function(self): if is_dict_type(self.choices): return self._dict_function() return self._list_function() class ZshCompleteCommand(ZshCompletionBase): '''Complete a command from $PATH.''' def __init__(self, ctxt, opts): self.ctxt = ctxt self.code = None path = None append = None prepend = None if opts: path = opts.get('path', None) append = opts.get('path_append', None) prepend = opts.get('path_prepend', None) if path: self.code = 'local -x PATH=%s' % shell.quote(path) elif append and prepend: append = shell.quote(append) prepend = shell.quote(prepend) self.code = 'local -x PATH=%s:"$PATH":%s' % (prepend, append) elif append: self.code = 'local -x PATH="$PATH":%s' % shell.quote(append) elif prepend: self.code = 'local -x PATH=%s:"$PATH"' % shell.quote(prepend) def get_action_string(self): if not self.code: return '_command_names' code = f'{self.code}\n_command_names' funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code) return funcname def get_function(self): if not self.code: return '_command_names' code = f'{self.code}\n_command_names' funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code) return funcname class ZshCompleteRange(ZshCompletionBase): '''Complete a range of integers.''' def __init__(self, ctxt, start, stop, step): self.ctxt = ctxt self.start = start self.stop = stop self.step = step def get_action_string(self): if self.step == 1: return f"'({{{self.start}..{self.stop}}})'" return f"'({{{self.start}..{self.stop}..{self.step}}})'" def get_function(self): if self.step == 1: code = f"command seq {self.start} {self.stop}" else: code = f"command seq {self.start} {self.step} {self.stop}" code = f'compadd -- $({code})' funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code) return funcname class ZshKeyValueList(ZshComplFunc): '''Used for completing a list of key-value pairs.''' # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments def __init__(self, ctxt, trace, completer, pair_separator, value_separator, values): trace.append('key_value_list') spec = [] for key, desc, complete in values: key = escape_colon(key) if desc: desc = shell.quote('[%s]' % escape_square_brackets(desc)) else: desc = '' if not complete: spec.append(f'{key}{desc}') elif complete[0] == 'none': spec.append(f'{key}{desc}:::') else: compl_obj = completer.complete_from_def(ctxt, trace, complete) action = compl_obj.get_action_string() spec.append(f'{key}{desc}:::{action}') code = '_values -s %s -S %s %s \\\n%s' % ( shell.quote(pair_separator), shell.quote(value_separator), shell.quote(ctxt.option.metavar or ''), indent(' \\\n'.join(spec), 2) ) func = ctxt.helpers.add_dynamic_func(ctxt, code) super().__init__(ctxt, [func], needs_braces=True) class ZshCompleteCombine(ZshCompletionBase): '''Combine multiple completers into one.''' def __init__(self, ctxt, trace, completer, commands): trace.append('combine') completions = [] for command_args in commands: compl_obj = completer.complete_from_def(ctxt, trace, command_args) action_string = compl_obj.get_action_string() completions.append(f'::{action_string}') code = '_alternative \\\n' code += indent(' \\\n'.join(completions), 2) self.func = ctxt.helpers.add_dynamic_func(ctxt, code) def get_function(self): return self.func def get_action_string(self): return shell.quote('{%s}' % self.func) class ZshCompleter(shell.ShellCompleter): '''Code generator for completing arguments in Zsh.''' # pylint: disable=too-many-public-methods # pylint: disable=missing-function-docstring # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments def none(self, ctxt, _trace, *_): return ZshComplFunc(ctxt, [' ']) def integer(self, ctxt, _trace, options=None): args = [] suffixes = [] help_ = ctxt.option.help or ctxt.option.metavar or '' if options: if 'min' in options: args += ['-l', str(options['min'])] if 'max' in options: args += ['-m', str(options['max'])] if 'suffixes' in options: for suffix, description in options['suffixes'].items(): suffixes.append(f'{suffix}:{description}') if 'help' in options: help_ = options['help'] return ZshComplFunc(ctxt, ['_numbers', *args, help_, *suffixes]) def float(self, ctxt, _trace, options=None): args = ['-f'] suffixes = [] help_ = ctxt.option.help or ctxt.option.metavar or '' if options: if 'min' in options: args += ['-l', str(options['min'])] if 'max' in options: args += ['-m', str(options['max'])] if 'suffixes' in options: for suffix, description in options['suffixes'].items(): suffixes.append(f'{suffix}:{description}') if 'help' in options: help_ = options['help'] return ZshComplFunc(ctxt, ['_numbers', *args, help_, *suffixes]) def choices(self, ctxt, trace, choices): return ZshCompleteChoices(ctxt, trace, choices) def command(self, ctxt, _trace, opts=None): return ZshCompleteCommand(ctxt, opts) def directory(self, ctxt, _trace, opts=None): directory = None if opts is None else opts.get('directory', None) if not directory: return ZshComplFunc(ctxt, ['_directories']) if directory.startswith('/'): return ZshComplFunc(ctxt, ['_directories', '-W', directory]) func = ctxt.helpers.use_function('path_files_relative') return ZshComplFunc(ctxt, [func, directory, '-/'], needs_braces=True) def file(self, ctxt, _trace, opts=None): fuzzy = False directory = None extensions = None ignore_globs = None if opts: fuzzy = opts.get('fuzzy', False) directory = opts.get('directory', None) extensions = opts.get('extensions', None) ignore_globs = opts.get('ignore_globs', None) args = [] if extensions: args.extend(['-g', make_file_extension_pattern(extensions, fuzzy)]) if ignore_globs: patterns = map(bash_glob_to_zsh_glob, ignore_globs) args.extend(['-F', '(%s)' % ' '.join(patterns)]) if not directory: return ZshComplFunc(ctxt, ['_files'] + args) if directory.startswith('/'): return ZshComplFunc(ctxt, ['_files', '-W', directory, *args]) func = ctxt.helpers.use_function('path_files_relative') return ZshComplFunc(ctxt, [func, directory, *args], needs_braces=True) def mime_file(self, ctxt, _trace, pattern): func = ctxt.helpers.use_function('mime_file') return ZshComplFunc(ctxt, [func, pattern], needs_braces=True) def group(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_groups']) def hostname(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_hosts']) def pid(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_pids']) def process(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_process_names', '-a']) def range(self, ctxt, _trace, start, stop, step=1): return ZshCompleteRange(ctxt, start, stop, step) def user(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_users']) def variable(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_vars']) def environment(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_parameters', '-g', '*export*']) def exec(self, ctxt, _trace, command): funcname = ctxt.helpers.use_function('exec') return ZshComplFunc(ctxt, [funcname, command], needs_braces=True) def exec_fast(self, ctxt, _trace, command): funcname = ctxt.helpers.use_function('exec') return ZshComplFunc(ctxt, [funcname, command], needs_braces=True) def exec_internal(self, ctxt, _trace, command): return ZshComplFunc(ctxt, [command], needs_braces=True) def value_list(self, ctxt, _trace, opts): desc = ctxt.option.metavar or '' values = opts['values'] separator = opts.get('separator', ',') duplicates = opts.get('duplicates', False) if is_dict_type(values): esc = escape_square_brackets values = ['%s[%s]' % (esc(item), esc(desc)) for item, desc in values.items()] else: values = [escape_square_brackets(i) for i in values] if not duplicates: return ZshComplFunc(ctxt, ['_values', '-s', separator, desc] + values) values_func = ZshComplFunc(ctxt, ['_values', desc] + values).get_function() if separator == ',': return ZshComplFunc(ctxt, ['_sequence', '-d', values_func]) return ZshComplFunc(ctxt, ['_sequence', '-s', separator, '-d', values_func]) def key_value_list(self, ctxt, trace, pair_separator, value_separator, values): return ZshKeyValueList(ctxt, trace, self, pair_separator, value_separator, values) def combine(self, ctxt, trace, commands): return ZshCompleteCombine(ctxt, trace, self, commands) def list(self, ctxt, trace, command, opts=None): separator = opts.get('separator', ',') if opts else ',' duplicates = opts.get('duplicates', False) if opts else False trace.append('list') obj = self.complete_from_def(ctxt, trace, command) func = obj.get_function() args = [] if separator != ',': args.extend(['-s', separator]) if duplicates: args.append('-d') return ZshComplFunc(ctxt, ['_sequence', *args, func]) def history(self, ctxt, _trace, pattern): func = ctxt.helpers.use_function('history') return ZshComplFunc(ctxt, [func, pattern], needs_braces=True) def commandline_string(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_cmdstring']) def command_arg(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_normal']) def date(self, ctxt, _trace, format_): return ZshComplFunc(ctxt, ['_dates', '-f', format_]) def date_format(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_date_formats']) def uid(self, ctxt, _trace): func = ctxt.helpers.use_function('uid_list') return ZshComplFunc(ctxt, [func], needs_braces=True) def gid(self, ctxt, _trace): func = ctxt.helpers.use_function('gid_list') return ZshComplFunc(ctxt, [func], needs_braces=True) def filesystem_type(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_file_systems']) def signal(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_signals']) def prefix(self, ctxt, trace, prefix, command): prefix_func = ctxt.helpers.use_function('prefix') obj = self.complete_from_def(ctxt, trace, command) func = obj.get_function() return ZshComplFunc(ctxt, [prefix_func, prefix, func], needs_braces=True) def ip_address(self, ctxt, _trace, type_='all'): if type_ == 'ipv4': return ZshComplFunc(ctxt, ['_bind_addresses', '-4']) if type_ == 'ipv6': return ZshComplFunc(ctxt, ['_bind_addresses', '-6']) return ZshComplFunc(ctxt, ['_bind_addresses']) # ========================================================================= # Bonus # ========================================================================= def mountpoint(self, ctxt, _trace): func = ctxt.helpers.use_function('mountpoint') return ZshComplFunc(ctxt, [func], needs_braces=True) def net_interface(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_net_interfaces']) def timezone(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_time_zone']) def locale(self, ctxt, _trace): return ZshComplFunc(ctxt, ['_locales']) def charset(self, ctxt, _trace): func = ctxt.helpers.use_function('charset_list') return ZshComplFunc(ctxt, [func], needs_braces=True) def alsa_card(self, ctxt, _trace): func = ctxt.helpers.use_function('alsa_complete_cards') return ZshComplFunc(ctxt, [func], needs_braces=True) def alsa_device(self, ctxt, _trace): func = ctxt.helpers.use_function('alsa_complete_devices') return ZshComplFunc(ctxt, [func], needs_braces=True) crazy-complete-crazy-complete-bc5afec/crazy_complete/zsh_helpers.py000066400000000000000000000300521520061347500261610ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''This module contains helper functions for Zsh.''' from .helpers import GeneralHelpers, ShellFunction _QUERY = ShellFunction('query', r''' # =========================================================================== # # This function is for querying the command line. # # COMMANDS # init # This is the first call you have to make, otherwise the other commands # won't (successfully) work. # # It parses according to and stores results in the # variables POSITIONALS, HAVING_OPTIONS and OPTION_VALUES. # # The first argument is a comma-separated list of options that the parser # should know about. Short options (-o), long options (--option), and # old-style options (-option) are supported. # # If an option takes an argument, it is suffixed by '='. # If an option takes an optional argument, it is suffixed by '=?'. # # get_positional # Prints out the positional argument number NUM (starting from 1) # # has_option [WITH_INCOMPLETE] # Checks if an option given in OPTIONS is passed on commandline. # If an option requires an argument, this command returns true only if the # option includes an argument. If 'WITH_INCOMPLETE' is specified, it also # returns true for options missing their arguments. # # option_is -- # Checks if one option in OPTIONS has a value of VALUES. # # EXAMPLE # local POSITIONALS HAVING_OPTIONS OPTION_VALUES # query init '-f,-a=,-optional=?' program_name -f -optional -a foo bar # query has_option -f # query option_is -a -- foo # # Here, -f is a flag, -a takes an argument, and -optional takes an optional # argument. # # Both queries return true. # # =========================================================================== __zsh_query_contains() { local arg='' key="$1"; shift for arg; do [[ "$key" == "$arg" ]] && return 0; done return 1 } local cmd="$1"; shift case "$cmd" in #ifdef get_positional get_positional) #ifdef DEBUG if (( $# != 1 )); then echo "%FUNCNAME%: get_positional: takes exactly one argument" >&2 return 1 fi if test "$1" -eq 0; then echo "%FUNCNAME%: get_positional: positionals start at 1, not 0!" >&2 return 1 fi #endif printf "%s" "${POSITIONALS[$1]}" return 0;; #endif #ifdef get_option get_option) local i=0 #ifdef DEBUG if (( $# == 0 )); then echo "%FUNCNAME%: get_option: arguments required" >&2 return 1 fi #endif for (( i=0; i <= ${#HAVING_OPTIONS[@]}; ++i )); do local option="${HAVING_OPTIONS[$i]}" if __zsh_query_contains "$option" "$@"; then printf "%s\n" "${OPTION_VALUES[$i]}" fi done return 0;; #endif #ifdef has_option has_option) #ifdef with_incomplete local option='' with_incomplete=0 [[ "$1" == 'WITH_INCOMPLETE' ]] && { with_incomplete=1; shift; } #else local option='' #endif #ifdef DEBUG if (( $# == 0 )); then echo "%FUNCNAME%: has_option: arguments required" >&2 return 1 fi #endif for option in "${HAVING_OPTIONS[@]}"; do __zsh_query_contains "$option" "$@" && return 0 done #ifdef with_incomplete (( with_incomplete )) && \ __zsh_query_contains "$INCOMPLETE_OPTION" "$@" && return 0 #endif return 1;; #endif #ifdef option_is option_is) local i=0 dash_dash_pos=0 cmd_option_is_options=() cmd_option_is_values=() dash_dash_pos=${@[(i)--]} cmd_option_is_options=("${@:1:$((dash_dash_pos - 1))}") cmd_option_is_values=("${@:$((dash_dash_pos + 1))}") #ifdef DEBUG if (( ${#cmd_option_is_options[@]} == 0 )); then echo "%FUNCNAME%: option_is: missing options" >&2 return 1 fi if (( ${#cmd_option_is_values[@]} == 0 )); then echo "%FUNCNAME%: option_is: missing values" >&2 return 1 fi #endif for (( i=${#HAVING_OPTIONS[@]}; i > 0; --i )); do local option="${HAVING_OPTIONS[$i]}" if __zsh_query_contains "$option" "${cmd_option_is_options[@]}"; then local value="${OPTION_VALUES[$i]}" __zsh_query_contains "$value" "${cmd_option_is_values[@]}" && return 0 fi done return 1;; #endif init) local -a options=(${=1}) shift;; *) echo "%FUNCNAME%: argv[1]: invalid command" >&2 return 1;; esac # continuing init... # =========================================================================== # Parsing of available options # =========================================================================== #ifdef old_options local old_opts_with_arg=() old_opts_with_optional_arg=() old_opts_without_arg=() #endif #ifdef long_options local long_opts_with_arg=() long_opts_with_optional_arg=() long_opts_without_arg=() #endif #ifdef short_options local short_opts_with_arg=() short_opts_with_optional_arg=() short_opts_without_arg=() #endif local option='' for option in "${options[@]}"; do case "$option" in #ifdef long_options --?*=) long_opts_with_arg+=("${option%=}");; --?*=\?) long_opts_with_optional_arg+=("${option%=?}");; --?*) long_opts_without_arg+=("$option");; #endif #ifdef short_options -?=) short_opts_with_arg+=("${option%=}");; -?=\?) short_opts_with_optional_arg+=("${option%=?}");; -?) short_opts_without_arg+=("$option");; #endif #ifdef old_options -??*=) old_opts_with_arg+=("${option%=}");; -??*=\?) old_opts_with_optional_arg+=("${option%=?}");; -??*) old_opts_without_arg+=("$option");; #endif #ifdef DEBUG *) echo "%FUNCNAME%: $option: not a valid short, long or oldstyle option" >&2; return 1;; #endif esac done # =========================================================================== # Parsing of command line options # =========================================================================== POSITIONALS=() HAVING_OPTIONS=() OPTION_VALUES=() INCOMPLETE_OPTION='' local args=("${(Q)@}") local argi=2 # argi[1] is program name for ((; argi <= ${#args[@]}; ++argi)); do local arg="${args[$argi]}" local have_trailing_arg=$(test $argi -lt $# && echo true || echo false) case "$arg" in --) POSITIONALS+=("${@:$((argi + 1))}") break;; --*=*) HAVING_OPTIONS+=("${arg%%=*}") OPTION_VALUES+=("${arg#*=}");; --*) #ifdef long_options if __zsh_query_contains "$arg" "${long_opts_with_arg[@]}"; then if $have_trailing_arg; then HAVING_OPTIONS+=("$arg") OPTION_VALUES+=("${args[$((++argi))]}") #ifdef with_incomplete else INCOMPLETE_OPTION="$arg" #endif fi else HAVING_OPTIONS+=("$arg") OPTION_VALUES+=("") fi #endif ;; -?*) # ignore '-' #ifdef old_options if [[ "$arg" == *=* ]]; then local option="${arg%%=*}" value="${arg#*=}" if __zsh_query_contains "$option" "${old_opts_with_arg[@]}" "${old_opts_with_optional_arg[@]}"; then HAVING_OPTIONS+=("$option") OPTION_VALUES+=("$value") continue fi elif __zsh_query_contains "$arg" "${old_opts_with_arg[@]}"; then if $have_trailing_arg; then HAVING_OPTIONS+=("$arg") OPTION_VALUES+=("${args[$((++argi))]}") #ifdef with_incomplete else INCOMPLETE_OPTION="$arg" #endif fi continue elif __zsh_query_contains "$arg" "${old_opts_without_arg[@]}" "${old_opts_with_optional_arg[@]}"; then HAVING_OPTIONS+=("$arg") OPTION_VALUES+=("") continue fi #endif #ifdef short_options local i=1 arg_length=${#arg} for ((; i < arg_length; ++i)); do local option="-${arg:$i:1}" local trailing_chars="${arg:$((i+1))}" if __zsh_query_contains "$option" "${short_opts_without_arg[@]}"; then HAVING_OPTIONS+=("$option") OPTION_VALUES+=("") elif __zsh_query_contains "$option" "${short_opts_with_arg[@]}"; then if [[ -n "$trailing_chars" ]]; then HAVING_OPTIONS+=("$option") OPTION_VALUES+=("$trailing_chars") elif $have_trailing_arg; then HAVING_OPTIONS+=("$option") OPTION_VALUES+=("${args[$((++argi))]}") #ifdef with_incomplete else INCOMPLETE_OPTION="$option" #endif fi continue 2 elif __zsh_query_contains "$option" "${short_opts_with_optional_arg[@]}"; then HAVING_OPTIONS+=("$option") OPTION_VALUES+=("$trailing_chars") # may be empty continue 2 fi done #endif ;; *) POSITIONALS+=("$arg");; esac done ''') _PREFIX = ShellFunction('prefix', r''' if [[ "$PREFIX" == "$1"* ]]; then PREFIX="${PREFIX#"$1"}" IPREFIX="$IPREFIX$1" $2 else compadd -- "$1" fi ''') _EXEC = ShellFunction('exec', r''' local item='' desc='' describe=() while IFS=$'\t' read -r item desc; do item="${item//:/\\:}" [[ -n "$desc" ]] && describe+=("$item:$desc") || describe+=("$item") done < <(eval "$1") _describe '' describe ''') _HISTORY = ShellFunction('history', r''' [[ -f "$HISTFILE" ]] || return local match='' command grep -E -o -- "$1" "$HISTFILE" | while read -r match; do compadd -- "$match" done ''') _MIME_FILE = ShellFunction('mime_file', r''' local line='' file='' mime='' i_opt='' if command file -i /dev/null &>/dev/null; then i_opt="-i" elif command file -I /dev/null &>/dev/null; then i_opt="-I" else compadd -- "$PREFIX"* return fi command file -L $i_opt -- "$PREFIX"* 2>/dev/null | while read -r line; do mime="${line##*:}" if [[ "$mime" == *inode/directory* ]] || command grep -q -E -- "$1" <<< "$mime"; then file="${line%:*}" if [[ "$file" == *\\* ]]; then file="$(command perl -pe 's/\\([0-7]{3})/chr(oct($1))/ge' <<< "$file")" fi if [[ "$mime" == *inode/directory* ]]; then compadd -- "$file/" else compadd -- "$file" fi fi done ''') _UID_LIST = ShellFunction('uid_list', r''' local items=($(command getent passwd | command awk -F: '{printf "%s:%s\n", $3, $1}')) _describe 'users' items ''') _GID_LIST = ShellFunction('gid_list', r''' local items=($(command getent group | command awk -F: '{printf "%s:%s\n", $3, $1}')) _describe 'groups' items ''') _CHARSET_LIST = ShellFunction('charset_list', r''' local items=($(command locale -m)) _describe 'charsets' items ''') _PATH_FILES_RELATIVE = ShellFunction('path_files_relative', r''' local DIR="$1"; shift _path_files -W "$PWD/$DIR" "$@" ''') # ============================================================================= # Bonus # ============================================================================= _MOUNTPOINT = ShellFunction('mountpoint', r''' local item='' command findmnt -lno TARGET | while read -r item; do compadd -- "$item" done ''') _ALSA_COMPLETE_CARDS = ShellFunction('alsa_complete_cards', r''' local card='' cards=() command aplay -l \ | command grep -Eo '^card [0-9]+: [^,]+' \ | command uniq \ | while builtin read card; do card="${card#card }" local id="${card%%: *}" local name="${card#*: }" cards+=("$id:$name") done _describe 'ALSA card' cards ''') _ALSA_COMPLETE_DEVICES = ShellFunction('alsa_complete_devices', r''' local card='' id='' name='' devices=() command aplay -l \ | command grep -Eo '^card [0-9]+: [^,]+' \ | command uniq \ | while builtin read card; do card="${card#card }" id="${card%%: *}" name="${card#*: }" devices+=("hw\\:$id:$name") done _describe 'ALSA device' devices ''') class ZshHelpers(GeneralHelpers): '''Class holding helper functions for Zsh.''' def __init__(self, config, function_prefix): super().__init__(config, function_prefix, ShellFunction) self.add_function(_QUERY) self.add_function(_EXEC) self.add_function(_PREFIX) self.add_function(_HISTORY) self.add_function(_PATH_FILES_RELATIVE) self.add_function(_MIME_FILE) self.add_function(_UID_LIST) self.add_function(_GID_LIST) self.add_function(_CHARSET_LIST) self.add_function(_MOUNTPOINT) self.add_function(_ALSA_COMPLETE_CARDS) self.add_function(_ALSA_COMPLETE_DEVICES) crazy-complete-crazy-complete-bc5afec/crazy_complete/zsh_utils.py000066400000000000000000000123761520061347500256700ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Utility functions for Zsh.''' from . import algo from . import shell from . import string_stream # pylint: disable=too-many-branches # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments def escape_colon(s): '''Escape colons in a string with backslash.''' return s.replace(':', '\\:') def escape_square_brackets(s): '''Escape square brackets with backslash.''' return s.replace('[', '\\[').replace(']', '\\]') def escape_colon_in_quoted_string(s): '''Escape a colon in a single/double quoted string.''' if ':' not in s: return s ss = string_stream.StringStream(s) result = '' while ss.have(): c = ss.get() if c == "'": sub = ss.parse_shell_single_quote(in_quotes=True) result += "'%s'" % sub.replace(':', r'\:') elif c == '"': sub = ss.parse_shell_double_quote(in_quotes=True) result += '"%s"' % sub.replace(':', r'\\:') elif c == ':': result += r'\\:' else: result += c return result def make_option_spec( option_strings, conflicting_options = None, description = None, complete = None, optional_arg = False, repeatable = False, hidden = False, final = False, metavar = None, action = None, long_opt_arg_sep = 'both'): ''' Make a Zsh option spec. Returns something like this: (--option -o){--option=,-o+}[Option description]:Metavar:Action ''' result = [] if conflicting_options is None: conflicting_options = [] # Hidden option =========================================================== if hidden: result.append("'!'") # Not options ============================================================= not_options = [] for o in sorted(conflicting_options): not_options.append(escape_colon(o)) if not repeatable: for o in sorted(option_strings): not_options.append(escape_colon(o)) if final: not_options = ['- *'] if not_options: result.append(shell.quote('(%s)' % ' '.join(algo.uniq(not_options)))) # Repeatable option ======================================================= if repeatable: result.append("'*'") # Option strings ========================================================== if complete and optional_arg is True: opts = [o+'-' if len(o) == 2 else o+'=-' for o in option_strings] elif complete: if long_opt_arg_sep == 'both': opts = [o+'+' if len(o) == 2 else o+'=' for o in option_strings] elif long_opt_arg_sep == 'equals': opts = [o+'+' if len(o) == 2 else o+'=-' for o in option_strings] elif long_opt_arg_sep == 'space': opts = [o+'+' if len(o) == 2 else o for o in option_strings] else: opts = option_strings if len(opts) == 1: result.append(opts[0]) else: result.append('{%s}' % ','.join(opts)) # Description ============================================================= if description is not None: result.append(shell.quote('[%s]' % escape_colon(escape_square_brackets(description)))) # Complete ================================================================ if complete: if metavar is None: metavar = ' ' action = escape_colon_in_quoted_string(action) result.append(':%s:%s' % (shell.quote(escape_colon(metavar)), action)) return ''.join(result) def make_positional_spec( number, repeatable, description, action): ''' Make a Zsh positional spec. Returns something like this: Number:Description:Action ''' if repeatable is True: num_spec = "'*'" else: num_spec = str(number) desc_spec = shell.quote(escape_colon(description)) if action == '_normal': return f'{num_spec}::{desc_spec}:{action}' action_escaped = escape_colon_in_quoted_string(action) return f'{num_spec}:{desc_spec}:{action_escaped}' def make_file_extension_pattern(extensions, fuzzy): '''Make a case-insensitive glob pattern matching `extensions`. Takes a list of extensions (e.g. ['txt', 'jpg']) and returns a Zsh-style pattern that matches all of them, ignoring case. Example output: '*.(#i)(txt|jpg)' ''' if len(extensions) == 1: pattern = '*.(#i)%s' % extensions[0] else: pattern = '*.(#i)(%s)' % '|'.join(extensions) if fuzzy: pattern += '*' return pattern def test(): '''Tests.''' cases = [ (0, r'foo', r'foo'), (1, r':', r'\\:'), (2, r'":"', r'"\\:"'), (3, r"':'", r"'\:'"), ] for num, instring, expected in cases: result = escape_colon_in_quoted_string(instring) if result != expected: print(f"Test {num} failed:") print(f"Having: {result}") print(f"Expected: {expected}") raise AssertionError("Test failed") crazy-complete-crazy-complete-bc5afec/crazy_complete/zsh_when.py000066400000000000000000000024711520061347500254640ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Module for generating conditional code in Zsh.''' from . import when from . import shell def _generate(query, tokens): '''Turn tokens/objects into condition code.''' r = [] for obj in tokens: if isinstance(obj, when.OptionIs): r.append(_generate_option_is(query, obj)) elif isinstance(obj, when.HasOption): r.append(_generate_has_option(query, obj)) elif obj in ('&&', '||', '!'): r.append(obj) elif obj == '(': r.append('{') elif obj == ')': r.append(';}') else: raise AssertionError("Not reached") if when.needs_braces(tokens): return '{ %s; }' % ' '.join(r) return ' '.join(r) def _generate_option_is(query, obj): func = query.use('option_is') args = [func, 'option_is', *obj.options, '--', *obj.values] return shell.join_quoted(args) def _generate_has_option(query, obj): func = query.use('has_option') args = [func, 'has_option', *obj.options] return shell.join_quoted(args) def generate_when_conditions(query, when_): '''Generate when condition code.''' tokens = when.parse_when(when_) return _generate(query, tokens) crazy-complete-crazy-complete-bc5afec/crazy_complete/zsh_wrapper.py000066400000000000000000000130731520061347500262030ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2025-2026 Benjamin Abendroth '''Code for generating wrapper.''' from . import cli from . import algo from . import preprocessor from .str_utils import replace_many _CODE = r''' %WRAPPER_FUNC%() { %ORIGINAL_COMPLETION_FUNC% local i=0 del=0 adjust=0 delete=() new_words=() for ((i=1; i <= ${#words[@]}; ++i)); do local w="${words[$i]}" if false; then true #ifdef long_opts_arg elif [[ "$w" == %LONG_OPTS_ARG_PATTERN1% ]]; then delete+=($i); elif [[ "$w" == %LONG_OPTS_ARG_PATTERN2% ]]; then delete+=($i $((++i))); #endif #ifdef long_opts_optional elif [[ "$w" == %LONG_OPTS_OPTIONAL_PATTERN% ]]; then delete+=($i); #endif #ifdef long_opts_flag elif [[ "$w" == %LONG_OPTS_FLAG_PATTERN% ]]; then delete+=($i); #endif #ifdef short_opts_arg elif [[ "$w" =~ '%SHORT_OPTS_ARG_REGEX1%' ]]; then delete+=($i $((++i))); elif [[ "$w" =~ '%SHORT_OPTS_ARG_REGEX2%' ]]; then delete+=($i); #endif #ifdef short_opts_optional elif [[ "$w" =~ '%SHORT_OPTS_OPTIONAL_REGEX%' ]]; then delete+=($i); #endif #ifdef short_opts_flag elif [[ "$w" =~ '%SHORT_OPTS_FLAG_REGEX%' ]]; then delete+=($i); #endif fi done for del in "${delete[@]}"; do (( del < CURRENT )) && (( ++adjust )) done (( CURRENT -= adjust )) for ((i=1; i <= ${#words[@]}; ++i)); do if [[ " ${delete[@]} " != *" $i "* ]]; then new_words+=("${words[$i]}") fi done words=("${new_words[@]}") words[1]='%WRAPS%' service=%WRAPS% (( $+_comps[%WRAPS%] )) && $_comps[%WRAPS%] } ''' def _make_long_opts_pattern(opts, arg_type): if not opts: return '' long, old = algo.partition(opts, cli.is_long_option_string) old = [o.lstrip('-') for o in old] long = [o.lstrip('-') for o in long] r = '' if long and old: r = '-(%s|-(%s))' % ('|'.join(old), '|'.join(long)) elif long: r = '--(%s)' % '|'.join(long) elif old: r = '-(%s)' % '|'.join(old) if arg_type == 'arg': r += '=*' elif arg_type == 'optional': r += '(|=*)' return r def _make_short_opts_flag_regex(flag_opts): if not flag_opts: return '' return '^-[%s]+$' % flag_opts def _make_short_opts_arg_regex(flag_opts, arg_opts, arg_type): if not arg_opts: return '' if flag_opts: r = '-[%s]*[%s]' % (flag_opts, arg_opts) else: r = '-[%s]' % arg_opts if arg_type == 'arg': r += '.+' elif arg_type == 'optional': r += '.*' return f'^{r}$' def generate_wrapper(ctxt, commandline): '''Generate code for wrapping a foreign command.''' make_completion_funcname = ctxt.helpers.make_completion_funcname completion_funcname = make_completion_funcname(commandline) wrapper_funcname = make_completion_funcname(commandline, '__wrapper') if not commandline.wraps: return (completion_funcname, None) long_opts_arg = [] long_opts_flag = [] long_opts_optional = [] short_opts_arg = [] short_opts_flag = [] short_opts_optional = [] for option in commandline.get_options(): if option.has_required_arg(): long_opts_arg += option.get_long_option_strings() long_opts_arg += option.get_old_option_strings() short_opts_arg += option.get_short_option_strings() elif option.has_optional_arg(): long_opts_optional += option.get_long_option_strings() long_opts_optional += option.get_old_option_strings() short_opts_optional += option.get_short_option_strings() else: long_opts_flag += option.get_long_option_strings() long_opts_flag += option.get_old_option_strings() short_opts_flag += option.get_short_option_strings() short_opts_flag = ''.join(o[1] for o in short_opts_flag) short_opts_arg = ''.join(o[1] for o in short_opts_arg) short_opts_optional = ''.join(o[1] for o in short_opts_optional) s = _CODE.strip() s = replace_many(s, [ ('%ORIGINAL_COMPLETION_FUNC%', completion_funcname), ('%WRAPPER_FUNC%', wrapper_funcname), ('%WRAPS%', commandline.wraps), ('%LONG_OPTS_FLAG_PATTERN%', _make_long_opts_pattern(long_opts_flag, None)), ('%LONG_OPTS_ARG_PATTERN1%', _make_long_opts_pattern(long_opts_arg, 'arg')), ('%LONG_OPTS_ARG_PATTERN2%', _make_long_opts_pattern(long_opts_arg, None)), ('%LONG_OPTS_OPTIONAL_PATTERN%', _make_long_opts_pattern(long_opts_optional, 'optional')), ('%SHORT_OPTS_FLAG_REGEX%', _make_short_opts_flag_regex(short_opts_flag)), ('%SHORT_OPTS_ARG_REGEX1%', _make_short_opts_arg_regex( short_opts_flag, short_opts_arg, None)), ('%SHORT_OPTS_ARG_REGEX2%', _make_short_opts_arg_regex( short_opts_flag, short_opts_arg, 'arg')), ('%SHORT_OPTS_OPTIONAL_REGEX%', _make_short_opts_arg_regex( short_opts_flag, short_opts_optional, 'optional')) ]) defines = [] if long_opts_flag: defines.append('long_opts_flag') if long_opts_arg: defines.append('long_opts_arg') if long_opts_optional: defines.append('long_opts_optional') if short_opts_flag: defines.append('short_opts_flag') if short_opts_arg: defines.append('short_opts_arg') if short_opts_optional: defines.append('short_opts_optional') s = preprocessor.preprocess(s, defines) return (wrapper_funcname, s) crazy-complete-crazy-complete-bc5afec/docs/000077500000000000000000000000001520061347500211715ustar00rootroot00000000000000crazy-complete-crazy-complete-bc5afec/docs/Makefile000066400000000000000000000011721520061347500226320ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) crazy-complete-crazy-complete-bc5afec/docs/basic_documentation.rst000066400000000000000000000107511520061347500257410ustar00rootroot00000000000000Crazy-Complete Documentation ============================ .. contents:: :local: :depth: 2 This documentation provides an overview of how to define shell completions for commands using **crazy-complete**. Command Definition ------------------ To define a completion for a command, use the following structure: .. code-block:: yaml prog: "" help: "" options: