Repository: xenomachina/kotlin-argparser Branch: master Commit: 9367da9e757c Files: 26 Total size: 230.5 KB Directory structure: gitextract_f8jhxc_z/ ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── COPYING ├── README.md ├── build.gradle ├── codecov.sh ├── detekt.yml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src/ ├── main/ │ └── kotlin/ │ └── com/ │ └── xenomachina/ │ └── argparser/ │ ├── ArgParser.kt │ ├── Default.kt │ ├── DefaultHelpFormatter.kt │ ├── Exceptions.kt │ ├── HelpFormatter.kt │ ├── OptionDelegate.kt │ ├── ParsingDelegate.kt │ ├── PositionalDelegate.kt │ ├── PosixNaming.kt │ ├── SystemExitException.kt │ └── WrappingDelegate.kt └── test/ └── kotlin/ └── com/ └── xenomachina/ └── argparser/ └── ArgParserTest.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ### Gradle ### .gradle /build/ # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar # Cache of project .gradletasknamecache # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.propertiesgradletasknamecache ### IntelliJ IDEA ### .idea/ ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties ================================================ FILE: .travis.yml ================================================ language: java jdk: - openjdk8 before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ cache: directories: - $HOME/.gradle/ after_success: - ./gradlew jacocoTestReport && ./codecov.sh ================================================ FILE: CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## 2.0.7 - 2018-05-08 ### Fixed - [Issue #54](https://github.com/xenomachina/kotlin-argparser/issues/54) Allow the dot character in options and arguments. Thanks @tgockel! - [Issue #47](https://github.com/xenomachina/kotlin-argparser/issues/47) If an option looks at the value of another option it can see the current value. If no value has been set a `MissingValueException` is thrown. ## 2.0.6 - 2018-03-26 ### Changed - Help text formatting now treats multi-newlines as paragraph separators, while single newlines are still treated like spaces. Thanks @leomillon! ## 2.0.5 - 2018-03-20 ### Changed - Releasing to Maven Central in addition to Bintray. This is probably the only really externally visible change. - Upgraded a bunch of dependencies, including gradlew. - gradle -> 4.5.1 - dokka -> = 0.9.16 - gradle_bintray -> = 1.8.0 - gradle_release -> = 2.6.0 - kotlin -> 1.2.30 - xenocom -> 0.0.6 ## 2.0.4 - 2018-01-18 ### Added - If the `programName` passed to `mainBody` is null, then the system property `com.xenomachina.argparser.programName` is used, if set. - The `parseInto` method can be used as an inline alternative to `force`. Thanks @shanethehat! - [Issue #24](https://github.com/xenomachina/kotlin-argparser/issues/18): `default` can now accept a lambda, making it possible to defer computation of defaults until actually required. Thanks @shanethehat! ### Changed - All instances of `progName` have been renamed to `programName`. - The gradle wrapper has been updated. Thanks @ColinHebert! ## 2.0.3 - 2017-06-12 ### Fixed - [Issue #18](https://github.com/xenomachina/kotlin-argparser/issues/18) ## 2.0.2 - 2017-06-12 ### Fixed - [Issue #19](https://github.com/xenomachina/kotlin-argparser/issues/19) by updating xenocom dependency to 0.0.5. Also updated kotlin to 1.1.2-5 for good measure. ## 2.0.1 - 2017-05-15 ### Changed - [Issue #14](https://github.com/xenomachina/kotlin-argparser/issues/14) — previously, automatic option naming would turn "camelCase" into "--camelCase". Now it is converted to "--camel-case". - Likewise, positinal argument auto-naming used to convert "camelCase" into "CAMELCASE". Now it is converted to "CAMEL-CASE". - Improve help formatting w/long program names - README formatting improved. Thanks @konfilios! ### Fixed - [Issue #17](https://github.com/xenomachina/kotlin-argparser/issues/17) — specifying 0 for the columns should format help without line wrapping. Previously, this did not work as documented, and would instead wrap text in very narrow columns. - [Issue #15](https://github.com/xenomachina/kotlin-argparser/issues/15) — make it possible to specify 'argName' on all variants of 'storing' and `adding` ## 2.0.0 - 2017-04-21 ### Added - `ArgParser.option` is now a public method, so it's possible to create many new option types that were not previously possible. The existing option types are all written in terms of `option`, so they can be used to get an idea of how it works. - More tests have been added. - Started using keepachangelog.com format for CHANGELOG.md - Made minor improvements to release process ### Changed - The `storing`, `adding` and `positionalList` methods of `ArgParser` have had their parameters slightly reordered to be consistent with the other methods. This is an incompatible change. The name(s) come first, if any, followed by `help`. Other parameters appear after `help`, with the `transform` function, if any, last. It is recommended that clients either pass the transform as a block (ie: with braces) or as a named parameter, as any future new parameters will necessarily change its position in the list. - Delegate and DelegateProvider are now abstract classes with internal constructors. This makes it much easier for me to separate internal and public parts of their API. This is an incompatible change, however it shouldn't really affect you unless you were trying to implement `Delegate`, which was't supported to begin with. - `default` methods on both `Delegate` and `DelegateProvider` are now extension methods. This makes it possible to generalize the type when adding a default. This is most noticable when using a nullable value (or `null` itself) for the default, though may also be useful in other cases (eg: a "storing" that always produces a `Rectangle`, but you want the default to be a `Circle`. The resulting delegate will be a `Delegate`.) - Registration of delegates now takes place at binding-time rather than construction time. This should be pretty indistinguishable from the old behavior unless you're creating delegates without binding them. - Help formatting has been improved so that it's far less likely to wrap option names in the usage table. - There have been numerous bugfixes, particularly around positionals ## 1.1.0 - 2017-03-09 ### Added - Auto-naming of options and positionals. - Each of the ArgParser methods that takes names and returns a Delegate has an overload that takes no name, but returns a DelegateProvider. - A DelegateProvider has an `operator fun provideDelegate` that returns a Delegate, using a name derived from the name of the property the DelegateProvider is being bound to. ### Removed - `addValidtator` is now deprecated. Use `addValidator` instead. ### Fixed - Removed documentation of `option` from `README.md`, as it is internal - Corrected spelling of `addValidator`. `addValidtator` is still there, but deprecated. It'll probably be removed in the next release, barring the addition of potato functionality. ## 1.0.2 - 2017-03-07 ### Changed - Upgrade to Kotlin 1.1, extract xenocom package. ## 1.0.1 - 2017-01-30 ### Fixed - Fix small bug where `runMain` didn't have a default value for `columns` parameter. (Now defaults to null.) ## 1.0.0 - 2017-01-27 ### Added - Initial release ================================================ FILE: COPYING ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ================================================ FILE: README.md ================================================ # Kotlin --argparser [![Maven Central](https://img.shields.io/maven-central/v/com.xenomachina/kotlin-argparser.svg)](https://mvnrepository.com/artifact/com.xenomachina/kotlin-argparser) [![Build Status](https://travis-ci.org/xenomachina/kotlin-argparser.svg?branch=master)](https://travis-ci.org/xenomachina/kotlin-argparser) [![codebeat badge](https://codebeat.co/badges/902174e2-31be-4f9d-a4ba-40178b075d2a)](https://codebeat.co/projects/github-com-xenomachina-kotlin-argparser-master) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) [![Javadocs](https://www.javadoc.io/badge/com.xenomachina/kotlin-argparser.svg)](https://www.javadoc.io/doc/com.xenomachina/kotlin-argparser) [![License: LGPL 2.1](https://img.shields.io/badge/license-LGPL--2.1-blue.svg)](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) This is a library for parsing command-line arguments. It can parse both options and positional arguments. It aims to be easy to use and concise yet powerful and robust. ## Overview Defining options and positional arguments is as simple as: ```kotlin import com.xenomachina.argparser.ArgParser class MyArgs(parser: ArgParser) { val v by parser.flagging("enable verbose mode") val name by parser.storing("name of the user") val count by parser.storing("number of the widgets") { toInt() } val source by parser.positional("source filename") val destination by parser.positional("destination filename") } ``` An instance of `MyArgs` will represent the set of parsed arguments. Each option and positional argument is declared as a property that delegates through a delegate factory method on an instance of `ArgParser`. The name of an option is inferred from the name of the property it is bound to. The options above are named `-v`, `--name` and `--count`, respectively. There are also two positional arguments. Direct control over an option's name is also possible, and for most types of options it is also possible to have multiple names, like a short and long name: ```kotlin class MyArgs(parser: ArgParser) { val verbose by parser.flagging( "-v", "--verbose", help = "enable verbose mode") val name by parser.storing( "-N", "--name", help = "name of the user") val count by parser.storing( "-c", "--count", help = "number of widgets") { toInt() } val source by parser.positional( "SOURCE", help = "source filename") val destination by parser.positional( "DEST", help = "destination filename") } ``` The unparsed command-line arguments are passed to the `ArgParser` instance at construction: ```kotlin fun main(args: Array) = mainBody { ArgParser(args).parseInto(::MyArgs).run { println("Hello, ${name}!") println("I'm going to move ${count} widgets from ${source} to ${destination}.") // TODO: move widgets } } ``` See [kotlin-argparser-example](https://github.com/xenomachina/kotlin-argparser-example) for a complete example project. ## Nomenclature Options, arguments, flags... what's the difference? An application's `main` function is passed an array of strings. These are the *unparsed command-line arguments*, or *unparsed arguments* for short. The unparsed arguments can then be parsed into *options*, which start with a hyphen ("`-`"), and *positional arguments*. For example, in the command `ls -l /tmp/`, the unparsed arguments would be `"-l", "/tmp"` where `-l` is an option, while `/tmp/` is a positional argument. Options can also have *option arguments*. In the command `ls --time-style=iso`, the option is `--time-style` and that options argument is `iso`. Note that in parsing a single unparsed argument can be split into an option and an option argument, or even into multiple options in some cases. A *flag* is a boolean option which has no arguments and which is false if not provided, but true if provided. The `-l` option of `ls` is a flag. ## Option Types ### Boolean Flags Boolean flags are created by asking the parser for a `flagging` delegate. One or more option names, may be provided: ```kotlin val verbose by parser.flagging("-v", "--verbose", help = "enable verbose mode") ``` Here the presence of either `-v` or `--verbose` options in the arguments will cause the `Boolean` property `verbose` to be `true`, otherwise it will be `false`. ### Storing a Single Argument Single argument options are created by asking the parser for a `storing` delegate. ```kotlin val name by parser.storing("-N", "--name", help = "name of the user") ``` Here either `-N` or `--name` with an argument will cause the `name` property to have that argument as its value. A function can also be supplied to transform the argument into the desired type. Here the `size` property will be an `Int` rather than a `String`: ```kotlin val size by parser.storing("-c", "--count", help = "number of widgets") { toInt() } ``` ### Adding to a Collection Options that add to a `Collection` each time they appear in the arguments are created with using the `adding` delegate. Just like `storing` delegates, a transform function may optionally be supplied: ```kotlin val includeDirs by parser.adding( "-I", help = "directory to search for header files") { File(this) } ``` Now each time the `-I` option appears, its transformed argument is appended to `includeDirs`. ### Mapping from an option to a fixed value For choosing between a fixed set of values (typically, but not necessarily, from an enum), a `mapping` delegate can be used: ```kotlin val mode by parser.mapping( "--fast" to Mode.FAST, "--small" to Mode.SMALL, "--quiet" to Mode.QUIET, help = "mode of operation") ``` Here the `mode` property will be set to the corresponding `ArgParser.Mode` value depending on which of `--fast`, `--small`, and `--quiet` appears (last) in the arguments. `mapping` is one of the few cases where it is not possible to infer the option name from the property name. ### More advanced options For all other types of options, the `option` method should be used. The methods mentioned above are, in fact, convenience methods built on top of the `option` method. For example, it is possible to create an option that has multiple arguments: ```kotlin fun ArgParser.putting(vararg names: String, help: String) = option>(*names, argNames = listOf("KEY", "VALUE"), help = help) { value.orElse { mutableMapOf() }.apply { put(arguments.first(), arguments.last()) } } ``` Note that the `option` method does not have an auto-naming overload. If you need this capability, create a `DelegateProvider` that creates your `Delegate`: ```kotlin fun ArgParser.putting(help: String) = ArgParser.DelegateProvider { identifier -> putting(identifierToOptionName(identifier), help = help) } ``` ## Positional Arguments Positional arguments are collected by using the `positional` and `positionalList` methods. For a single positional argument: ```kotlin val destination by parser.positional("destination filename") ``` An explicit name may also be specified: ```kotlin val destination by parser.positional("DEST", help = "destination filename") ``` The name ("DEST", here) is used in error handling and help text. For a list of positional arguments: ```kotlin val sources by parser.positionalList("SOURCE", 1..Int.MAX_VALUE, help = "source filename") ``` The range indicates how many arguments should be collected, and defaults to the value shown in this example. As the name suggests, the resulting property will be a `List`. Both of these methods accept an optional transform function for converting arguments from `String` to whatever type is actually desired: ```kotlin val destination by parser.positional("DEST", help = "...") { File(this) } val sources by parser.positionalList("SOURCE", 1..Int.MAX_VALUE, help = "...") { File(this) } ``` ## Modifying Delegates The delegates returned by any of these methods also have a few methods for setting optional attributes: ### Adding a Default Value Certain types of delegates (notably `storing`, `mapping`, and `positional`) have no default value, and hence will be required options unless a default value is provided. This is done with the `default` method: ```kotlin val name by parser.storing("-N", "--name", help = "...").default("John Doe") ``` Note that it *is* possible to use `null` for the default, though this may require specifying the type parameter for `default` explicitly: ```kotlin val name by parser.storing("-N", "--name", help = "...").default(null) ``` The type of the resulting property be nullable (a `String?` in this case). ### Adding a Validator Sometimes it's easier to validate an option at the end of parsing, in which case the `addValidator` method can be used. ```kotlin val percentages by parser.adding("--percentages", help = "...") { toInt() } .addValidator { if (value.sum() != 100) throw InvalidArgumentException( "Percentages must add up to 100%") } ``` ## Error Handling If the parser determines that execution should not continue it will throw a `SystemExitException` which has a status code appropriate for passing to `exitProcess` as well as a message for the user. These exceptions can be caused by user error, or even if the user requests help (eg: via the `--help` option). It is recommended that transform functions (given to `storing`, `positionalList`, etc.) and post-parsing validation, including that performed via, `addValidator` also throw a `SystemExitException` on failure. As a convenience, these exceptions can be handled by using the `mainBody` function: ```kotlin class ParsedArgs(parser: ArgParser) { val name by positional("The user's name").default("world") } fun main(args: Array) = mainBody { ArgParser(args).parseInto(::ParsedArgs).run { println("Hello, {name}!") } } ``` ## Parsing Parsing of command-line arguments is performed sequentially. So long as option-processing is enabled, each not-yet-processed command-line argument that starts with a hyphen (`-`) is treated as an option. ### Short Options Short options start with a single hyphen. If the option takes an argument, the argument can either be appended: ```bash # "-o" with argument "ARGUMENT" my_program -oARGUMENT ``` or can be the following command-line argument: ```bash # "-o" with argument "ARGUMENT" my_program -o ARGUMENT ``` Zero argument short options can also be appended to each other without intermediate hyphens: ```bash # "-x", "-y" and "-z" options my_program -xyz ``` An option that accepts arguments is also allowed at the end of such a chain: ```bash # "-x", "-y" and "-z" options, with argument for "-z" my_program -xyzARGUMENT ``` ### Long Options Long options start with a double hyphen (`--`). An argument to a long option can either be delimited with an equal sign (`=`): ```bash # "--foo" with argument "ARGUMENT" my_program --foo=ARGUMENT ``` or can be the following command-line argument: ```bash # "--foo" with argument "ARGUMENT" my_program --foo ARGUMENT ``` ### Multi-argument Options Multi-argument options are supported, though currently not by any of the convenience methods. Option-arguments after the first must be separate command-line arguments, for both an long and short forms of an option. ### Positional Arguments In GNU mode (the default), options can be interspersed with positional arguments, but in POSIX mode the first positional argument that is encountered disables option processing for the remaining arguments. In either mode, if the argument "--" is encountered while option processing is enabled, then option processing is disabled for the rest of the command-line. Once the options and option-arguments have been eliminated, what remains are considered to be positional arguments. Each positional argument delegate can specify a minimum and maximum number of arguments it is willing to collect. The positional arguments are distributed to the delegates by allocating each positional delegate at least as many arguments as it requires. If more than the minimum number of positional arguments have been supplied then additional arguments will be allocated to the first delegate up to its maximum, then the second, and so on, until all arguments have been allocated to a delegate. This makes it easy to create a program that behaves like `grep`: ```kotlin class Args(parser: ArgParser) { // accept 1 regex followed by n filenames val regex by parser.positional("REGEX", help = "regular expression to search for") val files by parser.positionalList("FILE", help = "file to search in") } ``` And equally easy to create a program that behaves like `cp`: ```kotlin class Args(parser: ArgParser) { // accept n source files followed by 1 destination val sources by parser.positionalList("SOURCE", help = "source file") val destination by parser.positional("DEST", help = "destination file") } ``` ## Forcing Parsing Parsing normally does not begin until a delegate's value is accessed. Sometimes this is not desirable, so it is possible to enforce the parsing of arguments into a class of values. This ensures that all arguments that are required are provided, and all arguments provided are consumed. Forcing can be done in a separate step using the `force` method: ```kotlin val parser = ArgParser(args) val parsedArgs = ParsedArgs(parser) parser.force() // now you can use parsedArgs ``` Alternatively, forcing can be done inline via the `parseInto` method: ```kotlin val parsedArgs = ArgParser(args).parseInto(::ParsedArgs) // now you can use parsedArgs ``` In both cases exceptions will be thrown where parsing or validation errors are found. ## Help Formatting By default, `ArgParser` will add a `--help` option (short name `-h`) for displaying usage information. If this option is present a `ShowHelpException` will be thrown. If the default exception handling is being used (see [Error Handling](#error-handling)) the program will halt and print a help message like the one below, based on the `ArgParser` configuration: usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT [-v]... SOURCE... DEST This is the prologue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam malesuada maximus eros. Fusce luctus risus eget quam consectetur, eu auctor est ullamcorper. Maecenas eget suscipit dui, sed sodales erat. Phasellus. required arguments: -o OUTPUT, directory in which all output should --output OUTPUT be generated optional arguments: -h, --help show this help message and exit -n, --dry-run don't do anything -I INCLUDE, search in this directory for header --include INCLUDE files -v, --verbose increase verbosity positional arguments: SOURCE source file DEST destination file This is the epilogue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel tortor nunc. Sed eu massa sed turpis auctor faucibus. Donec vel pellentesque tortor. Ut ultrices tempus lectus fermentum vestibulum. Phasellus. The creation of the `--help` option can be disabled by passing `null` as the `helpFormatter` when constructing the `ArgParser`, or configured by manually constructing a `HelpFormatter` instance. In the above example a `DefaultHelpFormatter` was created with the prologue and epilogue. ## Caveats - This library should be considered to be *very beta*. While there are no plans to make any breaking changes to the API, it's possible that there may be some until it is mature. - Upon reading the value any of the delegated properties created by an `ArgParser`, the arguments used to construct that `ArgParser` will be parsed. This means it's important that you don't attempt to create delegates on an `ArgParser` after any of its existing delegated properties have been read. Attempting to do so will cause an `IllegalStateException`. It would be nice if Kotlin had facilities for doing some of the work of `ArgParser` at compile time rather than run time, but so far the run time errors seem to be reasonably easy to avoid. ## Configuring Your Build Kotlin-argparser binaries are hosted on Maven Central and also Bintray's JCenter. In Gradle, add something like this in your `build.gradle`: ```groovy // you probably already have this part buildscript { repositories { mavenCentral() // or jcenter() } } dependencies { compile "com.xenomachina:kotlin-argparser:$kotlin_argparser_version" } ``` In Maven add something like this to your `pom.xml`: ```xml com.xenomachina kotlin-argparser VERSION ``` Information on setting up other build systems, as well as the current version number, can be found on [MVN Repository's page for Kotlin-argparser](https://mvnrepository.com/artifact/com.xenomachina/kotlin-argparser/latest). ## Thanks Thanks to the creators of Python's [`argparse`](https://docs.python.org/3/library/argparse.html) module, which provided the initial inspiration for this library. Thanks also to the team behind [Kotlin](https://kotlinlang.org/). Finally, thanks to all of the people who have contributed [code](https://github.com/xenomachina/kotlin-argparser/graphs/contributors) and/or [issues](https://github.com/xenomachina/kotlin-argparser/issues). ================================================ FILE: build.gradle ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ buildscript { repositories { mavenCentral() jcenter() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_version" classpath "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detekt_version" } } plugins { id 'com.palantir.git-version' version '0.12.0-rc2' id 'org.jmailen.kotlinter' version '1.12.0' } apply plugin: 'maven' apply plugin: 'kotlin' apply plugin: 'java' apply plugin: 'org.jetbrains.dokka' apply plugin: 'signing' apply plugin: 'com.jfrog.bintray' apply plugin: 'maven-publish' apply plugin: 'jacoco' apply plugin: "io.gitlab.arturbosch.detekt" assert name == 'kotlin-argparser' description = 'Concise, easy, powerful and robust command line argument parsing for Kotlin' group 'com.xenomachina' version = gitVersion() project.ext { githubUser = "xenomachina" vcsDev = "https://github.com/$githubUser" githubRepo = "$githubUser/$name" vcsUrl = "https://github.com/$githubRepo" } sourceCompatibility = 1.6 repositories { mavenCentral() jcenter() } test { useJUnitPlatform() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "com.xenomachina:xenocom:$xenocom_version" testCompile "io.kotlintest:kotlintest-runner-junit5:$kotlintest_version" testCompile "org.slf4j:slf4j-simple:$slf4j_version" // This is to ensure that kotlintest uses the correct version of // kotlin-reflect testCompile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" } sourceSets { main.java.srcDirs += 'src/main/kotlin' } ////////////////////////////////////////////////////////////////////////////// // Detekt config ////////////////////////////////////////////////////////////////////////////// detekt { // remove this when plugin stops choosing latest version by default version = "$detekt_version" defaultProfile { input = file("src/main/kotlin") filters = ".*/resources/.*,.*/build/.*" config = file("detekt.yml") } } ////////////////////////////////////////////////////////////////////////////// // Dokka config ////////////////////////////////////////////////////////////////////////////// dokka { moduleName = project.name // TODO: includes = ['Module.md'] linkMapping { dir = "src/main/kotlin" url = "$project.ext.vcsUrl/blob/master/src/main/kotlin" suffix = "#L" } sourceDirs = files('src/main/kotlin') } task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { outputFormat = "javadoc" outputDirectory = javadoc.destinationDir linkMapping { dir = "src/main/kotlin" url = "$project.ext.vcsUrl/blob/master/src/main/kotlin" suffix = "#L" } sourceDirs = files('src/main/kotlin') } //Based on comment by @jnizet at https://github.com/Kotlin/dokka/issues/42 task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { classifier = 'javadoc' from javadoc.destinationDir } task sourcesJar(type: Jar, dependsOn: classes) { classifier = 'sources' from sourceSets.main.allSource } artifacts { archives javadocJar, sourcesJar } if (System.env.CI != "true") { // Some of the stuff in here can break continuous integration, and none of // it is necessary in CI. ////////////////////////////////////////////////////////////////////////////// // POM config ////////////////////////////////////////////////////////////////////////////// def pomConfig = { packaging 'jar' name project.name description project.description url project.ext.vcsUrl scm { connection "scm:git:$project.ext.vcsUrl" developerConnection "scm:git:$project.ext.vcsDev" url "$project.ext.vcsUrl" } licenses { license { name "GNU Lesser General Public License, Version 2.1" url "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt" distribution "repo" } } developers { developer { id "xenomachina" name "Laurence Gonsalves" // State-of-the art anti-scraper encryption. ;-) email "moc.anihcamonex@ecnerual".reverse() } } } ////////////////////////////////////////////////////////////////////////////// // Maven Central upload ////////////////////////////////////////////////////////////////////////////// signing { useGpgCmd() sign configurations.archives } def ossrhUsernameOrEmpty = hasProperty('ossrhUsername') ? ossrhUsername : '' def ossrhPasswordOrEmpty = hasProperty('ossrhPassword') ? ossrhPassword : '' uploadArchives { repositories { mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { authentication(userName: ossrhUsernameOrEmpty, password: ossrhPasswordOrEmpty) } snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { authentication(userName: ossrhUsernameOrEmpty, password: ossrhPasswordOrEmpty) } pom.project pomConfig } } } ////////////////////////////////////////////////////////////////////////////// // bintray upload ////////////////////////////////////////////////////////////////////////////// // Based on https://github.com/bintray/gradle-bintray-plugin bintray { user = System.getenv('BINTRAY_USER') key = System.getenv('BINTRAY_KEY') pkg { repo = 'maven' name = project.name desc = project.description licenses = ['LGPL-2.1'] vcsUrl = "$project.ext.vcsUrl" websiteUrl = "$project.ext.vcsUrl" issueTrackerUrl = "$project.ext.vcsUrl/issues" githubRepo = project.ext.githubRepo githubReleaseNotesFile = 'CHANGELOG.md' version { name = project.version desc = "$project.name version $project.version" released = new Date() vcsTag = "$project.version" } } publications = ['MyPublication'] } // Create the publication with the pom configuration: publishing { publications { MyPublication(MavenPublication) { from components.java artifact sourcesJar artifact javadocJar pom.withXml { def root = asNode() root.children().last() + pomConfig } } } } } ////////////////////////////////////////////////////////////////////////////// // jacoco (codecov.io) plugin config ////////////////////////////////////////////////////////////////////////////// jacocoTestReport { reports { xml.enabled true html.enabled false } } ================================================ FILE: codecov.sh ================================================ #!/usr/bin/env bash set -e +o pipefail VERSION="8fda091" url="https://codecov.io" env="$CODECOV_ENV" service="" token="" search_in="" flags="" exit_with=0 curlargs="" curlawsargs="" dump="0" clean="0" curl_s="-s" name="$CODECOV_NAME" include_cov="" exclude_cov="" ddp="$(echo ~)/Library/Developer/Xcode/DerivedData" xp="" files="" cacert="$CODECOV_CA_BUNDLE" gcov_ignore="" gcov_include="" ft_gcov="1" ft_coveragepy="1" ft_fix="1" ft_search="1" ft_s3="1" ft_xcode="1" ft_network="1" ft_xcodeplist="0" _git_root=$(git rev-parse --show-toplevel 2>/dev/null || hg root 2>/dev/null || echo $PWD) git_root="$_git_root" remote_addr="" if [ "$git_root" = "$PWD" ]; then git_root="." fi url_o="" pr_o="" build_o="" commit_o="" search_in_o="" tag_o="" branch_o="" slug_o="" commit="$VCS_COMMIT_ID" branch="$VCS_BRANCH_NAME" pr="$VCS_PULL_REQUEST" slug="$VCS_SLUG" tag="$VCS_TAG" build_url="$CI_BUILD_URL" build="$CI_BUILD_ID" job="$CI_JOB_ID" beta_xcode_partials="" proj_root="$git_root" gcov_exe="gcov" gcov_arg="" b="\033[0;36m" g="\033[0;32m" r="\033[0;31m" e="\033[0;90m" x="\033[0m" show_help() { cat << EOF Codecov Bash $VERSION Global report uploading tool for Codecov Documentation at https://docs.codecov.io/docs Contribute at https://github.com/codecov/codecov-bash -h Display this help and exit -f FILE Target file(s) to upload -f "path/to/file" only upload this file skips searching unless provided patterns below -f '!*.bar' ignore all files at pattern *.bar -f '*.foo' include all files at pattern *.foo Must use single quotes. This is non-exclusive, use -s "*.foo" to match specific paths. -s DIR Directory to search for coverage reports. Already searches project root and artifact folders. -t TOKEN Set the private repository token (option) set environment variable CODECOV_TOKEN=:uuid -t @/path/to/token_file -t uuid -n NAME Custom defined name of the upload. Visible in Codecov UI -e ENV Specify environment variables to be included with this build Also accepting environment variables: CODECOV_ENV=VAR,VAR2 -e VAR,VAR2 -X feature Toggle functionalities -X gcov Disable gcov -X coveragepy Disable python coverage -X fix Disable report fixing -X search Disable searching for reports -X xcode Disable xcode processing -X network Disable uploading the file network -R root dir Used when not in git/hg project to identify project root directory -F flag Flag the upload to group coverage metrics -F unittests This upload is only unittests -F integration This upload is only integration tests -F ui,chrome This upload is Chrome - UI tests -c Move discovered coverage reports to the trash -Z Exit with 1 if not successful. Default will Exit with 0 -- xcode -- -D Custom Derived Data Path for Coverage.profdata and gcov processing Default '~/Library/Developer/Xcode/DerivedData' -J Specify packages to build coverage. This can significantly reduces time to build coverage reports. -J 'MyAppName' Will match "MyAppName" and "MyAppNameTests" -J '^ExampleApp$' Will match only "ExampleApp" not "ExampleAppTests" -- gcov -- -g GLOB Paths to ignore during gcov gathering -G GLOB Paths to include during gcov gathering -p dir Project root directory Also used when preparing gcov -x gcovexe gcov executable to run. Defaults to 'gcov' -a gcovargs extra arguments to pass to gcov -- Override CI Environment Variables -- These variables are automatically detected by popular CI providers -B branch Specify the branch name -C sha Specify the commit sha -P pr Specify the pull request number -b build Specify the build number -T tag Specify the git tag -- Enterprise -- -u URL Set the target url for Enterprise customers Not required when retrieving the bash uploader from your CCE (option) Set environment variable CODECOV_URL=https://my-hosted-codecov.com -r SLUG owner/repo slug used instead of the private repo token in Enterprise (option) set environment variable CODECOV_SLUG=:owner/:repo (option) set in your codecov.yml "codecov.slug" -S PATH File path to your cacert.pem file used to verify ssl with Codecov Enterprise (optional) (option) Set environment variable: CODECOV_CA_BUNDLE="/path/to/ca.pem" -U curlargs Extra curl arguments to communicate with Codecov. e.g., -U "--proxy http://http-proxy" -A curlargs Extra curl arguments to communicate with AWS. -- Debugging -- -d Dont upload and dump to stdout -K Remove color from the output -v Verbose mode EOF } say() { echo -e "$1" } urlencode() { echo "$1" | curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | cut -c 3- | sed -e 's/%0A//' } swiftcov() { _dir=$(dirname "$1") for _type in app framework xctest do find "$_dir" -name "*.$_type" | while read f do _proj=${f##*/} _proj=${_proj%."$_type"} if [ "$2" = "" ] || [ "$(echo "$_proj" | grep -i "$2")" != "" ]; then say " $g+$x Building reports for $_proj $_type" dest=$([ -f "$f/$_proj" ] && echo "$f/$_proj" || echo "$f/Contents/MacOS/$_proj") _proj_name=$(echo "$_proj" | sed -e 's/[[:space:]]//g') xcrun llvm-cov show $beta_xcode_partials -instr-profile "$1" "$dest" > "$_proj_name.$_type.coverage.txt" \ || say " ${r}x>${x} llvm-cov failed to produce results for $dest" fi done done } # Credits to: https://gist.github.com/pkuczynski/8665367 parse_yaml() { local prefix=$2 local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034') sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \ -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 | awk -F$fs '{ indent = length($1)/2; vname[indent] = $2; for (i in vname) {if (i > indent) {delete vname[i]}} if (length($3) > 0) { vn=""; if (indent > 0) {vn=(vn)(vname[0])("_")} printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3); } }' } if [ $# != 0 ]; then while getopts "a:A:b:B:cC:dD:e:f:F:g:G:hJ:Kn:p:P:r:R:s:S:t:T:u:U:vx:X:Z" o do case "$o" in "a") gcov_arg=$OPTARG ;; "A") curlawsargs="$OPTARG" ;; "b") build_o="$OPTARG" ;; "B") branch_o="$OPTARG" ;; "c") clean="1" ;; "C") commit_o="$OPTARG" ;; "d") dump="1" ;; "D") ddp="$OPTARG" ;; "e") env="$env,$OPTARG" ;; "f") if [ "${OPTARG::1}" = "!" ]; then exclude_cov="$exclude_cov -not -path '${OPTARG:1}'" elif [[ "$OPTARG" = *"*"* ]]; then include_cov="$include_cov -or -name '$OPTARG'" else ft_search=0 if [ "$files" = "" ]; then files="$OPTARG" else files="$files $OPTARG" fi fi ;; "F") if [ "$flags" = "" ]; then flags="$OPTARG" else flags="$flags,$OPTARG" fi ;; "g") gcov_ignore="$gcov_ignore -not -path '$OPTARG'" ;; "G") gcov_include="$gcov_include -path '$OPTARG'" ;; "h") show_help exit 0; ;; "J") if [ "$xp" = "" ]; then xp="$OPTARG" else xp="$xp\|$OPTARG" fi ;; "K") b="" g="" r="" e="" x="" ;; "n") name="$OPTARG" ;; "p") proj_root="$OPTARG" ;; "P") pr_o="$OPTARG" ;; "r") slug_o="$OPTARG" ;; "R") git_root="$OPTARG" ;; "s") if [ "$search_in_o" = "" ]; then search_in_o="$OPTARG" else search_in_o="$search_in_o $OPTARG" fi ;; "S") cacert="--cacert \"$OPTARG\"" ;; "t") if [ "${OPTARG::1}" = "@" ]; then token=$(cat "${OPTARG:1}" | tr -d ' \n') else token="$OPTARG" fi ;; "T") tag_o="$OPTARG" ;; "u") url_o=$(echo "$OPTARG" | sed -e 's/\/$//') ;; "U") curlargs="$OPTARG" ;; "v") set -x curl_s="" ;; "x") gcov_exe=$OPTARG ;; "X") if [ "$OPTARG" = "gcov" ]; then ft_gcov="0" elif [ "$OPTARG" = "coveragepy" ] || [ "$OPTARG" = "py" ]; then ft_coveragepy="0" elif [ "$OPTARG" = "xcodeplist" ]; then ft_xcodeplist="1" ft_xcode="0" elif [ "$OPTARG" = "fix" ] || [ "$OPTARG" = "fixes" ]; then ft_fix="0" elif [ "$OPTARG" = "xcode" ]; then ft_xcode="0" elif [ "$OPTARG" = "search" ]; then ft_search="0" elif [ "$OPTARG" = "xcodepartials" ]; then beta_xcode_partials="-use-color" elif [ "$OPTARG" = "network" ]; then ft_network="0" fi ;; "Z") exit_with=1 ;; esac done fi say " _____ _ / ____| | | | | ___ __| | ___ ___ _____ __ | | / _ \\ / _\` |/ _ \\/ __/ _ \\ \\ / / | |___| (_) | (_| | __/ (_| (_) \\ V / \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/ Bash-$VERSION " search_in="$proj_root" if [ "$JENKINS_URL" != "" ]; then say "$e==>$x Jenkins CI detected." # https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project # https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin#GitHubpullrequestbuilderplugin-EnvironmentVariables service="jenkins" branch=$([ ! -z "$ghprbSourceBranch" ] && echo "$ghprbSourceBranch" || \ [ ! -z "$GIT_BRANCH" ] && echo "$GIT_BRANCH" || \ [ ! -z "$BRANCH_NAME" ] && echo "$BRANCH_NAME") commit=$([ ! -z "$ghprbActualCommit" ] && echo "$ghprbActualCommit" || \ echo "$GIT_COMMIT") build="$BUILD_NUMBER" pr="${ghprbPullId:=$CHANGE_ID}" build_url=$(urlencode "$BUILD_URL") elif [ "$CI" = "true" ] && [ "$TRAVIS" = "true" ] && [ "$SHIPPABLE" != "true" ]; then say "$e==>$x Travis CI detected." # http://docs.travis-ci.com/user/ci-environment/#Environment-variables service="travis" branch="$TRAVIS_BRANCH" commit="$TRAVIS_COMMIT" build="$TRAVIS_JOB_NUMBER" pr="$TRAVIS_PULL_REQUEST" job="$TRAVIS_JOB_ID" slug="$TRAVIS_REPO_SLUG" tag="$TRAVIS_TAG" env="$env,TRAVIS_OS_NAME" language=$(printenv | grep "TRAVIS_.*_VERSION" | head -1) if [ "$language" != "" ]; then env="$env,${language%=*}" fi elif [ "$CI" = "true" ] && [ "$CI_NAME" = "codeship" ]; then say "$e==>$x Codeship CI detected." # https://www.codeship.io/documentation/continuous-integration/set-environment-variables/ service="codeship" branch="$CI_BRANCH" build="$CI_BUILD_NUMBER" build_url=$(urlencode "$CI_BUILD_URL") commit="$CI_COMMIT_ID" elif [ "$TEAMCITY_VERSION" != "" ]; then say "$e==>$x TeamCity CI detected." # https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters # https://confluence.jetbrains.com/plugins/servlet/mobile#content/view/74847298 if [ "$TEAMCITY_BUILD_BRANCH" = '' ]; then echo " Teamcity does not automatically make build parameters available as environment variables." echo " Add the following environment parameters to the build configuration" echo " env.TEAMCITY_BUILD_BRANCH = %teamcity.build.branch%" echo " env.TEAMCITY_BUILD_ID = %teamcity.build.id%" echo " env.TEAMCITY_BUILD_URL = %teamcity.serverUrl%/viewLog.html?buildId=%teamcity.build.id%" echo " env.TEAMCITY_BUILD_COMMIT = %system.build.vcs.number%" echo " env.TEAMCITY_BUILD_REPOSITORY = %vcsroot..url%" fi service="teamcity" branch="$TEAMCITY_BUILD_BRANCH" build="$TEAMCITY_BUILD_ID" build_url=$(urlencode "$TEAMCITY_BUILD_URL") if [ "$TEAMCITY_BUILD_COMMIT" != "" ]; then commit="$TEAMCITY_BUILD_COMMIT" else commit="$BUILD_VCS_NUMBER" fi remote_addr="$TEAMCITY_BUILD_REPOSITORY" elif [ "$CI" = "true" ] && [ "$CIRCLECI" = "true" ]; then say "$e==>$x Circle CI detected." # https://circleci.com/docs/environment-variables service="circleci" branch="$CIRCLE_BRANCH" build="$CIRCLE_BUILD_NUM" job="$CIRCLE_NODE_INDEX" if [ "$CIRCLE_PROJECT_REPONAME" != "" ]; then slug="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" else # git@github.com:owner/repo.git slug="${CIRCLE_REPOSITORY_URL##*:}" # owner/repo.git slug="${slug%%.git}" fi pr="$CIRCLE_PR_NUMBER" commit="$CIRCLE_SHA1" search_in="$search_in $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS" elif [ "$BUDDYBUILD_BRANCH" != "" ]; then say "$e==>$x buddybuild detected" # http://docs.buddybuild.com/v6/docs/custom-prebuild-and-postbuild-steps service="buddybuild" branch="$BUDDYBUILD_BRANCH" build="$BUDDYBUILD_BUILD_NUMBER" build_url="https://dashboard.buddybuild.com/public/apps/$BUDDYBUILD_APP_ID/build/$BUDDYBUILD_BUILD_ID" # BUDDYBUILD_TRIGGERED_BY if [ "$ddp" = "$(echo ~)/Library/Developer/Xcode/DerivedData" ]; then ddp="/private/tmp/sandbox/${BUDDYBUILD_APP_ID}/bbtest" fi elif [ "${bamboo_planRepository_revision}" != "" ]; then say "$e==>$x Bamboo detected" # https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html#Bamboovariables-Build-specificvariables service="bamboo" commit="${bamboo_planRepository_revision}" branch="${bamboo_planRepository_branch}" build="${bamboo_buildNumber}" build_url="${bamboo_buildResultsUrl}" remote_addr="${bamboo_planRepository_repositoryUrl}" elif [ "$CI" = "true" ] && [ "$BITRISE_IO" = "true" ]; then # http://devcenter.bitrise.io/faq/available-environment-variables/ say "$e==>$x Bitrise CI detected." service="bitrise" branch="$BITRISE_GIT_BRANCH" build="$BITRISE_BUILD_NUMBER" build_url=$(urlencode "$BITRISE_BUILD_URL") pr="$BITRISE_PULL_REQUEST" if [ "$GIT_CLONE_COMMIT_HASH" != "" ]; then commit="$GIT_CLONE_COMMIT_HASH" fi elif [ "$CI" = "true" ] && [ "$SEMAPHORE" = "true" ]; then say "$e==>$x Semaphore CI detected." # https://semaphoreapp.com/docs/available-environment-variables.html service="semaphore" branch="$BRANCH_NAME" build="$SEMAPHORE_BUILD_NUMBER" job="$SEMAPHORE_CURRENT_THREAD" pr="$PULL_REQUEST_NUMBER" slug="$SEMAPHORE_REPO_SLUG" commit="$REVISION" env="$env,SEMAPHORE_TRIGGER_SOURCE" elif [ "$CI" = "true" ] && [ "$BUILDKITE" = "true" ]; then say "$e==>$x Buildkite CI detected." # https://buildkite.com/docs/guides/environment-variables service="buildkite" branch="$BUILDKITE_BRANCH" build="$BUILDKITE_BUILD_NUMBER" job="$BUILDKITE_JOB_ID" build_url=$(urlencode "$BUILDKITE_BUILD_URL") slug="$BUILDKITE_PROJECT_SLUG" commit="$BUILDKITE_COMMIT" elif [ "$CI" = "true" ] && [ "$DRONE" = "true" ]; then say "$e==>$x Drone CI detected." # http://docs.drone.io/env.html # drone commits are not full shas service="drone.io" branch="$DRONE_BRANCH" build="$DRONE_BUILD_NUMBER" build_url=$(urlencode "${DRONE_BUILD_URL:-$CI_BUILD_URL}") pr="$DRONE_PULL_REQUEST" job="$DRONE_JOB_NUMBER" tag="$DRONE_TAG" elif [ "$HEROKU_TEST_RUN_BRANCH" != "" ]; then say "$e==>$x Heroku CI detected." # https://devcenter.heroku.com/articles/heroku-ci#environment-variables service="heroku" branch="$HEROKU_TEST_RUN_BRANCH" build="$HEROKU_TEST_RUN_ID" elif [ "$CI" = "True" ] && [ "$APPVEYOR" = "True" ]; then say "$e==>$x Appveyor CI detected." # http://www.appveyor.com/docs/environment-variables service="appveyor" branch="$APPVEYOR_REPO_BRANCH" build=$(urlencode "$APPVEYOR_JOB_ID") pr="$APPVEYOR_PULL_REQUEST_NUMBER" job="$APPVEYOR_ACCOUNT_NAME%2F$APPVEYOR_PROJECT_SLUG%2F$APPVEYOR_BUILD_VERSION" slug="$APPVEYOR_REPO_NAME" commit="$APPVEYOR_REPO_COMMIT" elif [ "$CI" = "true" ] && [ "$WERCKER_GIT_BRANCH" != "" ]; then say "$e==>$x Wercker CI detected." # http://devcenter.wercker.com/articles/steps/variables.html service="wercker" branch="$WERCKER_GIT_BRANCH" build="$WERCKER_MAIN_PIPELINE_STARTED" slug="$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY" commit="$WERCKER_GIT_COMMIT" elif [ "$CI" = "true" ] && [ "$MAGNUM" = "true" ]; then say "$e==>$x Magnum CI detected." # https://magnum-ci.com/docs/environment service="magnum" branch="$CI_BRANCH" build="$CI_BUILD_NUMBER" commit="$CI_COMMIT" elif [ "$CI" = "true" ] && [ "$SNAP_CI" = "true" ]; then say "$e==>$x Snap CI detected." # https://docs.snap-ci.com/environment-variables/ service="snap" branch=$([ "$SNAP_BRANCH" != "" ] && echo "$SNAP_BRANCH" || echo "$SNAP_UPSTREAM_BRANCH") build="$SNAP_PIPELINE_COUNTER" job="$SNAP_STAGE_NAME" pr="$SNAP_PULL_REQUEST_NUMBER" commit=$([ "$SNAP_COMMIT" != "" ] && echo "$SNAP_COMMIT" || echo "$SNAP_UPSTREAM_COMMIT") env="$env,DISPLAY" elif [ "$SHIPPABLE" = "true" ]; then say "$e==>$x Shippable CI detected." # http://docs.shippable.com/ci_configure/ service="shippable" branch=$([ "$HEAD_BRANCH" != "" ] && echo "$HEAD_BRANCH" || echo "$BRANCH") build="$BUILD_NUMBER" build_url=$(urlencode "$BUILD_URL") pr="$PULL_REQUEST" slug="$REPO_FULL_NAME" commit="$COMMIT" elif [ "$TDDIUM" = "true" ]; then say "Solano CI detected." # http://docs.solanolabs.com/Setup/tddium-set-environment-variables/ service="solano" commit="$TDDIUM_CURRENT_COMMIT" branch="$TDDIUM_CURRENT_BRANCH" build="$TDDIUM_TID" pr="$TDDIUM_PR_ID" elif [ "$GREENHOUSE" = "true" ]; then say "$e==>$x Greenhouse CI detected." # http://docs.greenhouseci.com/docs/environment-variables-files service="greenhouse" branch="$GREENHOUSE_BRANCH" build="$GREENHOUSE_BUILD_NUMBER" build_url=$(urlencode "$GREENHOUSE_BUILD_URL") pr="$GREENHOUSE_PULL_REQUEST" commit="$GREENHOUSE_COMMIT" search_in="$search_in $GREENHOUSE_EXPORT_DIR" elif [ "$GITLAB_CI" != "" ]; then say "$e==>$x GitLab CI detected." # http://doc.gitlab.com/ce/ci/variables/README.html service="gitlab" branch="$CI_BUILD_REF_NAME" build="$CI_BUILD_ID" remote_addr="${CI_BUILD_REPO:-$CI_REPOSITORY_URL}" commit="$CI_BUILD_REF" else say "${r}x>${x} No CI provider detected." say " Testing inside Docker? ${b}http://docs.codecov.io/docs/testing-with-docker${x}" say " Testing with Tox? ${b}https://docs.codecov.io/docs/python#section-testing-with-tox${x}" fi say " ${e}project root:${x} $git_root" # find branch, commit, repo from git command if [ "$GIT_BRANCH" != "" ]; then branch="$GIT_BRANCH" elif [ "$branch" = "" ]; then branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || hg branch 2>/dev/null || echo "") if [ "$branch" = "HEAD" ]; then branch="" fi fi if [ "$commit_o" = "" ]; then # merge commit -> actual commit mc=$(git log -1 --pretty=%B 2>/dev/null | tr -d '[[:space:]]' || true) if [[ "$mc" =~ ^Merge[[:space:]][a-z0-9]{40}[[:space:]]into[[:space:]][a-z0-9]{40}$ ]]; then # Merge xxx into yyy say " Fixing merge commit sha" commit=$(echo "$mc" | cut -d' ' -f2) elif [ "$GIT_COMMIT" != "" ]; then commit="$GIT_COMMIT" elif [ "$commit" = "" ]; then commit=$(git log -1 --format="%H" 2>/dev/null || hg id -i --debug 2>/dev/null | tr -d '+' || echo "") fi else commit="$commit_o" fi if [ "$CODECOV_TOKEN" != "" ] && [ "$token" = "" ]; then say "${e}-->${x} token set from env" token="$CODECOV_TOKEN" fi if [ "$CODECOV_URL" != "" ] && [ "$url_o" = "" ]; then say "${e}-->${x} url set from env" url_o=$(echo "$CODECOV_URL" | sed -e 's/\/$//') fi if [ "$CODECOV_SLUG" != "" ]; then say "${e}-->${x} slug set from env" slug_o="$CODECOV_SLUG" elif [ "$slug" = "" ]; then if [ "$remote_addr" = "" ]; then remote_addr=$(git config --get remote.origin.url || hg paths default || echo '') fi if [ "$remote_addr" != "" ]; then if echo "$remote_addr" | grep -q "//"; then # https slug=$(echo "$remote_addr" | cut -d / -f 4,5 | sed -e 's/\.git$//') else # ssh slug=$(echo "$remote_addr" | cut -d : -f 2 | sed -e 's/\.git$//') fi fi if [ "$slug" = "/" ]; then slug="" fi fi yaml=$(cd "$git_root" && \ git ls-files "*codecov.yml" "*codecov.yaml" 2>/dev/null \ || hg locate "*codecov.yml" "*codecov.yaml" 2>/dev/null \ || echo '') yaml=$(echo "$yaml" | head -1) if [ "$yaml" != "" ]; then say " ${e}Yaml found at:${x} $yaml" config=$(parse_yaml "$git_root/$yaml" || echo '') # TODO validate the yaml here if [ "$(echo "$config" | grep 'codecov_token="')" != "" ] && [ "$token" = "" ]; then say "${e}-->${x} token set from yaml" token="$(echo "$config" | grep 'codecov_token="' | sed -e 's/codecov_token="//' | sed -e 's/"\.*//')" fi if [ "$(echo "$config" | grep 'codecov_url="')" != "" ] && [ "$url_o" = "" ]; then say "${e}-->${x} url set from yaml" url_o="$(echo "$config" | grep 'codecov_url="' | sed -e 's/codecov_url="//' | sed -e 's/"\.*//')" fi if [ "$(echo "$config" | grep 'codecov_slug="')" != "" ] && [ "$slug_o" = "" ]; then say "${e}-->${x} slug set from yaml" slug_o="$(echo "$config" | grep 'codecov_slug="' | sed -e 's/codecov_slug="//' | sed -e 's/"\.*//')" fi else say " ${g}Yaml not found, that's ok! Learn more at${x} ${b}http://docs.codecov.io/docs/codecov-yaml${x}" fi if [ "$branch_o" != "" ]; then branch=$(urlencode "$branch_o") else branch=$(urlencode "$branch") fi query="branch=$branch\ &commit=$commit\ &build=$([ "$build_o" = "" ] && echo "$build" || echo "$build_o")\ &build_url=$build_url\ &name=$(urlencode "$name")\ &tag=$([ "$tag_o" = "" ] && echo "$tag" || echo "$tag_o")\ &slug=$([ "$slug_o" = "" ] && urlencode "$slug" || urlencode "$slug_o")\ &yaml=$(urlencode "$yaml")\ &service=$service\ &flags=$flags\ &pr=$([ "$pr_o" = "" ] && echo "${pr##\#}" || echo "${pr_o##\#}")\ &job=$job" if [ "$ft_search" = "1" ]; then # detect bower comoponents location bower_components="bower_components" bower_rc=$(cd "$git_root" && cat .bowerrc 2>/dev/null || echo "") if [ "$bower_rc" != "" ]; then bower_components=$(echo "$bower_rc" | tr -d '\n' | grep '"directory"' | cut -d'"' -f4 | sed -e 's/\/$//') if [ "$bower_components" = "" ]; then bower_components="bower_components" fi fi # Swift Coverage if [ "$ft_xcode" = "1" ] && [ -d "$ddp" ]; then say "${e}==>${x} Processing Xcode reports" say " DerivedData folder: $ddp" profdata_files=$(find "$ddp" -name '*.profdata' 2>/dev/null || echo '') if [ "$profdata_files" != "" ]; then # xcode via profdata if [ "$xp" = "" ]; then # xp=$(xcodebuild -showBuildSettings 2>/dev/null | grep -i "^\s*PRODUCT_NAME" | sed -e 's/.*= \(.*\)/\1/') # say " ${e}->${x} Speed up Xcode processing by adding ${e}-J '$xp'${x}" say " ${g}hint${x} Speed up Swift processing by using use ${g}-J 'AppName'${x} (regexp accepted)" say " ${g}hint${x} This will remove Pods/ from your report. Also ${b}https://docs.codecov.io/docs/ignoring-paths${x}" fi while read -r profdata; do if [ "$profdata" != "" ]; then swiftcov "$profdata" "$xp" fi done <<< "$profdata_files" else say " ${e}->${x} No Swift coverage found" fi # Obj-C Gcov Coverage if [ "$ft_gcov" = "1" ]; then say " ${e}->${x} Running $gcov_exe for Obj-C" bash -c "find $ddp -type f -name '*.gcda' $gcov_include $gcov_ignore -exec $gcov_exe -pbcu $gcov_arg {} +" || true fi fi if [ "$ft_xcodeplist" = "1" ] && [ -d "$ddp" ]; then say "${e}==>${x} Processing Xcode plists" plists_files=$(find "$ddp" -name '*.xccoverage' 2>/dev/null || echo '') if [ "$plists_files" != "" ]; then while read -r plist; do if [ "$plist" != "" ]; then say " ${g}Found${x} plist file at $plist" plutil -convert xml1 -o "$(basename "$plist").plist" -- $plist fi done <<< "$plists_files" fi fi # Gcov Coverage if [ "$ft_gcov" = "1" ]; then say "${e}==>${x} Running gcov in $proj_root ${e}(disable via -X gcov)${x}" bash -c "find $proj_root -type f -name '*.gcno' $gcov_include $gcov_ignore -exec $gcov_exe -pb $gcov_arg {} +" || true else say "${e}==>${x} gcov disabled" fi # Python Coverage if [ "$ft_coveragepy" = "1" ]; then if [ ! -f coverage.xml ]; then if which coverage >/dev/null 2>&1; then say "${e}==>${x} Python coveragepy exists ${e}disable via -X coveragepy${x}" dotcoverage=$(find "$git_root" -name '.coverage' -or -name '.coverage.*' | head -1 || echo '') if [ "$dotcoverage" != "" ]; then cd "$(dirname "$dotcoverage")" if [ ! -f .coverage ]; then say " ${e}->${x} Running coverage combine" coverage combine fi say " ${e}->${x} Running coverage xml" if [ "$(coverage xml -i)" != "No data to report." ]; then files="$files $PWD/coverage.xml" else say " ${r}No data to report.${x}" fi cd "$proj_root" else say " ${r}No .coverage file found.${x}" fi else say "${e}==>${x} Python coveragepy not found" fi fi else say "${e}==>${x} Python coveragepy disabled" fi if [ "$search_in_o" != "" ]; then # location override search_in="$search_in_o" fi say "$e==>$x Searching for coverage reports in:" for _path in $search_in do say " ${g}+${x} $_path" done patterns="find $search_in -type f \( -name '*coverage*.*' \ -or -name 'nosetests.xml' \ -or -name 'jacoco*.xml' \ -or -name 'clover.xml' \ -or -name 'report.xml' \ -or -name '*.codecov.*' \ -or -name 'codecov.*' \ -or -name 'cobertura.xml' \ -or -name 'excoveralls.json' \ -or -name 'luacov.report.out' \ -or -name 'coverage-final.json' \ -or -name 'naxsi.info' \ -or -name 'lcov.info' \ -or -name 'lcov.dat' \ -or -name '*.lcov' \ -or -name '*.clover' \ -or -name 'cover.out' \ -or -name 'gcov.info' \ -or -name '*.gcov' \ -or -name '*.lst' \ $include_cov \) \ $exclude_cov \ -not -name '*.profdata' \ -not -name 'coverage-summary.json' \ -not -name 'phpunit-code-coverage.xml' \ -not -name 'remapInstanbul.coverage*.json' \ -not -name 'phpunit-coverage.xml' \ -not -name '*codecov.yml' \ -not -name '*.serialized' \ -not -name '.coverage*' \ -not -name '.*coveragerc' \ -not -name '*.sh' \ -not -name '*.bat' \ -not -name '*.ps1' \ -not -name '*.cmake' \ -not -name '*.dox' \ -not -name '*.ec' \ -not -name '*.rst' \ -not -name '*.h' \ -not -name '*.scss' \ -not -name '*.o' \ -not -name '*.proto' \ -not -name '*.sbt' \ -not -name '*.xcoverage.*' \ -not -name '*.gz' \ -not -name '*.conf' \ -not -name '*.p12' \ -not -name '*.csv' \ -not -name '*.rsp' \ -not -name '*.m4' \ -not -name '*.pem' \ -not -name '*~' \ -not -name '*.exe' \ -not -name '*.am' \ -not -name '*.template' \ -not -name '*.cp' \ -not -name '*.bw' \ -not -name '*.crt' \ -not -name '*.log' \ -not -name '*.cmake' \ -not -name '*.pth' \ -not -name '*.in' \ -not -name '*.jar*' \ -not -name '*.pom*' \ -not -name '*.png' \ -not -name '*.jpg' \ -not -name '*.sql' \ -not -name '*.jpeg' \ -not -name '*.svg' \ -not -name '*.gif' \ -not -name '*.csv' \ -not -name '*.snapshot' \ -not -name '*.mak*' \ -not -name '*.bash' \ -not -name '*.data' \ -not -name '*.py' \ -not -name '*.class' \ -not -name '*.xcconfig' \ -not -name '*.ec' \ -not -name '*.coverage' \ -not -name '*.pyc' \ -not -name '*.cfg' \ -not -name '*.egg' \ -not -name '*.ru' \ -not -name '*.css' \ -not -name '*.less' \ -not -name '*.pyo' \ -not -name '*.whl' \ -not -name '*.html' \ -not -name '*.ftl' \ -not -name '*.erb' \ -not -name '*.rb' \ -not -name '*.js' \ -not -name '*.jade' \ -not -name '*.db' \ -not -name '*.md' \ -not -name '*.cpp' \ -not -name '*.gradle' \ -not -name '*.tar.tz' \ -not -name '*.scss' \ -not -name 'include.lst' \ -not -name 'fullLocaleNames.lst' \ -not -name 'inputFiles.lst' \ -not -name 'createdFiles.lst' \ -not -name 'scoverage.measurements.*' \ -not -name 'test_*_coverage.txt' \ -not -name 'testrunner-coverage*' \ -not -path '*/vendor/*' \ -not -path '*/htmlcov/*' \ -not -path '*/virtualenv/*' \ -not -path '*/js/generated/coverage/*' \ -not -path '*/.virtualenv/*' \ -not -path '*/virtualenvs/*' \ -not -path '*/.virtualenvs/*' \ -not -path '*/.env/*' \ -not -path '*/.envs/*' \ -not -path '*/env/*' \ -not -path '*/envs/*' \ -not -path '*/.venv/*' \ -not -path '*/.venvs/*' \ -not -path '*/venv/*' \ -not -path '*/venvs/*' \ -not -path '*/.git/*' \ -not -path '*/.hg/*' \ -not -path '*/.tox/*' \ -not -path '*/__pycache__/*' \ -not -path '*/.egg-info*' \ -not -path '*/$bower_components/*' \ -not -path '*/node_modules/*' \ -not -path '*/conftest_*.c.gcov' 2>/dev/null" files=$(eval "$patterns" || echo '') elif [ "$include_cov" != "" ]; then files=$(eval "find $search_in -type f \( ${include_cov:5} \)$exclude_cov 2>/dev/null" || echo '') fi num_of_files=$(echo "$files" | wc -l | tr -d ' ') if [ "$num_of_files" != '' ] && [ "$files" != '' ]; then say " ${e}->${x} Found $num_of_files reports" fi # no files found if [ "$files" = "" ]; then say "${r}-->${x} No coverage report found." say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" exit ${exit_with}; fi if [ "$ft_network" == "1" ]; then say "${e}==>${x} Detecting git/mercurial file structure" network=$(cd "$git_root" && git ls-files 2>/dev/null || hg locate 2>/dev/null || echo "") if [ "$network" = "" ]; then network=$(find "$git_root" -type f \ -not -path '*/virtualenv/*' \ -not -path '*/.virtualenv/*' \ -not -path '*/virtualenvs/*' \ -not -path '*/.virtualenvs/*' \ -not -path '*.png' \ -not -path '*.gif' \ -not -path '*.jpg' \ -not -path '*.jpeg' \ -not -path '*.md' \ -not -path '*/.env/*' \ -not -path '*/.envs/*' \ -not -path '*/env/*' \ -not -path '*/envs/*' \ -not -path '*/.venv/*' \ -not -path '*/.venvs/*' \ -not -path '*/venv/*' \ -not -path '*/venvs/*' \ -not -path '*/build/lib/*' \ -not -path '*/.git/*' \ -not -path '*/.egg-info/*' \ -not -path '*/shunit2-2.1.6/*' \ -not -path '*/vendor/*' \ -not -path '*/js/generated/coverage/*' \ -not -path '*/__pycache__/*' \ -not -path '*/node_modules/*' \ -not -path "*/$bower_components/*" 2>/dev/null || echo '') fi fi upload_file=`mktemp /tmp/codecov.XXXXXX` adjustments_file=`mktemp /tmp/codecov.adjustments.XXXXXX` cleanup() { rm -f $upload_file $adjustments_file } trap cleanup INT ABRT TERM if [ "$env" != "" ]; then inc_env="" say "${e}==>${x} Appending build variables" for varname in $(echo "$env" | tr ',' ' ') do if [ "$varname" != "" ]; then say " ${g}+${x} $varname" inc_env="${inc_env}${varname}=$(eval echo "\$${varname}") " fi done echo "$inc_env<<<<<< ENV" >> $upload_file fi if [ "$ft_network" == "1" ]; then i="woff|eot|otf" # fonts i="$i|gif|png|jpg|jpeg|psd" # images i="$i|ptt|pptx|numbers|pages|md|txt|xlsx|docx|doc|pdf" # docs i="$i|yml|yaml|.gitignore" # supporting docs echo "$network" | grep -vwE "($i)$" >> $upload_file echo "<<<<<< network" >> $upload_file fi fr=0 say "${e}==>${x} Reading reports" while IFS='' read -r file; do # read the coverage file if [ "$(echo "$file" | tr -d ' ')" != '' ]; then if [ -f "$file" ]; then report_len=$(wc -c < "$file") if [ "$report_len" -ne 0 ]; then say " ${g}+${x} $file ${e}bytes=$(echo "$report_len" | tr -d ' ')${x}" # append to to upload echo "# path=$(echo "$file" | sed "s|^$git_root/||")" >> $upload_file cat "$file" >> $upload_file echo "<<<<<< EOF" >> $upload_file fr=1 if [ "$clean" = "1" ]; then rm "$file" fi else say " ${r}-${x} Skipping empty file $file" fi else say " ${r}-${x} file not found at $file" fi fi done <<< "$(echo -e "$files")" if [ "$fr" = "0" ]; then say "${r}-->${x} No coverage data found." say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" say " search for your projects language to learn how to collect reports." exit ${exit_with}; fi if [ "$ft_fix" = "1" ]; then say "${e}==>${x} Appending adjustments" say " ${b}http://docs.codecov.io/docs/fixing-reports${x}" empty_line='^[[:space:]]*$' # // syntax_comment='^[[:space:]]*//.*' # /* or */ syntax_comment_block='^[[:space:]]*(\/\*|\*\/)[[:space:]]*$' # { or } syntax_bracket='^[[:space:]]*[\{\}][[:space:]]*(//.*)?$' # [ or ] syntax_list='^[[:space:]]*[][][[:space:]]*(//.*)?$' skip_dirs="-not -path '*/$bower_components/*' \ -not -path '*/node_modules/*'" cut_and_join() { awk 'BEGIN { FS=":" } $3 ~ /\/\*/ || $3 ~ /\*\// { print $0 ; next } $1!=key { if (key!="") print out ; key=$1 ; out=$1":"$2 ; next } { out=out","$2 } END { print out }' 2>/dev/null } if echo "$network" | grep -m1 '.kt$' 1>/dev/null; then # skip brackets and comments find "$git_root" -type f \ -name '*.kt' \ -exec \ grep -nIHE -e $syntax_bracket \ -e $syntax_comment_block {} \; \ | cut_and_join \ >> $adjustments_file \ || echo '' # last line in file find "$git_root" -type f \ -name '*.kt' -exec \ wc -l {} \; \ | while read l; do echo "EOF: $l"; done \ 2>/dev/null \ >> $adjustments_file \ || echo '' fi if echo "$network" | grep -m1 '.go$' 1>/dev/null; then # skip empty lines, comments, and brackets find "$git_root" -type f \ -not -path '*/vendor/*' \ -name '*.go' \ -exec \ grep -nIHE \ -e $empty_line \ -e $syntax_comment \ -e $syntax_comment_block \ -e $syntax_bracket \ {} \; \ | cut_and_join \ >> $adjustments_file \ || echo '' fi if echo "$network" | grep -m1 '.jsx$' 1>/dev/null; then # skip empty lines, comments, and brackets find "$git_root" -type f \ -name '*.jsx' \ $skip_dirs \ -exec \ grep -nIHE \ -e $empty_line \ -e $syntax_comment \ -e $syntax_bracket \ {} \; \ | cut_and_join \ >> $adjustments_file \ || echo '' fi if echo "$network" | grep -m1 '.php$' 1>/dev/null; then # skip empty lines, comments, and brackets find "$git_root" -type f \ -not -path '*/vendor/*' \ -name '*.php' \ -exec \ grep -nIHE \ -e $syntax_list \ -e $syntax_bracket \ -e '^[[:space:]]*\);[[:space:]]*(//.*)?$' \ {} \; \ | cut_and_join \ >> $adjustments_file \ || echo '' fi if echo "$network" | grep -m1 '\(.cpp\|.h\|.cxx\|.c\|.hpp\|.m\)$' 1>/dev/null; then # skip brackets find "$git_root" -type f \ $skip_dirs \ \( \ -name '*.h' \ -or -name '*.cpp' \ -or -name '*.cxx' \ -or -name '*.m' \ -or -name '*.c' \ -or -name '*.hpp' \ \) -exec \ grep -nIHE \ -e $empty_line \ -e $syntax_bracket \ -e '// LCOV_EXCL' \ {} \; \ | cut_and_join \ >> $adjustments_file \ || echo '' # skip brackets find "$git_root" -type f \ $skip_dirs \ \( \ -name '*.h' \ -or -name '*.cpp' \ -or -name '*.cxx' \ -or -name '*.m' \ -or -name '*.c' \ -or -name '*.hpp' \ \) -exec \ grep -nIH '// LCOV_EXCL' \ {} \; \ >> $adjustments_file \ || echo '' fi found=$(cat $adjustments_file | tr -d ' ') if [ "$found" != "" ]; then say " ${g}+${x} Found adjustments" echo "# path=fixes" >> $upload_file cat $adjustments_file >> $upload_file echo "<<<<<< EOF" >> $upload_file rm -rf $adjustments_file else say " ${e}->${x} No adjustments found" fi fi if [ "$url_o" != "" ]; then url="$url_o" fi if [ "$dump" != "0" ]; then # trim whitespace from query echo "$url/upload/v4?$(echo "package=bash-$VERSION&token=$token&$query" | tr -d ' ')" cat $upload_file else query=$(echo "${query}" | tr -d ' ') say "${e}==>${x} Uploading reports" say " ${e}url:${x} $url" say " ${e}query:${x} $query" # now add token to query query=$(echo "package=bash-$VERSION&token=$token&$query" | tr -d ' ') if [ "$ft_s3" = "1" ]; then i="0" while [ $i -lt 4 ] do i=$[$i+1] say " ${e}->${x} Pinging Codecov" res=$(curl $curl_s -X POST $curlargs $cacert "$url/upload/v4?$query" -H 'Accept: text/plain' || true) # a good replay is "https://codecov.io" + "\n" + "https://codecov.s3.amazonaws.com/..." status=$(echo "$res" | head -1 | grep 'HTTP ' | cut -d' ' -f2) if [ "$status" = "" ]; then s3target=$(echo "$res" | sed -n 2p) say " ${e}->${x} Uploading to S3 $(echo "$s3target" | cut -c1-32)" s3=$(curl $curl_s -fiX PUT $curlawsargs \ --data-binary @$upload_file \ -H 'Content-Type: text/plain' \ -H 'x-amz-acl: public-read' \ -H 'x-amz-storage-class: REDUCED_REDUNDANCY' \ "$s3target" || true) if [ "$s3" != "" ]; then say " ${g}->${x} View reports at ${b}$(echo "$res" | sed -n 1p)${x}" exit 0 else say " ${r}X>${x} Failed to upload to S3" fi elif [ "$status" = "400" ]; then # 400 Error say "${g}${res}${x}" exit ${exit_with} fi say " ${e}->${x} Sleeping for 30s and trying again..." sleep 30 done fi say " ${e}->${x} Uploading to Codecov" i="0" while [ $i -lt 4 ] do i=$[$i+1] res=$(curl $curl_s -X POST $curlargs $cacert --data-binary @$upload_file "$url/upload/v2?$query" -H 'Accept: text/plain' || echo 'HTTP 500') # HTTP 200 # http://.... status=$(echo "$res" | head -1 | cut -d' ' -f2) if [ "$status" = "" ]; then say " View reports at ${b}$(echo "$res" | head -2 | tail -1)${x}" exit 0 elif [ "${status:0:1}" = "5" ]; then say " ${e}->${x} Sleeping for 30s and trying again..." sleep 30 else say " ${g}${res}${x}" exit 0 exit ${exit_with} fi done fi say " ${r}X> Failed to upload coverage reports${x}" exit ${exit_with} # EOF ================================================ FILE: detekt.yml ================================================ # `git diff detekt-default detekt.yml` to see what's changed from default # settings test-pattern: # Configure exclusions for test sources patterns: # Test file regexes - '.*/test/.*' exclude-rule-sets: - 'comments' exclude-rules: - 'NamingRules' - 'WildcardImport' - 'MagicNumber' - 'MaxLineLength' - 'LateinitUsage' - 'StringLiteralDuplication' - 'SpreadOperator' - 'TooManyFunctions' - 'ForEachOnRange' build: maxIssues: 10 # TODO: reduce this threshhold weights: # complexity: 2 # LongParameterList: 1 # style: 1 # comments: 1 processors: active: true exclude: # - 'FunctionCountProcessor' # - 'PropertyCountProcessor' # - 'ClassCountProcessor' # - 'PackageCountProcessor' # - 'KtFileCountProcessor' console-reports: active: true exclude: # - 'ProjectStatisticsReport' # - 'ComplexityReport' # - 'NotificationReport' # - 'FindingsReport' # - 'BuildFailureReport' output-reports: active: true exclude: # - 'HtmlOutputReport' # - 'PlainOutputReport' # - 'XmlOutputReport' comments: active: true CommentOverPrivateFunction: active: false CommentOverPrivateProperty: active: false EndOfSentenceFormat: active: false endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$) UndocumentedPublicClass: active: false searchInNestedClass: true searchInInnerClass: true searchInInnerObject: true searchInInnerInterface: true UndocumentedPublicFunction: active: false complexity: active: true ComplexCondition: active: true threshold: 4 ComplexInterface: active: false threshold: 10 includeStaticDeclarations: false ComplexMethod: active: true threshold: 10 ignoreSingleWhenExpression: false LabeledExpression: active: false LargeClass: active: true threshold: 150 LongMethod: active: true threshold: 20 LongParameterList: active: true threshold: 6 ignoreDefaultParameters: false MethodOverloading: active: false threshold: 6 NestedBlockDepth: active: true threshold: 4 StringLiteralDuplication: active: false threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: true thresholdInFiles: 11 thresholdInClasses: 11 thresholdInInterfaces: 11 thresholdInObjects: 11 thresholdInEnums: 11 ignoreDeprecated: false empty-blocks: active: true EmptyCatchBlock: active: true allowedExceptionNameRegex: "^(_|(ignore|expected).*)" EmptyClassBlock: active: true EmptyDefaultConstructor: active: true EmptyDoWhileBlock: active: true EmptyElseBlock: active: true EmptyFinallyBlock: active: true EmptyForBlock: active: true EmptyFunctionBlock: active: true ignoreOverriddenFunctions: false EmptyIfBlock: active: true EmptyInitBlock: active: true EmptyKtFile: active: true EmptySecondaryConstructor: active: true EmptyWhenBlock: active: true EmptyWhileBlock: active: true exceptions: active: true ExceptionRaisedInUnexpectedLocation: active: false methodNames: 'toString,hashCode,equals,finalize' InstanceOfCheckForException: active: false NotImplementedDeclaration: active: false PrintStackTrace: active: false RethrowCaughtException: active: false ReturnFromFinally: active: false SwallowedException: active: false ThrowingExceptionFromFinally: active: false ThrowingExceptionInMain: active: false ThrowingExceptionsWithoutMessageOrCause: active: false exceptions: 'IllegalArgumentException,IllegalStateException,IOException' ThrowingNewInstanceOfSameException: active: false TooGenericExceptionCaught: active: true exceptionNames: - ArrayIndexOutOfBoundsException - Error - Exception - IllegalMonitorStateException - NullPointerException - IndexOutOfBoundsException - RuntimeException - Throwable TooGenericExceptionThrown: active: true exceptionNames: - Error - Exception - Throwable - RuntimeException formatting: active: true android: false autoCorrect: true ChainWrapping: active: true autoCorrect: true CommentSpacing: active: true autoCorrect: true Filename: active: true FinalNewline: active: true autoCorrect: true ImportOrdering: active: true autoCorrect: true Indentation: active: true autoCorrect: true indentSize: 4 continuationIndentSize: 4 MaximumLineLength: active: true maxLineLength: 120 ModifierOrdering: active: true autoCorrect: true NoBlankLineBeforeRbrace: active: true autoCorrect: true NoConsecutiveBlankLines: active: true autoCorrect: true NoEmptyClassBody: active: true autoCorrect: true NoItParamInMultilineLambda: active: true NoLineBreakAfterElse: active: true autoCorrect: true NoLineBreakBeforeAssignment: active: true autoCorrect: true NoMultipleSpaces: active: true autoCorrect: true NoSemicolons: active: true autoCorrect: true NoTrailingSpaces: active: true autoCorrect: true NoUnitReturn: active: true autoCorrect: true NoUnusedImports: active: true autoCorrect: true NoWildcardImports: active: true autoCorrect: true ParameterListWrapping: active: true autoCorrect: true indentSize: 4 SpacingAroundColon: active: true autoCorrect: true SpacingAroundComma: active: true autoCorrect: true SpacingAroundCurly: active: true autoCorrect: true SpacingAroundKeyword: active: true autoCorrect: true SpacingAroundOperators: active: true autoCorrect: true SpacingAroundRangeOperator: active: true autoCorrect: true StringTemplate: active: true autoCorrect: true naming: active: true ClassNaming: active: true classPattern: '[A-Z$][a-zA-Z0-9$]*' EnumNaming: active: true enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false forbiddenName: '' FunctionMaxLength: active: false maximumFunctionNameLength: 30 FunctionMinLength: active: false minimumFunctionNameLength: 3 FunctionNaming: active: true functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' excludeClassPattern: '$^' MatchingDeclarationName: active: true MemberNameEqualsClassName: active: false ignoreOverriddenFunction: true ObjectPropertyNaming: active: true propertyPattern: '[A-Za-z][_A-Za-z0-9]*' constantPattern: '[A-Za-z][_A-Za-z0-9]*' PackageNaming: active: true packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$' TopLevelPropertyNaming: active: true constantPattern: '[A-Z][_A-Z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' VariableMaxLength: active: false maximumVariableNameLength: 64 VariableMinLength: active: false minimumVariableNameLength: 1 VariableNaming: active: true variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' performance: active: true ForEachOnRange: active: true SpreadOperator: active: false UnnecessaryTemporaryInstantiation: active: true potential-bugs: active: true DuplicateCaseInWhenExpression: active: true EqualsAlwaysReturnsTrueOrFalse: active: false EqualsWithHashCodeExist: active: true ExplicitGarbageCollectionCall: active: true InvalidRange: active: false IteratorHasNextCallsNextMethod: active: false IteratorNotThrowingNoSuchElementException: active: false LateinitUsage: active: false excludeAnnotatedProperties: "" ignoreOnClassesPattern: "" UnconditionalJumpStatementInLoop: active: false UnreachableCode: active: true UnsafeCallOnNullableType: active: false UnsafeCast: active: false UselessPostfixExpression: active: false WrongEqualsTypeParameter: active: false style: active: true CollapsibleIfStatements: active: false DataClassContainsFunctions: active: false conversionFunctionPrefix: 'to' EqualsNullCall: active: false ExpressionBodySyntax: active: false ForbiddenComment: active: true values: 'FIXME:,STOPSHIP:' ForbiddenImport: active: false imports: '' FunctionOnlyReturningConstant: active: false ignoreOverridableFunction: true excludedFunctions: 'describeContents' LoopWithTooManyJumpStatements: active: false maxJumpCount: 1 MagicNumber: active: true ignoreNumbers: '-1,0,1,2' ignoreHashCodeFunction: false ignorePropertyDeclaration: false ignoreConstantDeclaration: true ignoreCompanionObjectPropertyDeclaration: true ignoreAnnotation: false ignoreNamedArgument: true ignoreEnums: false MaxLineLength: active: true maxLineLength: 120 excludePackageStatements: false excludeImportStatements: false excludeCommentStatements: false MayBeConst: active: false ModifierOrder: active: true NestedClassesVisibility: active: false NewLineAtEndOfFile: active: true NoTabs: active: false OptionalAbstractKeyword: active: true OptionalUnit: active: false OptionalWhenBraces: active: false ProtectedMemberInFinalClass: active: false RedundantVisibilityModifierRule: active: false ReturnCount: active: true max: 2 excludedFunctions: "equals" SafeCast: active: true SerialVersionUIDInSerializableClass: active: false SpacingBetweenPackageAndImports: active: false ThrowsCount: active: true max: 2 TrailingWhitespace: active: false UnnecessaryAbstractClass: active: false UnnecessaryInheritance: active: false UnnecessaryParentheses: active: false UntilInsteadOfRangeTo: active: false UnusedImports: active: false UnusedPrivateMember: active: false allowedNames: "(_|ignored|expected)" UseDataClass: active: false excludeAnnotatedClasses: "" UtilityClassWithPublicConstructor: active: false WildcardImport: active: true excludeImports: 'java.util.*,kotlinx.android.synthetic.*' # vim: sw=2 ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ detekt_version = 1.0.0.RC7 dokka_version = 0.9.17 gradle_bintray_version = 1.8.0 kotlintest_version = 3.1.0 kotlin_version = 1.2.41 slf4j_version = 1.7.25 xenocom_version = 0.0.7 ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ rootProject.name = "kotlin-argparser" ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/ArgParser.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser import com.xenomachina.argparser.PosixNaming.identifierToArgName import com.xenomachina.argparser.PosixNaming.identifierToOptionName import com.xenomachina.argparser.PosixNaming.optionNameToArgName import com.xenomachina.argparser.PosixNaming.selectRepresentativeOptionName import com.xenomachina.common.Holder import com.xenomachina.common.orElse import java.util.LinkedHashSet import kotlin.reflect.KProperty /** * A command-line option/argument parser. * * @param args the command line arguments to parse * @param mode parsing mode, defaults to GNU-style parsing * @param helpFormatter if non-null, creates `--help` and `-h` options that trigger a [ShowHelpException] which will use * the supplied [HelpFormatter] to generate a help message. */ class ArgParser( args: Array, mode: Mode = Mode.GNU, helpFormatter: HelpFormatter? = DefaultHelpFormatter() ) { enum class Mode { /** For GNU-style option parsing, where options may appear after positional arguments. */ GNU, /** For POSIX-style option parsing, where options must appear before positional arguments. */ POSIX } /** * Creates a Delegate for a zero-argument option that returns true if and only the option is present in args. */ fun flagging(vararg names: String, help: String): Delegate = option( *names, help = help) { true }.default(false) /** * Creates a DelegateProvider for a zero-argument option that returns true if and only the option is present in * args. */ fun flagging(help: String) = DelegateProvider { identifier -> flagging(identifierToOptionName(identifier), help = help) } /** * Creates a Delegate for a zero-argument option that returns the count of how many times the option appears in * args. */ fun counting(vararg names: String, help: String): Delegate = option( *names, isRepeating = true, help = help) { value.orElse { 0 } + 1 }.default(0) /** * Creates a DelegateProvider for a zero-argument option that returns the count of how many times the option appears * in args. */ fun counting(help: String) = DelegateProvider { identifier -> counting(identifierToOptionName(identifier), help = help) } /** * Creates a Delegate for a single-argument option that stores and returns the option's (transformed) argument. */ fun storing( vararg names: String, help: String, argName: String? = null, transform: String.() -> T ): Delegate { val nonNullArgName = argName ?: optionNameToArgName(selectRepresentativeOptionName(names)) return option( *names, errorName = nonNullArgName, argNames = listOf(nonNullArgName), help = help) { transform(arguments.first()) } } /** * Creates a DelegateProvider for a single-argument option that stores and returns the option's (transformed) * argument. */ fun storing( help: String, argName: String? = null, transform: String.() -> T ) = DelegateProvider { identifier -> storing(identifierToOptionName(identifier), help = help, argName = argName, transform = transform) } /** * Creates a Delegate for a single-argument option that stores and returns the option's argument. */ fun storing(vararg names: String, help: String, argName: String? = null): Delegate = storing(*names, help = help, argName = argName) { this } /** * Creates a DelegateProvider for a single-argument option that stores and returns the option's argument. */ fun storing(help: String, argName: String? = null) = DelegateProvider { identifier -> storing(identifierToOptionName(identifier), help = help, argName = argName) } /** * Creates a Delegate for a single-argument option that adds the option's (transformed) argument to a * MutableCollection each time the option appears in args, and returns said MutableCollection. */ fun > adding( vararg names: String, help: String, argName: String? = null, initialValue: T, transform: String.() -> E ): Delegate { val nonNullArgName = argName ?: optionNameToArgName(selectRepresentativeOptionName(names)) return option( *names, help = help, argNames = listOf(nonNullArgName), isRepeating = true) { val result = value.orElse { initialValue } result.add(transform(arguments.first())) result }.default(initialValue) } /** * Creates a DelegateProvider for a single-argument option that adds the option's (transformed) argument to a * MutableCollection each time the option appears in args, and returns said MutableCollection. */ fun > adding( help: String, argName: String? = null, initialValue: T, transform: String.() -> E ) = DelegateProvider { identifier -> adding( identifierToOptionName(identifier), help = help, argName = argName, initialValue = initialValue, transform = transform) } /** * Creates a Delegate for a single-argument option that adds the option's (transformed) argument to a * MutableList each time the option appears in args, and returns said MutableCollection. */ fun adding( vararg names: String, help: String, argName: String? = null, transform: String.() -> T ) = adding(*names, help = help, argName = argName, initialValue = mutableListOf(), transform = transform) /** * Creates a DelegateProvider for a single-argument option that adds the option's (transformed) argument to a * MutableList each time the option appears in args, and returns said MutableCollection. */ fun adding( help: String, argName: String? = null, transform: String.() -> T ) = DelegateProvider { identifier -> adding(identifierToOptionName(identifier), help = help, argName = argName, transform = transform) } /** * Creates a Delegate for a single-argument option that adds the option's argument to a MutableList each time the * option appears in args, and returns said MutableCollection. */ fun adding(vararg names: String, help: String, argName: String? = null): Delegate> = adding(*names, help = help, argName = argName) { this } /** * Creates a DelegateProvider for a single-argument option that adds the option's argument to a MutableList each * time the option appears in args, and returns said MutableCollection. */ fun adding(help: String) = DelegateProvider { identifier -> adding(identifierToOptionName(identifier), help = help) } /** * Creates a Delegate for a zero-argument option that maps from the option's name as it appears in args to one of a * fixed set of values. */ fun mapping(vararg pairs: Pair, help: String): Delegate = mapping(mapOf(*pairs), help = help) /** * Creates a Delegate for a zero-argument option that maps from the option's name as it appears in args to one of a * fixed set of values. */ fun mapping(map: Map, help: String): Delegate { val names = map.keys.toTypedArray() return option(*names, errorName = map.keys.joinToString("|"), help = help) { // This cannot be null, because the optionName was added to the map // at the same time it was registered with the ArgParser. map[optionName]!! } } /** * Creates a Delegate for an option with the specified names. * @param names names of options, with leading "-" or "--" * @param errorName name to use when talking about this option in error messages, or null to base it upon the * option names * @param help the help text for this option * @param argNames names of this option's arguments * @param isRepeating whether or not it makes sense to repeat this option -- usually used for options where * specifying the option more than once yields a value than cannot be expressed by specifying the option only once * @param handler a function that computes the value of this option from an [OptionInvocation] */ fun option( // TODO: add optionalArg: Boolean vararg names: String, help: String, errorName: String? = null, argNames: List = emptyList(), isRepeating: Boolean = false, handler: OptionInvocation.() -> T ): Delegate { val delegate = OptionDelegate( parser = this, errorName = errorName ?: optionNameToArgName(selectRepresentativeOptionName(names)), help = help, optionNames = listOf(*names), argNames = argNames.toList(), isRepeating = isRepeating, handler = handler) return delegate } /** * Creates a Delegate for a single positional argument which returns the argument's value. */ fun positional(name: String, help: String) = positional(name, help = help) { this } /** * Creates a DelegateProvider for a single positional argument which returns the argument's value. */ fun positional(help: String) = DelegateProvider { identifier -> positional(identifierToArgName(identifier), help = help) } /** * Creates a Delegate for a single positional argument which returns the argument's transformed value. */ fun positional( name: String, help: String, transform: String.() -> T ): Delegate { return WrappingDelegate( positionalList(name, help = help, sizeRange = 1..1, transform = transform) ) { it[0] } } /** * Creates a DelegateProvider for a single positional argument which returns the argument's transformed value. */ fun positional( help: String, transform: String.() -> T ) = DelegateProvider { identifier -> positional(identifierToArgName(identifier), help = help, transform = transform) } /** * Creates a Delegate for a sequence of positional arguments which returns a List containing the arguments. */ fun positionalList( name: String, help: String, sizeRange: IntRange = 1..Int.MAX_VALUE ) = positionalList(name, help = help, sizeRange = sizeRange) { this } /** * Creates a DelegateProvider for a sequence of positional arguments which returns a List containing the arguments. */ fun positionalList( help: String, sizeRange: IntRange = 1..Int.MAX_VALUE ) = DelegateProvider { identifier -> positionalList(identifierToArgName(identifier), help = help, sizeRange = sizeRange) } /** * Creates a Delegate for a sequence of positional arguments which returns a List containing the transformed * arguments. */ fun positionalList( name: String, help: String, sizeRange: IntRange = 1..Int.MAX_VALUE, transform: String.() -> T ): Delegate> { sizeRange.run { require(step == 1) { "step must be 1, not $step" } require(first <= last) { "backwards ranges are not allowed: $first > $last" } require(first >= 0) { "sizeRange cannot start at $first, must be non-negative" } // Technically, last == 0 is ok but not especially useful, so we // disallow it as it's probably unintentional. require(last > 0) { "sizeRange only allows $last arguments, must allow at least 1" } } return PositionalDelegate(this, name, sizeRange, help = help, transform = transform) } /** * Creates a DelegateProvider for a sequence of positional arguments which returns a List containing the transformed * arguments. */ fun positionalList( help: String, sizeRange: IntRange = 1..Int.MAX_VALUE, transform: String.() -> T ) = DelegateProvider { identifier -> positionalList(identifierToArgName(identifier), help, sizeRange, transform) } abstract class Delegate internal constructor() { /** The value associated with this delegate */ abstract val value: T /** The name used to refer to this delegate's value in error messages */ abstract val errorName: String /** The user-visible help text for this delegate */ abstract val help: String /** Add validation logic. Validator should throw a [SystemExitException] on failure. */ abstract fun addValidator(validator: Delegate.() -> Unit): Delegate /** Allows this object to act as a property delegate */ operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value /** * Allows this object to act as a property delegate provider. * * It provides itself, and also registers itself with the [ArgParser] at that time. */ operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgParser.Delegate { registerRoot() return this } internal abstract val parser: ArgParser /** * Indicates whether or not a value has been set for this delegate */ internal abstract val hasValue: Boolean internal fun checkHasValue() { if (!hasValue) throw MissingValueException(errorName) } internal abstract fun validate() internal abstract fun toHelpFormatterValue(): HelpFormatter.Value internal fun registerRoot() { parser.checkNotParsed() parser.delegates.add(this) registerLeaf(this) } internal abstract fun registerLeaf(root: Delegate<*>) internal abstract val hasValidators: Boolean } /** * Provides a [Delegate] when given a name. This makes it possible to infer * a name for the `Delegate` based on the name it is bound to, rather than * specifying a name explicitly. */ class DelegateProvider( private val default: (() -> T)? = null, internal val ctor: (identifier: String) -> Delegate ) { operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): Delegate { val delegate = ctor(prop.name) return (if (default == null) delegate else delegate.default(default)).provideDelegate(thisRef, prop) } } /** * @property value a Holder containing the current value associated with this option, or null if unset * @property optionName the name used for this option in this invocation * @property arguments the arguments supplied for this option */ data class OptionInvocation internal constructor( // Internal constructor so future versions can add properties // without breaking compatibility. val value: Holder?, val optionName: String, val arguments: List ) private val shortOptionDelegates = mutableMapOf>() private val longOptionDelegates = mutableMapOf>() private val positionalDelegates = mutableListOf, Boolean>>() private val delegates = LinkedHashSet>() internal fun registerOption(name: String, delegate: OptionDelegate<*>) { if (name.startsWith("--")) { require(name.length > 2) { "long option '$name' must have at least one character after hyphen" } require(name !in longOptionDelegates) { "long option '$name' already in use" } longOptionDelegates.put(name, delegate) } else if (name.startsWith("-")) { require(name.length == 2) { "short option '$name' can only have one character after hyphen" } val key = name.get(1) require(key !in shortOptionDelegates) { "short option '$name' already in use" } shortOptionDelegates.put(key, delegate) } else { throw IllegalArgumentException("illegal option name '$name' -- must start with '-' or '--'") } } internal fun registerPositional(delegate: PositionalDelegate<*>, hasDefault: Boolean) { positionalDelegates.add(delegate to hasDefault) } private var inValidation = false private var finished = false /** * Ensures that arguments have been parsed and validated. * * @throws SystemExitException if parsing or validation failed. */ fun force() { if (!inParse) { if (!finished) { parseOptions if (!inValidation) { inValidation = true try { for (delegate in delegates) delegate.checkHasValue() for (delegate in delegates) delegate.validate() } finally { inValidation = false } } } } } /** * Provides an instance of T, where all arguments have already been parsed and validated */ fun parseInto(constructor: (ArgParser) -> T): T { if (builtinDelegateCount != delegates.size) { throw IllegalStateException("You can only use the parseInto function with a clean ArgParser instance") } val provided = constructor(this) force() return provided } private var inParse = false internal fun checkNotParsed() { if (inParse || finished) throw IllegalStateException("arguments have already been parsed") } private val parseOptions by lazy { val positionalArguments = mutableListOf() inParse = true try { var i = 0 optionLoop@ while (i < args.size) { val arg = args[i] i += when { arg == "--" -> { i++ break@optionLoop } arg.startsWith("--") -> parseLongOpt(i, args) arg.startsWith("-") -> parseShortOpts(i, args) else -> { positionalArguments.add(arg) when (mode) { Mode.GNU -> 1 Mode.POSIX -> { i++ break@optionLoop } } } } } // Collect remaining arguments as positional-only arguments positionalArguments.addAll(args.slice(i..args.size - 1)) parsePositionalArguments(positionalArguments) finished = true } finally { inParse = false } } private fun parsePositionalArguments(args: List) { var lastValueName: String? = null var index = 0 var remaining = args.size var extra = (remaining - positionalDelegates.map { if (it.second) 0 else it.first.sizeRange.first }.sum()).coerceAtLeast(0) for ((delegate, hasDefault) in positionalDelegates) { val minSize = if (hasDefault) 0 else delegate.sizeRange.first val sizeRange = delegate.sizeRange val chunkSize = (minSize + extra).coerceAtMost(sizeRange.endInclusive) if (chunkSize > remaining) { throw MissingRequiredPositionalArgumentException(delegate.errorName) } if (chunkSize != 0 || !hasDefault) { delegate.parseArguments(args.subList(index, index + chunkSize)) } lastValueName = delegate.errorName index += chunkSize remaining -= chunkSize extra -= chunkSize - minSize } if (remaining > 0) { throw UnexpectedPositionalArgumentException(lastValueName) } } /** * @param index index into args, starting at a long option, eg: "--verbose" * @param args array of command-line arguments * @return number of arguments that have been processed */ private fun parseLongOpt(index: Int, args: Array): Int { val name: String val firstArg: String? val m = NAME_EQUALS_VALUE_REGEX.matchEntire(args[index]) if (m == null) { name = args[index] firstArg = null } else { // if NAME_EQUALS_VALUE_REGEX then there must be groups 1 and 2 name = m.groups[1]!!.value firstArg = m.groups[2]!!.value } val delegate = longOptionDelegates.get(name) if (delegate == null) { throw UnrecognizedOptionException(name) } else { var consumedArgs = delegate.parseOption(name, firstArg, index + 1, args) if (firstArg != null) { if (consumedArgs < 1) throw UnexpectedOptionArgumentException(name) consumedArgs -= 1 } return 1 + consumedArgs } } /** * @param index index into args, starting at a set of short options, eg: "-abXv" * @param args array of command-line arguments * @return number of arguments that have been processed */ private fun parseShortOpts(index: Int, args: Array): Int { val opts = args[index] var optIndex = 1 while (optIndex < opts.length) { val optKey = opts[optIndex] val optName = "-$optKey" optIndex++ // optIndex now points just after optKey val delegate = shortOptionDelegates.get(optKey) if (delegate == null) { throw UnrecognizedOptionException(optName) } else { val firstArg = if (optIndex >= opts.length) null else opts.substring(optIndex) val consumed = delegate.parseOption(optName, firstArg, index + 1, args) if (consumed > 0) { return consumed + (if (firstArg == null) 1 else 0) } } } return 1 } init { if (helpFormatter != null) { option("-h", "--help", errorName = "HELP", // This should never be used, but we need to say something help = "show this help message and exit") { throw ShowHelpException(helpFormatter, delegates.toList()) }.default(Unit).registerRoot() } } private val builtinDelegateCount = delegates.size } private val NAME_EQUALS_VALUE_REGEX = Regex("^([^=]+)=(.*)$") internal val LEADING_HYPHENS_REGEX = Regex("^-{1,2}") private const val OPTION_CHAR_CLASS = "[a-zA-Z0-9]" internal val OPTION_NAME_RE = Regex("^(-$OPTION_CHAR_CLASS)|(--$OPTION_CHAR_CLASS+([-_\\.]$OPTION_CHAR_CLASS+)*)$") private const val ARG_INITIAL_CHAR_CLASS = "[A-Z]" private const val ARG_CHAR_CLASS = "[A-Z0-9]" internal val ARG_NAME_RE = Regex("^$ARG_INITIAL_CHAR_CLASS+([-_\\.]$ARG_CHAR_CLASS+)*$") ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/Default.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser /** * Returns a new `DelegateProvider` with the specified default value. * * @param newDefault the default value for the resulting [ArgParser.Delegate] */ fun ArgParser.DelegateProvider.default(newDefault: T): ArgParser.DelegateProvider { return ArgParser.DelegateProvider(ctor = ctor, default = { newDefault }) } /** * Returns a new `DelegateProvider` with the specified default value from a lambda. * * @param newDefault the default value for the resulting [ArgParser.Delegate] */ fun ArgParser.DelegateProvider.default(newDefault: () -> T): ArgParser.DelegateProvider { return ArgParser.DelegateProvider(ctor = ctor, default = newDefault) } /** * Returns a new `Delegate` with the specified default value. * * @param newDefault the default value for the resulting [ArgParser.Delegate] */ fun ArgParser.Delegate.default(defaultValue: T): ArgParser.Delegate = default { defaultValue } /** * Returns a new `Delegate` with the specified default value as a lambda. * * @param newDefault the default value for the resulting [ArgParser.Delegate] */ fun ArgParser.Delegate.default(defaultValue: () -> T): ArgParser.Delegate { if (hasValidators) { throw IllegalStateException("Cannot add default after adding validators") } val inner = this return object : ArgParser.Delegate() { override val hasValidators: Boolean get() = inner.hasValidators override fun toHelpFormatterValue(): HelpFormatter.Value = inner.toHelpFormatterValue().copy(isRequired = false) override fun validate() { inner.validate() } override val parser: ArgParser get() = inner.parser override val value: T get() { inner.parser.force() return if (inner.hasValue) inner.value else defaultValue() } override val hasValue: Boolean get() = true override val errorName: String get() = inner.errorName override val help: String get() = inner.help override fun addValidator(validator: ArgParser.Delegate.() -> Unit): ArgParser.Delegate = apply { inner.addValidator { validator(this@apply) } } override fun registerLeaf(root: ArgParser.Delegate<*>) { inner.registerLeaf(root) } } } ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/DefaultHelpFormatter.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser import com.xenomachina.text.NBSP_CODEPOINT import com.xenomachina.text.term.codePointWidth import com.xenomachina.text.term.columnize import com.xenomachina.text.term.wrapText /** * Default implementation of [HelpFormatter]. Output is modelled after that of common UNIX utilities and looks * something like this: * * ``` * usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT * [-v]... SOURCE... DEST * * Does something really useful. * * required arguments: * -o OUTPUT, directory in which all output should * --output OUTPUT be generated * * optional arguments: * -h, --help show this help message and exit * * -n, --dry-run don't do anything * * -I INCLUDE, search in this directory for header * --include INCLUDE files * * -v, --verbose increase verbosity * * positional arguments: * SOURCE source file * * DEST destination file * * More info is available at http://program-name.example.com/ * ``` * * @property prologue Text that should appear near the beginning of the help, immediately after the usage summary. * @property epilogue Text that should appear at the end of the help. */ class DefaultHelpFormatter( val prologue: String? = null, val epilogue: String? = null ) : HelpFormatter { val indent = " " val indentWidth = indent.codePointWidth() override fun format( programName: String?, columns: Int, values: List ): String { val effectiveColumns = when { columns < 0 -> throw IllegalArgumentException("columns must be non-negative") columns == 0 -> Int.MAX_VALUE else -> columns } val sb = StringBuilder() appendUsage(sb, effectiveColumns, programName, values) sb.append("\n") if (!prologue.isNullOrEmpty()) { sb.append("\n\n") // we just checked that prologue is non-null sb.append(prologue!!.wrapText(effectiveColumns)) sb.append("\n\n") } val required = mutableListOf() val optional = mutableListOf() val positional = mutableListOf() for (value in values) { when { value.isPositional -> positional value.isRequired -> required else -> optional }.add(value) } val usageColumns = 2 * indentWidth - 1 + if (columns == 0) { values.map { usageText(it).length }.max() ?: 0 } else { // Make left column as narrow as possible without wrapping any of the individual usages, though no wider // than half the screen. (values.map { usageText(it).split(" ").map { it.length }.max() ?: 0 }.max() ?: 0).coerceAtMost(effectiveColumns / 2) } appendSection(sb, usageColumns, effectiveColumns, "required", required) appendSection(sb, usageColumns, effectiveColumns, "optional", optional) appendSection(sb, usageColumns, effectiveColumns, "positional", positional) if (!epilogue?.trim().isNullOrEmpty()) { sb.append("\n") // we just checked that epilogue is non-null sb.append(epilogue!!.trim().wrapText(effectiveColumns)) sb.append("\n") } return sb.toString() } private fun appendSection( sb: StringBuilder, usageColumns: Int, columns: Int, name: String, values: List ) { if (!values.isEmpty()) { sb.append("\n") sb.append("$name arguments:\n") for (value in values) { val left = usageText(value).wrapText(usageColumns - indentWidth).prependIndent(indent) val right = value.help.wrapText(columns - usageColumns - 2 * indentWidth).prependIndent(indent) sb.append(columnize(left, right, minWidths = intArrayOf(usageColumns))) sb.append("\n\n") } } } private fun usageText(value: HelpFormatter.Value) = value.usages.map { it.replace(' ', '\u00a0') }.joinToString(", ") private fun appendUsage(sb: StringBuilder, columns: Int, programName: String?, values: List) { var usageStart = USAGE_PREFIX + (if (programName != null) " $programName" else "") val valueSB = StringBuilder() for (value in values) value.run { if (!usages.isEmpty()) { val usage = usages[0].replace(' ', NBSP_CODEPOINT.toChar()) if (isRequired) { valueSB.append(" $usage") } else { valueSB.append(" [$usage]") } if (isRepeating) { valueSB.append("...") } } } if (usageStart.length > columns / 2) { sb.append(usageStart) sb.append("\n") val valueIndent = (USAGE_PREFIX + " " + indent).codePointWidth() val valueColumns = columns - valueIndent sb.append(valueSB.toString().wrapText(valueColumns).prependIndent(" ".repeat(valueIndent))) } else { usageStart += " " val valueColumns = columns - usageStart.length sb.append(columnize(usageStart, valueSB.toString().wrapText(valueColumns))) } } } private const val USAGE_PREFIX = "usage:" ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/Exceptions.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser import java.io.Writer /** * Indicates that the user requested that help should be shown (with the * `--help` option, for example). */ class ShowHelpException internal constructor( private val helpFormatter: HelpFormatter, private val delegates: List> ) : SystemExitException("Help was requested", 0) { override fun printUserMessage(writer: Writer, programName: String?, columns: Int) { writer.write(helpFormatter.format(programName, columns, delegates.map { it.toHelpFormatterValue() })) } } /** * Indicates that an unrecognized option was supplied. * * @property optName the name of the option */ open class UnrecognizedOptionException(val optName: String) : SystemExitException("unrecognized option '$optName'", 2) /** * Indicates that a value is missing after parsing has completed. * * @property valueName the name of the missing value */ open class MissingValueException(val valueName: String) : SystemExitException("missing $valueName", 2) /** * Indicates that the value of a supplied argument is invalid. */ open class InvalidArgumentException(message: String) : SystemExitException(message, 2) /** * Indicates that a required option argument was not supplied. * * @property optName the name of the option * @property argName the name of the missing argument, or null */ open class OptionMissingRequiredArgumentException(val optName: String, val argName: String? = null) : SystemExitException( "option '$optName' is missing " + ( if (argName == null) "a required argument" else "the required argument $argName"), 2) /** * Indicates that a required positional argument was not supplied. * * @property argName the name of the positional argument */ open class MissingRequiredPositionalArgumentException(val argName: String) : SystemExitException("missing $argName operand", 2) /** * Indicates that an argument was forced upon an option that does not take one. * * For example, if the arguments contained "--foo=bar" and the "--foo" option does not consume any arguments. * * @property optName the name of the option */ open class UnexpectedOptionArgumentException(val optName: String) : SystemExitException("option '$optName' doesn't allow an argument", 2) /** * Indicates that there is an unhandled positional argument. * * @property valueName the name of the missing value */ open class UnexpectedPositionalArgumentException(val valueName: String?) : SystemExitException("unexpected argument${if (valueName == null) "" else " after $valueName"}", 2) ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/HelpFormatter.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser /** * Formats help for an [ArgParser]. */ interface HelpFormatter { /** * Formats a help message. * * @param programName name of the program as it should appear in usage information, or null if * program name is unknown. * @param columns width of display help should be formatted for, measured in character cells, or 0 for infinite * width. * @param values [Value] objects describing the arguments types available. */ fun format(programName: String?, columns: Int, values: List): String /** * An option or positional argument type which should be formatted for help * * @param usages possible usage strings for this argument type * @param isRequired indicates whether this is required * @param isRepeating indicates whether it makes sense to repeat this argument * @param isPositional indicates whether this is a positional argument * @param help help text provided at Delegate construction time */ data class Value( val usages: List, val isRequired: Boolean, val isRepeating: Boolean, val isPositional: Boolean, val help: String ) } ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/OptionDelegate.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser import com.xenomachina.common.Holder internal class OptionDelegate( parser: ArgParser, errorName: String, help: String, val optionNames: List, val argNames: List, val isRepeating: Boolean, val handler: ArgParser.OptionInvocation.() -> T ) : ParsingDelegate(parser, errorName, help) { init { for (optionName in optionNames) { if (!OPTION_NAME_RE.matches(optionName)) { throw IllegalArgumentException("$optionName is not a valid option name") } } for (argName in argNames) { if (!ARG_NAME_RE.matches(argName)) { throw IllegalArgumentException("$argName is not a valid argument name") } } } fun parseOption(name: String, firstArg: String?, index: Int, args: Array): Int { val arguments = mutableListOf() if (!argNames.isEmpty()) { if (firstArg != null) arguments.add(firstArg) val required = argNames.size - arguments.size if (required + index > args.size) { // Only pass an argName if more than one argument. // Naming it when there's just one seems unnecessarily verbose. val argName = if (argNames.size > 1) argNames[args.size - index] else null throw OptionMissingRequiredArgumentException(name, argName) } for (i in 0 until required) { arguments.add(args[index + i]) } } val input = ArgParser.OptionInvocation(holder, name, arguments) holder = Holder(handler(input)) return argNames.size } override fun toHelpFormatterValue(): HelpFormatter.Value { return HelpFormatter.Value( isRequired = (holder == null), isRepeating = isRepeating, usages = if (!argNames.isEmpty()) { optionNames.map { "$it ${argNames.joinToString(" ")}" } } else { optionNames }, isPositional = false, help = help) } override fun registerLeaf(root: ArgParser.Delegate<*>) { for (name in optionNames) { parser.registerOption(name, this) } } } ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/ParsingDelegate.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser import com.xenomachina.common.Holder internal abstract class ParsingDelegate( override val parser: ArgParser, override val errorName: String, override val help: String ) : ArgParser.Delegate() { protected var holder: Holder? = null override fun addValidator(validator: ArgParser.Delegate.() -> Unit): ArgParser.Delegate = apply { validators.add(validator) } override val hasValidators: Boolean get() = validators.isNotEmpty() override val value: T get() { parser.force() checkHasValue() return holder!!.value } override val hasValue: Boolean get() = holder != null override fun validate() { for (validator in validators) validator() } private val validators = mutableListOf.() -> Unit>() } ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/PositionalDelegate.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser import com.xenomachina.common.Holder internal class PositionalDelegate( parser: ArgParser, argName: String, val sizeRange: IntRange, help: String, val transform: String.() -> T ) : ParsingDelegate>(parser, argName, help) { init { require(ARG_NAME_RE.matches(argName)) { "$argName is not a valid argument name" } } override fun registerLeaf(root: ArgParser.Delegate<*>) { assert(holder == null) val hasDefault = root.hasValue if (hasDefault && sizeRange.first != 1) { throw IllegalStateException( "default value can only be applied to positional that requires a minimum of 1 arguments") } // TODO: this feels like a bit of a kludge. Consider making .default only work on positional and not // postionalList by having them return different types? parser.registerPositional(this, hasDefault) } fun parseArguments(args: List) { holder = Holder(args.map(transform)) } override fun toHelpFormatterValue(): HelpFormatter.Value { return HelpFormatter.Value( isRequired = sizeRange.first > 0, isRepeating = sizeRange.last > 1, usages = listOf(errorName), isPositional = true, help = help) } } ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/PosixNaming.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser /** * Defines rules for POSIX-style argument and option naming. */ internal object PosixNaming { fun identifierToOptionName(identifier: String): String { return when (identifier.length) { 1 -> "-" + identifier else -> "--" + identifier.camelCaseToUnderscored() } } fun String.camelCaseToUnderscored(): String { return replace('_', '-') .replace(Regex("(\\p{javaLowerCase})(\\p{javaUpperCase})")) { m -> m.groups[1]!!.value + "-" + m.groups[2]!!.value.toLowerCase() } } fun identifierToArgName(identifier: String): String { return identifier.camelCaseToUnderscored().toUpperCase() } fun selectRepresentativeOptionName(names: Array): String { if (names.size < 1) throw IllegalArgumentException("need at least one option name") // Return first long option... for (name in names) { if (name.startsWith("--")) { return name } } // ... but failing that just return first option. return names[0] } fun optionNameToArgName(name: String) = LEADING_HYPHENS_REGEX.replace(name, "").toUpperCase().replace('-', '_') } ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/SystemExitException.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser import java.io.OutputStreamWriter import java.io.Writer import kotlin.system.exitProcess /** * An exception that wants the process to terminate with a specific status code, and also (optionally) wants to display * a message to [System.out] or [System.err]. * * @property returnCode the return code that this process should exit with */ open class SystemExitException(message: String, val returnCode: Int) : Exception(message) { /** * Prints a message for the user to either `System.err` or `System.out`, and then exits with the appropriate * return code. * * @param programName the name of this program as invoked, or null if not known * @param columns the number of columns to wrap at, or 0 if not to wrap at all */ fun printAndExit(programName: String? = null, columns: Int = 0): Nothing { val writer = OutputStreamWriter(if (returnCode == 0) System.out else System.err) printUserMessage(writer, programName, columns) writer.flush() exitProcess(returnCode) } /** * Prints a message for the user to the specified `Writer`. * * @param writer where to write message for the user * @param programName the name of this program as invoked, or null if not known * @param columns the number of columns to wrap at, or 0 if not to wrap at all */ open fun printUserMessage(writer: Writer, programName: String?, columns: Int) { val leader = if (programName == null) "" else "$programName: " writer.write("$leader$message\n") } } /** * Calls [SystemExitException.printAndExit] on any `SystemExitException` that * is caught. * * @param programName the name of the program. If null, the system property com.xenomachina.argparser.programName will * be used, if set. * @param columns the number of columns to wrap any caught * `SystemExitException` to. Specify null for reasonable defaults, or 0 to not * wrap at all. * @param body the code that may throw a `SystemExitException` */ fun mainBody(programName: String? = null, columns: Int? = null, body: () -> R): R { try { return body() } catch (e: SystemExitException) { e.printAndExit( programName ?: System.getProperty(PROGRAM_NAME_PROPERTY), columns ?: System.getenv("COLUMNS")?.toInt() ?: DEFAULT_COLUMNS) } } private const val PROGRAM_NAME_PROPERTY = "com.xenomachina.argparser.programName" private const val DEFAULT_COLUMNS = 80 ================================================ FILE: src/main/kotlin/com/xenomachina/argparser/WrappingDelegate.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser internal class WrappingDelegate( private val inner: ArgParser.Delegate, private val wrap: (U) -> W ) : ArgParser.Delegate() { override val parser: ArgParser get() = inner.parser override val value: W get() = wrap(inner.value) override val hasValue: Boolean get() = inner.hasValue override val errorName: String get() = inner.errorName override val help: String get() = inner.help override fun validate() { inner.validate() } override fun toHelpFormatterValue(): HelpFormatter.Value = inner.toHelpFormatterValue() override fun addValidator(validator: ArgParser.Delegate.() -> Unit): ArgParser.Delegate = apply { inner.addValidator { validator(this@WrappingDelegate) } } override val hasValidators: Boolean get() = inner.hasValidators override fun registerLeaf(root: ArgParser.Delegate<*>) { inner.registerLeaf(root) } } ================================================ FILE: src/test/kotlin/com/xenomachina/argparser/ArgParserTest.kt ================================================ // Copyright © 2016 Laurence Gonsalves // // This file is part of kotlin-argparser, a library which can be found at // http://github.com/xenomachina/kotlin-argparser // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or (at your // option) any later version. // // This library 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 Lesser General Public License // for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library; if not, see http://www.gnu.org/licenses/ package com.xenomachina.argparser import com.xenomachina.argparser.PosixNaming.identifierToOptionName import com.xenomachina.common.orElse import io.kotlintest.matchers.beOfType import io.kotlintest.should import io.kotlintest.shouldBe import io.kotlintest.shouldThrow import io.kotlintest.specs.FunSpec import java.io.File import java.io.StringWriter class ArgParserTest : FunSpec({ test("Option name validation") { val parser = parserOf() // These are all acceptable. parser.option("-x", help = TEST_HELP) { 0 } parser.option("--x", help = TEST_HELP) { 0 } parser.option("--xy", help = TEST_HELP) { 0 } parser.option("-X", help = TEST_HELP) { 0 } parser.option("--X", help = TEST_HELP) { 0 } parser.option("--XY", help = TEST_HELP) { 0 } parser.option("--X-Y", help = TEST_HELP) { 0 } parser.option("--X_Y", help = TEST_HELP) { 0 } parser.option("-5", help = TEST_HELP) { 0 } parser.option("--5", help = TEST_HELP) { 0 } parser.option("--5Y", help = TEST_HELP) { 0 } parser.option("--X5", help = TEST_HELP) { 0 } parser.option("--x.y", help = TEST_HELP) { 0 } shouldThrow { parser.option("-_", help = TEST_HELP) { 0 } } shouldThrow { parser.option("---x", help = TEST_HELP) { 0 } } shouldThrow { parser.option("x", help = TEST_HELP) { 0 } } shouldThrow { parser.option("", help = TEST_HELP) { 0 } } shouldThrow { parser.option("-xx", help = TEST_HELP) { 0 } } shouldThrow { parser.option("--foo bar", help = TEST_HELP) { 0 } } shouldThrow { parser.option("--foo--bar", help = TEST_HELP) { 0 } } shouldThrow { parser.option("--f!oobar", help = TEST_HELP) { 0 } } shouldThrow { parser.option("--.", help = TEST_HELP) { 0 } } shouldThrow { parser.option("--.foo", help = TEST_HELP) { 0 } } shouldThrow { parser.option("--foo.", help = TEST_HELP) { 0 } } } test("Positional name validation") { val parser = parserOf() // These are all acceptable. parser.positional("X", help = TEST_HELP) { 0 } parser.positional("XYZ", help = TEST_HELP) { 0 } parser.positional("XY-Z", help = TEST_HELP) { 0 } parser.positional("XY_Z", help = TEST_HELP) { 0 } parser.positional("XY.Z", help = TEST_HELP) { 0 } shouldThrow { parser.positional("-", help = TEST_HELP) { 0 } } shouldThrow { parser.positional("_", help = TEST_HELP) { 0 } } shouldThrow { parser.positional("x", help = TEST_HELP) { 0 } } shouldThrow { parser.positional("", help = TEST_HELP) { 0 } } shouldThrow { parser.positional("-X", help = TEST_HELP) { 0 } } shouldThrow { parser.positional("X-", help = TEST_HELP) { 0 } } shouldThrow { parser.positional("X--Y", help = TEST_HELP) { 0 } } shouldThrow { parser.positional("X!", help = TEST_HELP) { 0 } } shouldThrow { parser.positional("5", help = TEST_HELP) { 0 } } shouldThrow { parser.positional(".", help = TEST_HELP) { 0 } } shouldThrow { parser.positional(".XY", help = TEST_HELP) { 0 } } shouldThrow { parser.positional("XY.", help = TEST_HELP) { 0 } } // This should be acceptable parser.option("--foobar", argNames = listOf("X-Y"), help = TEST_HELP) { 0 } // This should not shouldThrow { parser.option("--foobar", argNames = listOf("X--Y"), help = TEST_HELP) { 0 } } } test("Argless short options") { class Args(parser: ArgParser) { val xyz by parser.option>("-x", "-y", "-z", help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName") } } } Args(parserOf("-x", "-y", "-z", "-z", "-y")).xyz shouldBe listOf("-x", "-y", "-z", "-z", "-y") Args(parserOf("-xyz")).xyz shouldBe listOf("-x", "-y", "-z") } test("Short options with args") { class Args(parser: ArgParser) { val a by parser.flagging("-a", help = TEST_HELP) val b by parser.flagging("-b", help = TEST_HELP) val c by parser.flagging("-c", help = TEST_HELP) val xyz by parser.option>("-x", "-y", "-z", argNames = oneArgName, help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName:${arguments.first()}") } } } // Test with value as separate arg Args(parserOf("-x", "0", "-y", "1", "-z", "2", "-z", "3", "-y", "4")).xyz shouldBe listOf("-x:0", "-y:1", "-z:2", "-z:3", "-y:4") // Test with value concatenated Args(parserOf("-x0", "-y1", "-z2", "-z3", "-y4")).xyz shouldBe listOf("-x:0", "-y:1", "-z:2", "-z:3", "-y:4") // Test with = between option and value. Note that the "=" is treated as part of the option value for short options. Args(parserOf("-x=0", "-y=1", "-z=2", "-z=3", "-y=4")).xyz shouldBe listOf("-x:=0", "-y:=1", "-z:=2", "-z:=3", "-y:=4") // Test chained options. Note that an option with arguments must be last in the chain val chain1 = Args(parserOf("-abxc")) chain1.a shouldBe true chain1.b shouldBe true chain1.c shouldBe false chain1.xyz shouldBe listOf("-x:c") val chain2 = Args(parserOf("-axbc")) chain2.a shouldBe true chain2.b shouldBe false chain2.c shouldBe false chain2.xyz shouldBe listOf("-x:bc") } test("Mixed short options") { class Args(parser: ArgParser) { val def by parser.option>("-d", "-e", "-f", help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName") } } val abc by parser.option>("-a", "-b", "-c", help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName") } } } Args(parserOf("-adbefccbafed")).run { def shouldBe listOf("-d", "-e", "-f", "-f", "-e", "-d") abc shouldBe listOf("-a", "-b", "-c", "-c", "-b", "-a") } } test("Mixed short options with args") { class Args(parser: ArgParser) { val def by parser.option>("-d", "-e", "-f", help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName") } } val abc by parser.option>("-a", "-b", "-c", help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName") } } val xyz by parser.option>("-x", "-y", "-z", argNames = oneArgName, help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName:${arguments.first()}") } } } Args(parserOf("-adecfy5", "-x0", "-bzxy")).run { abc shouldBe listOf("-a", "-c", "-b") def shouldBe listOf("-d", "-e", "-f") xyz shouldBe listOf("-y:5", "-x:0", "-z:xy") } } test("Argless long options") { class Args(parser: ArgParser) { val xyz by parser.option>("--xray", "--yellow", "--zebra", help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName") } } } Args(parserOf("--xray", "--yellow", "--zebra", "--zebra", "--yellow")).xyz shouldBe listOf("--xray", "--yellow", "--zebra", "--zebra", "--yellow") Args(parserOf("--xray", "--yellow", "--zebra")).xyz shouldBe listOf("--xray", "--yellow", "--zebra") } test("Dotted long options") { class Args(parser: ArgParser) { val xyz by parser.option>("--x.ray", "--color.yellow", "--animal.zebra", help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName") } } } Args(parserOf("--x.ray", "--animal.zebra", "--color.yellow", "--x.ray")).xyz shouldBe listOf("--x.ray", "--animal.zebra", "--color.yellow", "--x.ray") } test("Long options with one arg") { class Args(parser: ArgParser) { val xyz by parser.option>("--xray", "--yellow", "--zaphod", argNames = oneArgName, help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName:${arguments.first()}") } } } // Test with value as separate arg Args(parserOf("--xray", "0", "--yellow", "1", "--zaphod", "2", "--zaphod", "3", "--yellow", "4")).xyz shouldBe listOf("--xray:0", "--yellow:1", "--zaphod:2", "--zaphod:3", "--yellow:4") // Test with = between option and value Args(parserOf("--xray=0", "--yellow=1", "--zaphod=2", "--zaphod=3", "--yellow=4")).xyz shouldBe listOf("--xray:0", "--yellow:1", "--zaphod:2", "--zaphod:3", "--yellow:4") shouldThrow { Args(parserOf("--xray0", "--yellow1", "--zaphod2", "--zaphod3", "--yellow4")).xyz }.run { message shouldBe "unrecognized option '--xray0'" } } test("Long options with multiple args") { class Args(parser: ArgParser) { val xyz by parser.option>("--xray", "--yak", "--zaphod", argNames = listOf("COLOR", "SIZE", "FLAVOR"), help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName:$arguments") } } } // Test with value as separate arg Args(parserOf("--xray", "red", "5", "salty")).xyz shouldBe listOf("--xray:[red, 5, salty]") Args(parserOf("--zaphod", "green", "42", "sweet", "--yak", "blue", "7", "bitter")).xyz shouldBe listOf( "--zaphod:[green, 42, sweet]", "--yak:[blue, 7, bitter]") // Note that something that looks like an option is consumed as an argument if it appears where an argument // should be. This is expected behavior. Args(parserOf("--zaphod", "green", "42", "--yak")).xyz shouldBe listOf( "--zaphod:[green, 42, --yak]") shouldThrow { Args(parserOf("--zaphod", "green", "42", "sweet", "--yak", "blue", "7")).xyz }.run { message shouldBe "option '--yak' is missing the required argument FLAVOR" } shouldThrow { Args(parserOf("--zaphod", "green")).xyz }.run { message shouldBe "option '--zaphod' is missing the required argument SIZE" } shouldThrow { Args(parserOf("--xray")).xyz }.run { message shouldBe "option '--xray' is missing the required argument COLOR" } } test("Delegate provider") { fun ArgParser.putting(vararg names: String, help: String) = option>(*names, argNames = listOf("KEY", "VALUE"), help = help) { value.orElse { mutableMapOf() }.apply { put(arguments.first(), arguments.last()) } } fun ArgParser.putting(help: String) = ArgParser.DelegateProvider { identifier -> putting(identifierToOptionName(identifier), help = help) } class Args(parser: ArgParser) { val dict by parser.putting(TEST_HELP) } // Test with value as separate arg Args(parserOf("--dict", "red", "5")).dict shouldBe mapOf("red" to "5") Args(parserOf( "--dict", "green", "42", "--dict", "blue", "7" )).dict shouldBe mapOf( "green" to "42", "blue" to "7") // Note that something that looks like an option is consumed as an argument if it appears where an argument // should be. This is expected behavior. Args(parserOf("--dict", "green", "--dict")).dict shouldBe mapOf("green" to "--dict") shouldThrow { Args(parserOf("--dict", "green", "42", "--dict", "blue")).dict }.run { message shouldBe "option '--dict' is missing the required argument VALUE" } shouldThrow { Args(parserOf("--dict")).dict }.run { message shouldBe "option '--dict' is missing the required argument KEY" } } test("Default") { class Args(parser: ArgParser) { val x by parser.storing("-x", help = TEST_HELP) { toInt() }.default(5) } // Test with no value Args(parserOf()).x shouldBe 5 // Test with value Args(parserOf("-x6")).x shouldBe 6 // Test with value as separate arg Args(parserOf("-x", "7")).x shouldBe 7 // Test with multiple values Args(parserOf("-x9", "-x8")).x shouldBe 8 } test("DDefault with providerefault") { class Args(parser: ArgParser) { val x by parser.storing(help = TEST_HELP) { toInt() }.default(5) } // Test with no value Args(parserOf()).x shouldBe 5 // Test with value Args(parserOf("-x6")).x shouldBe 6 // Test with value as separate arg Args(parserOf("-x", "7")).x shouldBe 7 // Test with multiple values Args(parserOf("-x9", "-x8")).x shouldBe 8 } test("Default with la") { class Args(parser: ArgParser) { var defaultCalled = false val x by parser.storing(help = TEST_HELP) { toInt() }.default { defaultCalled = true; 5 } } // Test default hasn't been called Args(parserOf("-x6")).defaultCalled shouldBe false // Test with no value val args = Args(parserOf()) args.x shouldBe 5 args.defaultCalled shouldBe true } test("Flag") { class Args(parser: ArgParser) { val x by parser.flagging("-x", "--ecks", help = TEST_HELP) val y by parser.flagging("-y", help = TEST_HELP) val z by parser.flagging("--zed", help = TEST_HELP) } Args(parserOf("-x", "-y", "--zed", "--zed", "-y")).run { x shouldBe true y shouldBe true z shouldBe true } Args(parserOf()).run { x shouldBe false y shouldBe false z shouldBe false } Args(parserOf("-y", "--ecks")).run { x shouldBe true y shouldBe true } Args(parserOf("--zed")).run { x shouldBe false y shouldBe false z shouldBe true } } test("Argument no parser") { class Args(parser: ArgParser) { val x by parser.storing("--ecks", "-x", help = TEST_HELP) } Args(parserOf("-x", "foo")).x shouldBe "foo" Args(parserOf("-x", "bar", "-x", "baz")).x shouldBe "baz" Args(parserOf("--ecks", "long", "-x", "short")).x shouldBe "short" Args(parserOf("-x", "short", "--ecks", "long")).x shouldBe "long" val args = Args(parserOf()) shouldThrow { args.x }.run { message shouldBe "missing ECKS" } } test("Argument missing long") { class Args(parser: ArgParser) { val x by parser.storing("--ecks", help = TEST_HELP) } val args = Args(parserOf()) shouldThrow { args.x }.run { message shouldBe "missing ECKS" } } test("Argument missing short") { class Args(parser: ArgParser) { val x by parser.storing("-x", help = TEST_HELP) } val args = Args(parserOf()) shouldThrow { args.x }.run { message shouldBe "missing X" } } test("Argument withParser") { class Args(parser: ArgParser) { val x by parser.storing("-x", "--ecks", help = TEST_HELP) { toInt() } } val opts1 = Args(parserOf("-x", "5")) opts1.x shouldBe 5 val opts2 = Args(parserOf("-x", "1", "-x", "2")) opts2.x shouldBe 2 val opts3 = Args(parserOf("--ecks", "3", "-x", "4")) opts3.x shouldBe 4 val opts4 = Args(parserOf("-x", "5", "--ecks", "6")) opts4.x shouldBe 6 val opts6 = Args(parserOf()) shouldThrow { opts6.x }.run { message shouldBe "missing ECKS" } } test("Accumulator noParser") { class Args(parser: ArgParser) { val x by parser.adding("-x", "--ecks", help = TEST_HELP) } Args(parserOf()).x shouldBe listOf() Args(parserOf("-x", "foo")).x shouldBe listOf("foo") Args(parserOf("-x", "bar", "-x", "baz")).x shouldBe listOf("bar", "baz") Args(parserOf("--ecks", "long", "-x", "short")).x shouldBe listOf("long", "short") Args(parserOf("-x", "short", "--ecks", "long")).x shouldBe listOf("short", "long") } test("Accumulator withParser") { class Args(parser: ArgParser) { val x by parser.adding("-x", "--ecks", help = TEST_HELP) { toInt() } } Args(parserOf()).x shouldBe listOf() Args(parserOf("-x", "5")).x shouldBe listOf(5) Args(parserOf("-x", "1", "-x", "2")).x shouldBe listOf(1, 2) Args(parserOf("--ecks", "3", "-x", "4")).x shouldBe listOf(3, 4) Args(parserOf("-x", "5", "--ecks", "6")).x shouldBe listOf(5, 6) } class ColorArgs(parser: ArgParser) { val color by parser.mapping( "--red" to Color.RED, "--green" to Color.GREEN, "--blue" to Color.BLUE, help = TEST_HELP) } test("Mapping") { ColorArgs(parserOf("--red")).color shouldBe Color.RED ColorArgs(parserOf("--green")).color shouldBe Color.GREEN ColorArgs(parserOf("--blue")).color shouldBe Color.BLUE // Last one takes precedence ColorArgs(parserOf("--blue", "--red")).color shouldBe Color.RED ColorArgs(parserOf("--blue", "--green")).color shouldBe Color.GREEN ColorArgs(parserOf("--red", "--blue")).color shouldBe Color.BLUE val args = ColorArgs(parserOf()) shouldThrow { args.color }.run { message shouldBe "missing --red|--green|--blue" } } class OptionalColorArgs(parser: ArgParser) { val color by parser.mapping( "--red" to Color.RED, "--green" to Color.GREEN, "--blue" to Color.BLUE, help = TEST_HELP) .default(Color.GREEN) } test("Mapping withDefault") { OptionalColorArgs(parserOf("--red")).color shouldBe Color.RED OptionalColorArgs(parserOf("--green")).color shouldBe Color.GREEN OptionalColorArgs(parserOf("--blue")).color shouldBe Color.BLUE OptionalColorArgs(parserOf()).color shouldBe Color.GREEN } test("Unrecognized short opt") { shouldThrow { OptionalColorArgs(parserOf("-x")).color }.run { message shouldBe "unrecognized option '-x'" } } test("Unrecognized long opt") { shouldThrow { OptionalColorArgs(parserOf("--ecks")).color }.run { message shouldBe "unrecognized option '--ecks'" } } test("Storing no arg") { class Args(parser: ArgParser) { val x by parser.storing("-x", "--ecks", help = TEST_HELP) } // Note that name actually used for option is used in message shouldThrow { Args(parserOf("-x")).x }.run { message shouldBe "option '-x' is missing a required argument" } // Note that name actually used for option is used in message shouldThrow { Args(parserOf("--ecks")).x }.run { message shouldBe "option '--ecks' is missing a required argument" } } test("Short storing no arg chained") { class Args(parser: ArgParser) { val y by parser.flagging("-y", help = TEST_HELP) val x by parser.storing("-x", help = TEST_HELP) } // Note that despite chaining, hyphen appears in message shouldThrow { Args(parserOf("-yx")).x }.run { message shouldBe "option '-x' is missing a required argument" } } test("Init validation") { class Args(parser: ArgParser) { val yDelegate = parser.storing("-y", help = TEST_HELP) { toInt() } val y by yDelegate val xDelegate = parser.storing("-x", help = TEST_HELP) { toInt() } val x by xDelegate init { if (y >= x) throw InvalidArgumentException("${yDelegate.errorName} must be less than ${xDelegate.errorName}") // A better way to accomplish validation that only depends on one Delegate is to use // Delegate.addValidator. See testAddValidator for an example of this. if (x.rem(2) != 0) throw InvalidArgumentException("${xDelegate.errorName} must be even, $x is odd") } } // This should pass validation val opts0 = Args(parserOf("-y1", "-x10")) opts0.y shouldBe 1 opts0.x shouldBe 10 shouldThrow { Args(parserOf("-y20", "-x10")).x }.run { message shouldBe "Y must be less than X" } shouldThrow { Args(parserOf("-y10", "-x15")).x }.run { message shouldBe "X must be even, 15 is odd" } shouldThrow { Args(parserOf("-y10", "-x15")).x }.run { message shouldBe "X must be even, 15 is odd" } } test("Add validator") { class Args(parser: ArgParser) { val yDelegate = parser.storing("-y", help = TEST_HELP) { toInt() } val y by yDelegate val xDelegate = parser.storing("-x", help = TEST_HELP) { toInt() } .addValidator { if (value.rem(2) != 0) throw InvalidArgumentException("$errorName must be even, $value is odd") } val x by xDelegate init { if (y >= x) throw InvalidArgumentException("${yDelegate.errorName} must be less than ${xDelegate.errorName}") } } // This should pass validation val opts0 = Args(parserOf("-y1", "-x10")) opts0.y shouldBe 1 opts0.x shouldBe 10 shouldThrow { Args(parserOf("-y20", "-x10")).x }.run { message shouldBe "Y must be less than X" } shouldThrow { Args(parserOf("-y10", "-x15")).x }.run { message shouldBe "X must be even, 15 is odd" } } test("Unconsumed") { class Args(parser: ArgParser) { val y by parser.flagging("-y", "--why", help = TEST_HELP) val x by parser.flagging("-x", "--ecks", help = TEST_HELP) } // No problem. Args(parserOf("-yx")).run { x shouldBe true y shouldBe true } // Attempting to give -y a parameter, "z", is treated as unrecognized option. shouldThrow { Args(parserOf("-yz")).y }.run { message shouldBe "unrecognized option '-z'" } // Unconsumed "z" again, but note that it triggers even if we don't look at y. shouldThrow { Args(parserOf("-yz")).x }.run { message shouldBe "unrecognized option '-z'" } // No problem again, this time with long opts. Args(parserOf("--why", "--ecks")).run { x shouldBe true y shouldBe true } // Attempting to give --why a parameter, "z" causes an error. shouldThrow { Args(parserOf("--why=z")).y }.run { message shouldBe "option '--why' doesn't allow an argument" } // Unconsumed "z" again, but note that it triggers even if we don't look at y. shouldThrow { Args(parserOf("--why=z")).x }.run { message shouldBe "option '--why' doesn't allow an argument" } } test("Positional basic") { class Args(parser: ArgParser) { val flag by parser.flagging("-f", "--flag", help = TEST_HELP) val store by parser.storing("-s", "--store", help = TEST_HELP).default("DEFAULT") val sources by parser.positionalList("SOURCE", help = TEST_HELP) val destination by parser.positional("DEST", help = TEST_HELP) } Args(parserOf("foo", "bar", "baz", "quux")).run { flag shouldBe false store shouldBe "DEFAULT" sources shouldBe listOf("foo", "bar", "baz") destination shouldBe "quux" } Args(parserOf("-f", "foo", "bar", "baz", "quux")).run { flag shouldBe true store shouldBe "DEFAULT" sources shouldBe listOf("foo", "bar", "baz") destination shouldBe "quux" } Args(parserOf("-s", "foo", "bar", "baz", "quux")).run { flag shouldBe false store shouldBe "foo" sources shouldBe listOf("bar", "baz") destination shouldBe "quux" } Args(parserOf("-s", "foo", "bar", "-f", "baz", "quux")).run { flag shouldBe true store shouldBe "foo" sources shouldBe listOf("bar", "baz") destination shouldBe "quux" } // "--" disables option processing for all further arguments. // Note that "-f" is now considered a positional argument. Args(parserOf("-s", "foo", "--", "bar", "-f", "baz", "quux")).run { flag shouldBe false store shouldBe "foo" sources shouldBe listOf("bar", "-f", "baz") destination shouldBe "quux" } // "--" disables option processing for all further arguments. // Note that the second "--" is also considered a positional argument. Args(parserOf("-s", "foo", "--", "bar", "--", "-f", "baz", "quux")).run { flag shouldBe false store shouldBe "foo" sources shouldBe listOf("bar", "--", "-f", "baz") destination shouldBe "quux" } Args(parserOf("-s", "foo", "bar", "-f", "baz", "quux", mode = ArgParser.Mode.POSIX)).run { flag shouldBe false store shouldBe "foo" sources shouldBe listOf("bar", "-f", "baz") destination shouldBe "quux" } } test("Positional with parser") { class Args(parser: ArgParser) { val flag by parser.flagging("-f", "--flag", help = TEST_HELP) val store by parser.storing("-s", "--store", help = TEST_HELP).default("DEFAULT") val start by parser.positionalList("START", TEST_HELP, 3..4) { toInt() } val end by parser.positionalList("END", TEST_HELP, 3..5) { toInt() } } shouldThrow { Args(parserOf("1", "2")).flag }.run { message shouldBe "missing START operand" } shouldThrow { Args(parserOf("1", "2", "3", "4", "5")).flag }.run { message shouldBe "missing END operand" } Args(parserOf("1", "2", "3", "4", "5", "6")).run { flag shouldBe false store shouldBe "DEFAULT" // end needs at least 3 args, so start only consumes 3 start shouldBe listOf(1, 2, 3) end shouldBe listOf(4, 5, 6) } Args(parserOf("1", "2", "3", "4", "5", "6", "7")).run { flag shouldBe false store shouldBe "DEFAULT" // end only needs at 3 args, so start can consume 4 start shouldBe listOf(1, 2, 3, 4) end shouldBe listOf(5, 6, 7) } Args(parserOf("1", "2", "3", "4", "5", "6", "7", "8")).run { flag shouldBe false store shouldBe "DEFAULT" // start can't consume more than 4, so end gets the rest. start shouldBe listOf(1, 2, 3, 4) end shouldBe listOf(5, 6, 7, 8) } Args(parserOf("1", "2", "3", "4", "5", "6", "7", "8", "9")).run { flag shouldBe false store shouldBe "DEFAULT" // once again, start can't consume more than 4, so end gets the rest. start shouldBe listOf(1, 2, 3, 4) end shouldBe listOf(5, 6, 7, 8, 9) } shouldThrow { Args(parserOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "10")).flag }.run { message shouldBe "unexpected argument after END" } } test("Counting") { class Args(parser: ArgParser) { val verbosity by parser.counting("-v", "--verbose", help = TEST_HELP) } Args(parserOf()).run { verbosity shouldBe 0 } Args(parserOf("-v")).run { verbosity shouldBe 1 } Args(parserOf("-v", "-v")).run { verbosity shouldBe 2 } } test("Help") { class Args(parser: ArgParser) { val dryRun by parser.flagging("-n", "--dry-run", help = "don't do anything") val includes by parser.adding("-I", "--include", help = "search in this directory for header files") val outDir by parser.storing("-o", "--output", help = "directory in which all output should be generated") val verbosity by parser.counting("-v", "--verbose", help = "increase verbosity") val sources by parser.positionalList("SOURCE", help = "source file") val destination by parser.positional("DEST", help = "destination file") } shouldThrow { Args(parserOf("--help", helpFormatter = DefaultHelpFormatter( prologue = """ This is the prologue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam malesuada maximus eros. Fusce luctus risus eget quam consectetur, eu auctor est ullamcorper. Maecenas eget suscipit dui, sed sodales erat. Phasellus. This is the second paragraph of the prologue. I don't have anything else to say, but I'd like there to be enough text that it wraps to the next line. """, epilogue = """ This is the epilogue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel tortor nunc. Sed eu massa sed turpis auctor faucibus. Donec vel pellentesque tortor. Ut ultrices tempus lectus fermentum vestibulum. Phasellus. """))).dryRun }.run { val help = StringWriter().apply { printUserMessage(this, "program_name", 60) }.toString() help shouldBe """ usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT [-v]... SOURCE... DEST This is the prologue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam malesuada maximus eros. Fusce luctus risus eget quam consectetur, eu auctor est ullamcorper. Maecenas eget suscipit dui, sed sodales erat. Phasellus. This is the second paragraph of the prologue. I don't have anything else to say, but I'd like there to be enough text that it wraps to the next line. required arguments: -o OUTPUT, directory in which all output should --output OUTPUT be generated optional arguments: -h, --help show this help message and exit -n, --dry-run don't do anything -I INCLUDE, search in this directory for header --include INCLUDE files -v, --verbose increase verbosity positional arguments: SOURCE source file DEST destination file This is the epilogue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel tortor nunc. Sed eu massa sed turpis auctor faucibus. Donec vel pellentesque tortor. Ut ultrices tempus lectus fermentum vestibulum. Phasellus. """.trimStart() val help2 = StringWriter().apply { printUserMessage(this, "a_really_long_program_name", 60) }.toString() help2 shouldBe """ usage: a_really_long_program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT [-v]... SOURCE... DEST This is the prologue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam malesuada maximus eros. Fusce luctus risus eget quam consectetur, eu auctor est ullamcorper. Maecenas eget suscipit dui, sed sodales erat. Phasellus. This is the second paragraph of the prologue. I don't have anything else to say, but I'd like there to be enough text that it wraps to the next line. required arguments: -o OUTPUT, directory in which all output should --output OUTPUT be generated optional arguments: -h, --help show this help message and exit -n, --dry-run don't do anything -I INCLUDE, search in this directory for header --include INCLUDE files -v, --verbose increase verbosity positional arguments: SOURCE source file DEST destination file This is the epilogue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel tortor nunc. Sed eu massa sed turpis auctor faucibus. Donec vel pellentesque tortor. Ut ultrices tempus lectus fermentum vestibulum. Phasellus. """.trimStart() // Regression test for issue #17 val help_wide = StringWriter().apply { printUserMessage(this, "program_name", 0) }.toString() help_wide shouldBe """ usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT [-v]... SOURCE... DEST This is the prologue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam malesuada maximus eros. Fusce luctus risus eget quam consectetur, eu auctor est ullamcorper. Maecenas eget suscipit dui, sed sodales erat. Phasellus. This is the second paragraph of the prologue. I don't have anything else to say, but I'd like there to be enough text that it wraps to the next line. required arguments: -o OUTPUT, --output OUTPUT directory in which all output should be generated optional arguments: -h, --help show this help message and exit -n, --dry-run don't do anything -I INCLUDE, --include INCLUDE search in this directory for header files -v, --verbose increase verbosity positional arguments: SOURCE source file DEST destination file This is the epilogue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel tortor nunc. Sed eu massa sed turpis auctor faucibus. Donec vel pellentesque tortor. Ut ultrices tempus lectus fermentum vestibulum. Phasellus. """.trimStart() } } test("Implicit long flag name") { class Args(parser: ArgParser) { val flag1 by parser.flagging(help = TEST_HELP) val flag2 by parser.flagging(help = TEST_HELP) val count by parser.counting(help = TEST_HELP) val store by parser.storing(help = TEST_HELP) val store_int by parser.storing(help = TEST_HELP) { toInt() } val adder by parser.adding(help = TEST_HELP) val int_adder by parser.adding(help = TEST_HELP) { toInt() } val int_set_adder by parser.adding(initialValue = mutableSetOf(), help = TEST_HELP) { toInt() } val positional by parser.positional(help = TEST_HELP) val positional_int by parser.positional(help = TEST_HELP) { toInt() } val positionalList by parser.positionalList(sizeRange = 2..2, help = TEST_HELP) val positionalList_int by parser.positionalList(sizeRange = 2..2, help = TEST_HELP) { toInt() } } Args(parserOf( "--flag1", "--count", "--count", "--store=hello", "--store-int=42", "--adder=foo", "--adder=bar", "--int-adder=2", "--int-adder=4", "--int-adder=6", "--int-set-adder=64", "--int-set-adder=128", "--int-set-adder=20", "1", "1", "2", "3", "5", "8" )).run { flag1 shouldBe true flag2 shouldBe false count shouldBe 2 store shouldBe "hello" store_int shouldBe 42 adder shouldBe listOf("foo", "bar") int_adder shouldBe listOf(2, 4, 6) int_set_adder shouldBe setOf(20, 64, 128) positional shouldBe "1" positional_int shouldBe 1 positionalList shouldBe listOf("2", "3") positionalList_int shouldBe listOf(5, 8) } shouldThrow { Args(parserOf( "13", "21", "34", "55", "89" )).run { flag1 shouldBe false } }.run { message shouldBe "missing POSITIONAL-LIST-INT operand" } } fun nullableString(): String? = null test("Nullable optional") { class Args(parser: ArgParser) { val path by parser.storing("The path", transform = ::File) .default(nullableString()?.let(::File)) } } test("Nullable optional without transform") { class Args(parser: ArgParser) { val str by parser.storing(TEST_HELP) .default(nullableString()) } Args(parserOf("--str=foo")).run { str shouldBe "foo" } Args(parserOf()).run { str shouldBe null } } test("Default generalization") { class Args(parser: ArgParser) { val shape by parser.storing("The path", transform = ::Rectangle) .default(Circle()) val rect by parser.storing("The path", transform = ::Rectangle) } val args = Args(parserOf("--rect=foo")) staticType(args.shape) shouldBe Shape::class args.shape should beOfType() staticType(args.rect) shouldBe Rectangle::class val args2 = Args(parserOf()) shouldThrow { args2.rect }.run { message shouldBe "missing RECT" } } test("Default generalization without transform") { class Args(parser: ArgParser) { val str by parser.storing(TEST_HELP) .default(5) } Args(parserOf("--str=foo")).run { str shouldBe "foo" } Args(parserOf()).run { str shouldBe 5 } } test("Auto named flagging") { class Args(parser: ArgParser) { val autoFlag by parser.flagging(TEST_HELP) } Args(parserOf()).autoFlag shouldBe false Args(parserOf("--auto-flag")).autoFlag shouldBe true } test("Auto named counting") { class Args(parser: ArgParser) { val autoCount by parser.counting(TEST_HELP) } Args(parserOf()).autoCount shouldBe 0 Args(parserOf("--auto-count")).autoCount shouldBe 1 Args(parserOf("--auto-count", "--auto-count")).autoCount shouldBe 2 } test("Auto named storing") { class Args(parser: ArgParser) { val autoStore by parser.storing(TEST_HELP) } shouldThrow { Args(parserOf()).autoStore }.run { message shouldBe "missing AUTO_STORE" } Args(parserOf("--auto-store=foo")).autoStore shouldBe "foo" Args(parserOf("--auto-store", "bar", "--auto-store", "baz")).autoStore shouldBe "baz" } test("Auto named storing with transform") { class Args(parser: ArgParser) { val autoStore by parser.storing(TEST_HELP) { toInt() } } shouldThrow { Args(parserOf()).autoStore }.run { message shouldBe "missing AUTO_STORE" } Args(parserOf("--auto-store=5")).autoStore shouldBe 5 Args(parserOf("--auto-store", "11", "--auto-store", "42")).autoStore shouldBe 42 } test("Auto named adding") { class Args(parser: ArgParser) { val autoAccumulator by parser.adding(TEST_HELP) } Args(parserOf()).autoAccumulator shouldBe emptyList() Args(parserOf("--auto-accumulator=foo")).autoAccumulator shouldBe listOf("foo") Args(parserOf("--auto-accumulator", "bar", "--auto-accumulator", "baz")).autoAccumulator shouldBe listOf("bar", "baz") } test("Auto named adding with transform") { class Args(parser: ArgParser) { val autoAccumulator by parser.adding(TEST_HELP) { toInt() } } Args(parserOf()).autoAccumulator shouldBe emptyList() Args(parserOf("--auto-accumulator=5")).autoAccumulator shouldBe listOf(5) Args(parserOf("--auto-accumulator", "11", "--auto-accumulator", "42")).autoAccumulator shouldBe listOf(11, 42) } test("Auto named adding with transform and initial") { class Args(parser: ArgParser) { val autoAccumulator by parser.adding(TEST_HELP, initialValue = mutableSetOf()) { toInt() } } Args(parserOf()).autoAccumulator shouldBe emptySet() Args(parserOf("--auto-accumulator=5")).autoAccumulator shouldBe setOf(5) Args(parserOf("--auto-accumulator", "11", "--auto-accumulator", "42")).autoAccumulator shouldBe setOf(42, 11) } test("Auto named positional") { class Args(parser: ArgParser) { val autoPositional by parser.positional(TEST_HELP) } shouldThrow { Args(parserOf()).autoPositional }.run { message shouldBe "missing AUTO-POSITIONAL operand" } Args(parserOf("foo")).autoPositional shouldBe "foo" } test("Auto named positional with transform") { class Args(parser: ArgParser) { val autoPositional by parser.positional(TEST_HELP) { toInt() } } shouldThrow { Args(parserOf()).autoPositional }.run { message shouldBe "missing AUTO-POSITIONAL operand" } Args(parserOf("47")).autoPositional shouldBe 47 } test("Auto named positional list") { class Args(parser: ArgParser) { val autoPositional by parser.positionalList(TEST_HELP) } shouldThrow { Args(parserOf()).autoPositional }.run { message shouldBe "missing AUTO-POSITIONAL operand" } Args(parserOf("foo")).autoPositional shouldBe listOf("foo") } test("Auto named positional list with transform") { class Args(parser: ArgParser) { val autoPositional by parser.positionalList(TEST_HELP) { toInt() } } shouldThrow { Args(parserOf()).autoPositional }.run { message shouldBe "missing AUTO-POSITIONAL operand" } Args(parserOf("47")).autoPositional shouldBe listOf(47) Args(parserOf("27", "38")).autoPositional shouldBe listOf(27, 38) } test("Positional default") { class Args(parser: ArgParser) { val name by parser.positional("NAME", TEST_HELP).default("John") } Args(parserOf()).name shouldBe "John" Args(parserOf("Alfred")).name shouldBe "Alfred" } test("Positional list default") { class Args(parser: ArgParser) { val name by parser.positionalList("NAME", TEST_HELP).default(listOf("Jack", "Jill")) } Args(parserOf()).name shouldBe listOf("Jack", "Jill") Args(parserOf("Jack")).name shouldBe listOf("Jack") Args(parserOf("John", "Jim", "Jack", "Jason")).name shouldBe listOf("John", "Jim", "Jack", "Jason") } test("Auto named long option with multiple args") { class Args(parser: ArgParser) { val xyz by parser.option>( "--xyz", argNames = listOf("COLOR", "SIZE", "FLAVOR"), help = TEST_HELP) { value.orElse { mutableListOf() }.apply { add("$optionName:$arguments") } } } // Test with value as separate arg Args(parserOf("--xyz", "red", "5", "salty")).xyz shouldBe listOf("--xyz:[red, 5, salty]") Args(parserOf("--xyz", "green", "42", "sweet", "--xyz", "blue", "7", "bitter")).xyz shouldBe listOf( "--xyz:[green, 42, sweet]", "--xyz:[blue, 7, bitter]") // Note that something that looks like an option is consumed as an argument if it appears where an argument // should be. This is expected behavior. Args(parserOf("--xyz", "green", "42", "--xyz")).xyz shouldBe listOf( "--xyz:[green, 42, --xyz]") shouldThrow { Args(parserOf("--xyz", "green", "42", "sweet", "--xyz", "blue", "7")).xyz }.run { message shouldBe "option '--xyz' is missing the required argument FLAVOR" } shouldThrow { Args(parserOf("--xyz", "green")).xyz }.run { message shouldBe "option '--xyz' is missing the required argument SIZE" } shouldThrow { Args(parserOf("--xyz")).xyz }.run { message shouldBe "option '--xyz' is missing the required argument COLOR" } } test("Positional add validator") { class Args(parser: ArgParser) { val yDelegate = parser.positional("Y", TEST_HELP) { toInt() } val y by yDelegate val xDelegate = parser.positional("X", TEST_HELP) { toInt() } .addValidator { if (value.rem(2) != 0) throw InvalidArgumentException("$errorName must be even, $value is odd") } val x by xDelegate init { if (y >= x) throw InvalidArgumentException("${yDelegate.errorName} must be less than ${xDelegate.errorName}") } } // This should pass validation val opts0 = Args(parserOf("1", "10")) opts0.y shouldBe 1 opts0.x shouldBe 10 shouldThrow { Args(parserOf("20", "10")).x }.run { message shouldBe "Y must be less than X" } shouldThrow { Args(parserOf("10", "15")).x }.run { message shouldBe "X must be even, 15 is odd" } } test("Positional list add validator") { class Args(parser: ArgParser) { val yDelegate = parser.positionalList("Y", TEST_HELP, 2..2) { toInt() } val y by yDelegate val xDelegate = parser.positionalList("X", TEST_HELP, 2..2) { toInt() } .addValidator { for (i in value) { if (i.rem(2) != 0) throw InvalidArgumentException("$errorName elements must be even, $i is odd") } } val x by xDelegate } // This should pass validation val opts0 = Args(parserOf("1", "10", "4", "8")) opts0.y shouldBe listOf(1, 10) opts0.x shouldBe listOf(4, 8) shouldThrow { Args(parserOf("10", "15", "42", "37")).x }.run { message shouldBe "X elements must be even, 37 is odd" } } test("Parse into") { class Args(parser: ArgParser) { val str by parser.storing(TEST_HELP) } parserOf("--str=foo").parseInto(::Args).run { str shouldBe "foo" } } test("Parse into unrecognized option failure") { class Args(parser: ArgParser) { val str by parser.storing(TEST_HELP) } shouldThrow { parserOf("--str=foo", "--eggs=bacon").parseInto(::Args) } } test("Parse into missing value failure") { class Args(parser: ArgParser) { val str by parser.storing(TEST_HELP) val eggs by parser.storing(TEST_HELP) } shouldThrow { parserOf("--str=foo").parseInto(::Args) } } test("Parse into illegal state test") { class Args(parser: ArgParser) { val str by parser.storing(TEST_HELP) } shouldThrow { val parser = parserOf("--str=foo") @Suppress("unused_variable") val oops by parser.storing("--oops", help = TEST_HELP).default("oops") parser.parseInto(::Args) } } test("Issue15") { class Args(parser: ArgParser) { val manual by parser.storing("--named-by-hand", help = TEST_HELP, argName = "HANDYS-ARG") val auto by parser.storing(TEST_HELP, argName = "OTTOS-ARG") val foo by parser.adding(help = TEST_HELP, argName = "BAR") { toInt() } val bar by parser.adding("--baz", help = TEST_HELP, argName = "QUUX") } shouldThrow { Args(parserOf("--help")).manual }.run { // TODO: find a way to make this less brittle (ie: don't use help text) StringWriter().apply { printUserMessage(this, null, 10000) }.toString().trim() shouldBe """ usage: [-h] --named-by-hand HANDYS-ARG --auto OTTOS-ARG [--foo BAR]... [--baz QUUX]... required arguments: --named-by-hand HANDYS-ARG test help message --auto OTTOS-ARG test help message optional arguments: -h, --help show this help message and exit --foo BAR test help message --baz QUUX test help message""".trim() } } test("Issue18 addValidator then default") { class Args(parser: ArgParser) { val x by parser.storing( "-x", help = TEST_HELP, transform = String::toInt ).addValidator { value shouldBe 0 }.default(0) } shouldThrow { Args(parserOf()) }.message shouldBe "Cannot add default after adding validators" } test("Issue18 default then addValidator") { class Args(parser: ArgParser) { val x by parser.storing( "-x", help = "", transform = String::toInt ).default(0).addValidator { value shouldBe 0 } } val x = Args(parserOf()).x x shouldBe 0 } test("Issue47") { class Args(parser: ArgParser) { val caseInsensitive by parser.flagging("-c", "--case_insensitive", help = TEST_HELP) val includedExtensions by parser.adding("-e", "--include_ext", help = TEST_HELP) { extensionCheckCaseInsensitive() } private fun String.extensionCheckCaseInsensitive() = if (caseInsensitive) this.toLowerCase() else this } val includeExtensions = Args(parserOf("-e", "Foo", "-c", "-e", "Bar")).includedExtensions includeExtensions shouldBe listOf("Foo", "bar") } class DependentArgs(parser: ArgParser) { val suffix by parser.storing(TEST_HELP) val x by parser.adding(TEST_HELP) { "$this:$suffix" } } test("Dependent args test order mat") { val result = DependentArgs(parserOf("--suffix", "bar", "-x", "foo", "-x", "dry", "--suffix", "fish", "-x", "cat")).x result shouldBe listOf("foo:bar", "dry:bar", "cat:fish") } test("Dependent args test unset throws missing value excep") { shouldThrow { DependentArgs(parserOf("-x", "foo", "-x", "dry", "--suffix", "fish", "-x", "cat")).x }.run { message shouldBe "missing SUFFIX" } } class DependentArgsWithDefault(parser: ArgParser) { val suffix by parser.storing(TEST_HELP).default("") val x by parser.adding(TEST_HELP) { "$this:$suffix" } } test("Dependent args test with default unset") { DependentArgsWithDefault(parserOf("-x", "foo", "-x", "dry", "--suffix", "fish", "-x", "cat")).x shouldBe listOf("foo:", "dry:", "cat:fish") } class DependentArgsWithDependentDefault(parser: ArgParser) { val a by parser.storing(TEST_HELP).default("") val b by parser.storing(TEST_HELP).default { "=$a" } } test("Dependent args test with dependent def") { DependentArgsWithDependentDefault(parserOf()).run { a shouldBe "" b shouldBe "=" } DependentArgsWithDependentDefault(parserOf("-aFoo")).run { a shouldBe "Foo" b shouldBe "=Foo" } DependentArgsWithDependentDefault(parserOf("-bBar")).run { a shouldBe "" b shouldBe "Bar" } DependentArgsWithDependentDefault(parserOf("-aFoo", "-bBar")).run { a shouldBe "Foo" b shouldBe "Bar" } DependentArgsWithDependentDefault(parserOf("-bBar", "-aFoo")).run { a shouldBe "Foo" b shouldBe "Bar" } } }) /** Used in tests where we need a simple enum */ enum class Color { RED, GREEN, BLUE } /** Used in tests where we need a simple class hierarchy */ open class Shape class Rectangle(val s: String) : Shape() class Circle : Shape() /** Creates an ArgParser for the specified (var)args. */ fun parserOf( vararg args: String, mode: ArgParser.Mode = ArgParser.Mode.GNU, helpFormatter: HelpFormatter? = DefaultHelpFormatter() ) = ArgParser(args, mode, helpFormatter) /** * Helper function for getting the static (not runtime) type of an expression. This is useful for verifying that the * inferred type of an expression is what you think it should be. For example: * * staticType(actuallyACircle) shouldBe Shape::class */ inline fun staticType(@Suppress("UNUSED_PARAMETER") x: T) = T::class val oneArgName = listOf("ARG_NAME") val TEST_HELP = "test help message"