├── .travis.yml ├── LICENSE.TXT ├── README.md ├── RELEASE.md ├── pom.xml └── src ├── main └── java │ ├── com │ └── github │ │ └── jankroken │ │ └── commandline │ │ ├── CommandLineParser.java │ │ ├── OptionStyle.java │ │ ├── ParseResult.java │ │ ├── annotations │ │ ├── AllAvailableArguments.java │ │ ├── ArgumentsUntilDelimiter.java │ │ ├── Description.java │ │ ├── LongSwitch.java │ │ ├── LooseArguments.java │ │ ├── Multiple.java │ │ ├── Option.java │ │ ├── Required.java │ │ ├── ShortSwitch.java │ │ ├── SingleArgument.java │ │ ├── SubConfiguration.java │ │ └── Toggle.java │ │ ├── domain │ │ ├── ArgumentToken.java │ │ ├── CommandLineException.java │ │ ├── CommandLineWrappedException.java │ │ ├── InternalErrorException.java │ │ ├── InvalidCommandLineException.java │ │ ├── InvalidOptionConfigurationException.java │ │ ├── UnrecognizedSwitchException.java │ │ └── internal │ │ │ ├── ArgumentConsumption.java │ │ │ ├── ArgumentConsumptionBuilder.java │ │ │ ├── ArgumentConsumptionType.java │ │ │ ├── LongOrCompactTokenizer.java │ │ │ ├── Occurrences.java │ │ │ ├── OptionSet.java │ │ │ ├── OptionSetLevel.java │ │ │ ├── OptionSpecification.java │ │ │ ├── OptionSpecificationBuilder.java │ │ │ ├── OptionSpecificationFactory.java │ │ │ ├── SimpleTokenizer.java │ │ │ ├── Switch.java │ │ │ ├── SwitchToken.java │ │ │ ├── Token.java │ │ │ ├── TokenQueue.java │ │ │ └── Tokenizer.java │ │ └── util │ │ ├── ArrayIterator.java │ │ ├── Constants.java │ │ ├── Methods.java │ │ └── PeekIterator.java │ └── module-info.java └── test └── java └── com └── github └── jankroken └── commandline ├── domain ├── LongOrCompactTokenizerTest.java └── OptionSpecificationFactoryTest.java ├── error ├── InvalidArgumentsTests.java ├── InvalidConfigurationTests.java ├── InvalidTypeConfiguration.java ├── MissingArgumentConsumptionConfiguration.java ├── MissingSwitchesConfiguration.java ├── MultipleConsumptionsConfiguration.java └── RequiredConfiguration.java ├── happy ├── AlbumConfiguration.java ├── BasicParserTest.java ├── DelimiterConfiguration.java ├── LongOrCompactParserTest.java ├── LooseArgsConfiguration.java ├── LooseArgumentsConfiguration.java ├── MultipleArgsConfiguration.java ├── MultipleConfiguration.java ├── MultipleSubconfigsConfiguration.java ├── SimpleConfiguration.java ├── SimpleParserTest.java └── SimpleSuperConfiguration.java └── util ├── ArrayIteratorTest.java ├── MethodsTest.java └── PeekIteratorTest.java /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Commandline: Users Guide 2 | ======================== 3 | 4 | See [a href="RELEASE.md"]RELEASE.md[/a] for release notes 5 | 6 | What is CommandLine 7 | ------------------- 8 | 9 | CommandLine is a Java library to parse command line arguments. Most argument parsing 10 | libraries are modelled after the GNU getopt library dominant in the unix C environment 11 | (Apache commons-cli is a great library based on this model). In contrast, this library 12 | is based on a mapping from the command line arguments onto objects, by using annotations. 13 | 14 | The reason I wrote this library is that I think using an argument to object mapping in 15 | most cases is easier and more obvious than parsing arguments the getopt way. Also, by 16 | using sub configurations, this approach scales a lot better. Note however, that this 17 | approach does not work for all cases. Specifically, this library 18 | (and object mapping of arguments in general) does not maintain order of arguments. One 19 | example of that is the unix 'find' command, where the command line arguments makes out 20 | a domain specific language. An example of a command is 21 | 22 | find . -name "*.txt" -o '(' -name "*.html" -a -amin 4 ')' -exec ls -l '{}' ';' 23 | 24 | The functionality that is currently supported is: 25 | 26 | * Options with none, one or multiple arguments 27 | * Optional and mandatory options 28 | * Multiple instances of options, without mixing the arguments specified for each occurrence 29 | * Option arguments with a delimiter (similar to the '-exec' option of 'find') 30 | * Option groups (and multiple occurrences of option groups) 31 | * Support for arguments not connected to an option 32 | * A short and long representation of each switch/option 33 | * Unrecognized arguments will be caught and result in an exception 34 | * Escaping the rest of the arguments with the GNU style "--" switch 35 | * Single letter switches that can be concatenated in one switch/option (as in 'ls -lSr') 36 | 37 | Functionality that is currently not supported is 38 | 39 | * Generating a help text 40 | * Built in validation, and automatic validation based on argument types of the annotated methods 41 | * Argument separators other than whitespace (a.e. --color=true) 42 | * A single argument not separated from the switch (like the GCC -O3) 43 | 44 | Versioning and required Java version 45 | ------------------------------------ 46 | 47 | The version number of this module indicates backwards compatibility and required java version. 48 | It is in the form major.minor.patch. The major version is increased when backwards compatibility 49 | is broken. The minor number indicates the minimum required Java version, and the patch number 50 | is increased when a new fully backwards compatible version is released. 51 | 52 | At the time of writing, the current version of command line is 1.7.0, which indicates that 53 | Java 7 is needed to use the library. When the library is rewritten for java 8, the version 54 | will likely be 2.8.0, where 2 indicates that the interface to the library has been modified, 55 | 8 indicates that Java SE version 8 is required, and 0 indicates that it's the first 2.8 release. 56 | 57 | Installation 58 | ------------ 59 | 60 | To use the library in a maven project, add the following maven dependency to your pom.xml: 61 | 62 | 63 | com.github.jankroken 64 | commandline 65 | 1.7.0 66 | 67 | 68 | If you just want to download the jar file and add it to the project by hand, you can download the jar 69 | file from the maven central repository, a.e. 70 | http://search.maven.org/remotecontent?filepath=com/github/jankroken/commandline/1.7.0/commandline-1.7.0.jar 71 | 72 | Using the library 73 | ----------------- 74 | 75 | To use the library, you must create at least one class with a no argument constructor. Arguments 76 | are added by annotating setter methods. The following example shows a configuration class with 77 | two arguments, '-file' (with one argument) and an optional '-debug'. These two arguments have the 78 | short versions '-f' and '-d'. 79 | 80 | There are two option styles available for the parser. 81 | 82 | * OptionStyle.SIMPLE - All switches have to be standalone. All (both long and short) needs to be prepended with a 83 | single dash on the command line. Example: 'findalbum -v -title "Undeveloped" -artist "OhGr"' 84 | * OptionStyle.LONG_OR_COMPACT - Long switches are prepended by two dashes. Short switches are prepended with a 85 | single dash, and may be concatenated into one switch. Example: 'ls -lSr --color true' 86 | 87 | Note that it is recommended to create a dedicated class hierarchy to represent the command line, to maintain the 88 | a strong separation between the domain model and infrastructure/surroundings. 89 | 90 | ### A simple example ### 91 | 92 | This example uses the SIMPLE OptionStyle 93 | 94 | public class Arguments () { 95 | private String filename; 96 | private boolean debug = false; 97 | 98 | @Option 99 | @LongSwitch("file") 100 | @ShortSwitch("f") 101 | @SingleArgument 102 | @Required 103 | public void setFilename(String filename) { 104 | this.filename = filename; 105 | } 106 | 107 | @Option 108 | @LongSwitch("debug") 109 | @ShortSwitch("d") 110 | @Toggle(true) 111 | public void setDebug(boolean debug) { 112 | this.debug = debug; 113 | } 114 | 115 | public String getFilename() { 116 | return filename; 117 | } 118 | 119 | public boolean getDebug() { 120 | return debug; 121 | } 122 | } 123 | 124 | The following code actually scans the argument list, and creates an instance of the class 125 | 126 | public class Main { 127 | public final static final void main(String[] args) { 128 | try { 129 | Arguments arguments = CommandLineParser.parse(Arguments.class, args, OptionStyle.SIMPLE); 130 | doSomething(arguments); 131 | } catch (InvalidCommandLineException clException) { 132 | ... 133 | } catch (InvalidOptionConfigurationException configException) { 134 | ... 135 | } catch (UnrecognizedSwitchException unrecognizedSwitchException) { 136 | ... 137 | } 138 | } 139 | } 140 | 141 | This will successfully parse the following command line 142 | 143 | java Main -file hello.txt -debug 144 | 145 | ### A more advanced example ### 146 | 147 | Occasionally, you might need to parse multiple occurrences of a sequence of options. 148 | The following example demonstrates both what the hell I'm talking about and how to 149 | do exactly that: 150 | 151 | public class AlbumConfiguration { 152 | 153 | private String artist; 154 | private String name; 155 | private String year; 156 | 157 | @Option 158 | @LongSwitch("artist") 159 | @SingleArgument 160 | public void setArtist(String artist) { 161 | this.artist = artist; 162 | } 163 | 164 | @Option 165 | @LongSwitch("name") 166 | @SingleArgument 167 | public void setName(String name) { 168 | this.name = name; 169 | } 170 | 171 | @Option 172 | @LongSwitch("year") 173 | @SingleArgument 174 | public void setYear(String year) { 175 | this.year = year; 176 | } 177 | 178 | public String getArtist() { 179 | return artist; 180 | } 181 | 182 | public String getName() { 183 | return name; 184 | } 185 | 186 | public String getYear() { 187 | return year; 188 | } 189 | } 190 | 191 | 192 | public class MusicDatabaseConfiguration { 193 | 194 | private String database; 195 | private boolean verbose; 196 | private List albums; 197 | 198 | 199 | @Option 200 | @LongSwitch("database") 201 | @ShortSwitch("d") 202 | @SingleArgument 203 | public void setFilename(String database) { 204 | this.database = database; 205 | } 206 | 207 | @Option 208 | @LongSwitch("verbose") 209 | @ShortSwitch("v") 210 | @Toggle(true) 211 | public void setVerbose(boolean verbose) { 212 | this.verbose = verbose; 213 | } 214 | 215 | @Option 216 | @LongSwitch("album") 217 | @ShortSwitch("a") 218 | @Multiple 219 | @SubConfiguration(AlbumConfiguration.class) 220 | public void setAlbums(List albums) { 221 | this.albums = albums; 222 | } 223 | 224 | public boolean getVerbose() { 225 | return verbose; 226 | } 227 | 228 | public String getDatabase() { 229 | return database; 230 | } 231 | 232 | public List getAlbums() { 233 | return albums; 234 | } 235 | } 236 | 237 | 238 | public class Main { 239 | public final static final void main(String[] args) { 240 | try { 241 | MusicDatabaseConfiguration dbConfig = CommandLineParser.parse(MusicDatabaseConfiguration.class, args, 242 | OptionStyle.LONG_OR_COMPACT); 243 | doSomething(arguments); 244 | } catch (InvalidCommandLineException clException) { 245 | ... 246 | } catch (InvalidOptionConfigurationException configException) { 247 | ... 248 | } catch (UnrecognizedSwitchException unrecognizedSwitchException) { 249 | ... 250 | } 251 | } 252 | } 253 | 254 | As you notice, this code got two option configurations. AlbumConfiguration represents an album, 255 | while MusicDatabaseConfiguration represents information about a music database and a list of albums. 256 | If you give the following command line: 257 | 258 | run.sh -database myalbums.db -verbose 259 | -album -artist "Access to Arasaka" -name "Void();" -year 2010 260 | -album -artist "Decree" -name "Fateless" -year 2011 261 | -album -artist "Karin Park" -name "Ashes to Gold" -year 2009 262 | -album -artist "The Kick" -name "Metal Heart" -year 2010 263 | 264 | You would get an instance of MusicDatabaseConfiguration, containing the values database, verbose 265 | and a list of AlbumConfiguration with 4 elements, each representing one of the 4 albums specified 266 | on the command line. 267 | 268 | ### The LONG_OR_COMPACT OptionStyle ### 269 | 270 | In the LONG_OR_COMPACT OptionStyle, arguments that are prepended with two dashes are considered regular switches, 271 | and are interpreted as such. An argument prepended with only a single dash is parsed as a list of single letter 272 | switches. Although there's separate annotations for short and long switches, they are treated completely alike. 273 | 274 | public class MyConfiguration { 275 | 276 | private boolean verbose; 277 | private boolean optimize; 278 | private boolean compress; 279 | private String name; 280 | 281 | @Option 282 | @LongSwitch("verbose") 283 | @ShortSwitch("v") 284 | @Toggle(true) 285 | public void setVerbose(boolean verbose) { 286 | this.verbose = verbose; 287 | } 288 | 289 | @Option 290 | @LongSwitch("optimize") 291 | @ShortSwitch("o") 292 | @Toggle(true) 293 | public void setOptimize(boolean optimize) { 294 | this.optimize = optimize; 295 | } 296 | 297 | @Option 298 | @LongSwitch("compress") 299 | @ShortSwitch("c") 300 | @Toggle(true) 301 | public void setCompress(boolean compress) { 302 | this.compress = compress; 303 | } 304 | 305 | @Option 306 | @LongSwitch("name") 307 | @ShortSwitch("n") 308 | @SingleArgument 309 | public void setName(String name) { 310 | this.name = name; 311 | } 312 | } 313 | 314 | 315 | public class Main { 316 | public final static final void main(String[] args) { 317 | try { 318 | MusicDatabaseConfiguration dbConfig = CommandLineParser.parse(MusicDatabaseConfiguration.class, args, OptionStyle.SIMPLE); 319 | doSomething(arguments); 320 | } catch (InvalidCommandLineException clException) { 321 | ... 322 | } catch (InvalidOptionConfigurationException configException) { 323 | ... 324 | } catch (UnrecognizedSwitchException unrecognizedSwitchException) { 325 | ... 326 | } 327 | } 328 | } 329 | 330 | With OptionStyle.SIMPLE you run the application with arguments like 331 | 332 | run.sh -name "Keyser Söze" -verbose -optimize -compress 333 | 334 | or the shorter 335 | 336 | run.sh -n "Keyser Söze" -v -o -c 337 | 338 | with OptionStyle.LONG_OR_COMPACT, you can specify the options as 339 | 340 | run.sh --name "Keyser Söze" --verbose --optimize --compress 341 | 342 | or the shorter 343 | 344 | run.sh --n "Keyser Soze" --v --o --c 345 | 346 | You can also use the even shorter 347 | 348 | run.sh -n "Keyser Söze" -v -o -c 349 | 350 | or even 351 | 352 | run.sh -n "Keyser Söze" -voc 353 | 354 | You can also use 355 | 356 | run.sh -vocn "Keyser Söze" 357 | 358 | but this won't work 359 | 360 | run.sh -nvoc "Keyser Söze" 361 | 362 | as the option n will be interpreted as not having any arguments, but be immediately followed by the option v 363 | 364 | ### Using argument delimiters ### 365 | 366 | Sometimes, an option needs arguments that might be interpreted as arguments themselves. The prime 367 | example of this is the find -exec argument. To help in situations like that, it is possible to 368 | specify that an option will consume arguments following it, until the delimiter is encountered. 369 | 370 | class ExecutorConfiguration { 371 | ... 372 | @Option 373 | @LongSwitch("verbose") 374 | @ShortSwitch("v") 375 | @Toggle(true) 376 | public void setVerbose(boolean verbose) { 377 | this.verbose = verbose; 378 | } 379 | 380 | @Option 381 | @LongSwitch("exec") 382 | @ShortSwitch("e") 383 | @UntilDelimiter(";") 384 | public void setVerbose(boolean verbose) { 385 | this.verbose = verbose; 386 | } 387 | } 388 | 389 | with this specification, you can use a command line line 390 | 391 | run.sh -exec ls -lSr -- "*.txt" \; -verbose 392 | 393 | and "ls","l","--","*.txt" and ";" are all considered arguments to the -exec option, 394 | while -verbose is interpreted as a second option. Note also that the use of the delimiter 395 | overrides the meaning of -- (which is to prevent anything following it from being interpreted 396 | as switches. 397 | 398 | ### Annotations ### 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 430 | 431 | 432 | 433 | 436 | 437 | 438 | 439 | 442 | 443 | 444 | 445 |
@OptionSpecifies that the annotated method represents an option
@LongSwitch(switchname)Specifies the long switch used to give this option on the command line
@ShortSwitch(switchname)Specifies the short switch used to give this option on the command line
@LooseArgumentsSpecifies that this setter handles arguments that are not connected to an option
@Toggle(value)Specifies that this option is a toggle. The boolean argument is passed directly to the setter method
@SingleArgumentSpecifies that this option takes a single String argument
@AllAvailableArgumentsSpecifies that this option takes all the argument available until the next switch or end of the argument 428 | list 429 |
@ArgumentsUntilDelimiter(delimiter)Specifies that the option takes all arguments available until it encounters the delimiter or the end of the 434 | argument list. Any switches will be interpreted as regular options 435 |
@SubConfiguration(class)Specifies that parsing should continue based on the specified class. When that parsing is finnished, parsing 440 | will resume for the main configuration 441 |
@MultipleSpecifies that the option might occur multiple times 446 |
447 | 448 | #### Rules #### 449 | 450 | * @Option must be specifies for all methods that should be processed 451 | * For regular options, one of @LongSwitch and @ShortSwitch must be specified, but it's not required to specify 452 | both 453 | * if @LooseArguments is specified, @ShortSwitch and @LongSwitch cannot be specified 454 | * For switches, one of @Toggle, @SingleArgument, @AllAvailableArguments, @ArgumentsUntilDelimiter or 455 | @SubConfiguration must be specified 456 | * @Multiple can be specified for all option types, including loose arguments 457 | * @LongSwitch and @ShortSwitch must be given without leading dashes. 458 | 459 | #### Types #### 460 | 461 | * Toggles require the setter argument to be boolean 462 | * Single arguments require the setter argument to be a String 463 | * Multiple arguments require the setter argument to be a List 464 | * SubConfiguration(class) requires the setter argument to be an instance of the specified class 465 | * LooseArguments requires the setter argument to be String 466 | * When @Multiple is specified, the setter argument needs to be List<T>, where T is the type the 467 | setter would have had if @Multiple had not been specified 468 | 469 | For further examples, please consult the unit tests 470 | 471 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ------------- 3 | 4 | 1.10.0 5 | - Now requires Java 10 6 | - Using the module system to hide internal classes 7 | 8 | 1.7.0 9 | 10 | - Initial release. Requires Java 7 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.github.jankroken 4 | commandline 5 | 1.11rc3 6 | jar 7 | commandline 8 | An annotation based command line parser 9 | http://github.com/jankroken/commandline/ 10 | 2011 11 | 12 | 13 | 14 | The Apache Software License, Version 2.0 15 | http://www.apache.org/licenses/LICENSE-2.0.txt 16 | repo 17 | 18 | 19 | 20 | 21 | scm:git:git://github.com/jankroken/commandline.git 22 | scm:git:git@github.com:jankroken/commandline.git 23 | http://github.com/jankroken/commandline 24 | HEAD 25 | 26 | 27 | 28 | 29 | ossrh 30 | https://oss.sonatype.org/content/repositories/snapshots 31 | 32 | 33 | ossrh 34 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 35 | 36 | 37 | 38 | 39 | 40 | Jan Kroken 41 | jan.kroken@gmail.com 42 | 43 | 44 | 45 | 46 | 47 | org.junit.jupiter 48 | junit-jupiter-api 49 | ${junit.version} 50 | test 51 | 52 | 53 | org.assertj 54 | assertj-core 55 | ${assertj.version} 56 | test 57 | 58 | 59 | 60 | 61 | 62 | org.apache.maven.plugins 63 | maven-compiler-plugin 64 | 3.8.1 65 | 66 | 11 67 | 11 68 | 69 | 70 | 71 | org.ow2.asm 72 | asm 73 | 9.2 74 | 75 | 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-gpg-plugin 80 | 81 | 82 | sign-artifacts 83 | verify 84 | 85 | sign 86 | 87 | 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-source-plugin 93 | 94 | 95 | attach-sources 96 | 97 | jar-no-fork 98 | 99 | 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-javadoc-plugin 105 | 3.3.0 106 | 107 | 108 | -html5 109 | --override-methods=summary 110 | 111 | 112 | 113 | 114 | attach-javadocs 115 | 116 | jar 117 | 118 | 119 | 120 | 121 | 122 | org.ow2.asm 123 | asm 124 | 9.2 125 | 126 | 127 | org.apache.commons 128 | commons-lang3 129 | 3.12.0 130 | 131 | 132 | 133 | 134 | org.sonatype.plugins 135 | nexus-staging-maven-plugin 136 | 1.6.8 137 | true 138 | 139 | ossrh 140 | https://oss.sonatype.org/ 141 | true 142 | 143 | 144 | 145 | 146 | 147 | UTF-8 148 | 5.7.2 149 | 3.20.2 150 | 151 | 152 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/CommandLineParser.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline; 2 | 3 | import com.github.jankroken.commandline.domain.CommandLineException; 4 | import com.github.jankroken.commandline.domain.internal.LongOrCompactTokenizer; 5 | import com.github.jankroken.commandline.domain.internal.OptionSet; 6 | import com.github.jankroken.commandline.domain.internal.SimpleTokenizer; 7 | import com.github.jankroken.commandline.domain.internal.Tokenizer; 8 | import com.github.jankroken.commandline.util.ArrayIterator; 9 | import com.github.jankroken.commandline.util.PeekIterator; 10 | 11 | import java.lang.reflect.InvocationTargetException; 12 | 13 | import static com.github.jankroken.commandline.OptionStyle.SIMPLE; 14 | import static com.github.jankroken.commandline.domain.internal.OptionSetLevel.MAIN_OPTIONS; 15 | 16 | 17 | public class CommandLineParser { 18 | 19 | /** 20 | * A command line parser. It takes two arguments, a class and an argument list, 21 | * and returns an instance of the class, populated from the argument list based 22 | * on the annotations in the class. The class must have a no argument constructor. 23 | * 24 | * @param The type of the provided class 25 | * @param optionClass A class that contains annotated setters that options/arguments will be assigned to 26 | * @param args The provided argument list, typically the argument from the static main method 27 | * @return An instance of the provided class, populated with the options and arguments of the argument list 28 | * @throws IllegalAccessException May be thrown when invoking the setters in the specified class 29 | * @throws InstantiationException May be thrown when creating an instance of the specified class 30 | * @throws InvocationTargetException May be thrown when invoking the setters in the specified class 31 | * @throws com.github.jankroken.commandline.domain.InternalErrorException This indicates an internal error in the parser, and will not normally be thrown 32 | * @throws com.github.jankroken.commandline.domain.InvalidCommandLineException This indicates that the command line specified does not match the annotations in the provided class 33 | * @throws com.github.jankroken.commandline.domain.InvalidOptionConfigurationException This indicates that the annotations of setters in the provided class are not valid 34 | * @throws com.github.jankroken.commandline.domain.UnrecognizedSwitchException This indicates that the argument list contains a switch which is not specified by the class annotations 35 | */ 36 | public static T parse(Class optionClass, String[] args, OptionStyle style) 37 | throws IllegalAccessException, InstantiationException, InvocationTargetException { 38 | T spec; 39 | try { 40 | spec = optionClass.getConstructor().newInstance(); 41 | } catch (NoSuchMethodException noSuchMethodException) { 42 | throw new RuntimeException(noSuchMethodException); 43 | } 44 | var optionSet = new OptionSet(spec, MAIN_OPTIONS); 45 | Tokenizer tokenizer; 46 | if (style == SIMPLE) { 47 | tokenizer = new SimpleTokenizer(new PeekIterator<>(new ArrayIterator<>(args))); 48 | } else { 49 | tokenizer = new LongOrCompactTokenizer(new PeekIterator<>(new ArrayIterator<>(args))); 50 | } 51 | optionSet.consumeOptions(tokenizer); 52 | return spec; 53 | } 54 | 55 | public static ParseResult tryParse(Class optionClass, String[] args, OptionStyle style) { 56 | try { 57 | return new ParseResult(parse(optionClass, args, style)); 58 | } catch(CommandLineException | IllegalAccessException | InstantiationException | InvocationTargetException e) { 59 | return new ParseResult(e); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/OptionStyle.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline; 2 | 3 | public enum OptionStyle { 4 | SIMPLE, 5 | LONG_OR_COMPACT 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/ParseResult.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline; 2 | 3 | import com.github.jankroken.commandline.domain.*; 4 | 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.util.Optional; 7 | import java.util.function.Function; 8 | 9 | import static com.github.jankroken.commandline.ParseResult.ErrorType.*; 10 | 11 | public class ParseResult { 12 | public enum ErrorType { 13 | OK, 14 | INTERNAL_ERROR, 15 | INVALID_COMMAND_LINE, 16 | INVALID_OPTION_CONFIGURATION, 17 | UNRECOGNIZED_SWITCH, 18 | INVALID_CONFIGURATION 19 | } 20 | 21 | private final ErrorType errorType; 22 | private final boolean success; 23 | private final T value; 24 | private final Exception rootCause; 25 | private final String errorMessage; 26 | 27 | protected ParseResult(Exception e, boolean success, T value) { 28 | errorType = success ? OK : toErrorType(e); 29 | rootCause = e; 30 | errorMessage = e == null ? null : e.getMessage(); 31 | this.success = success; 32 | this.value = value; 33 | } 34 | 35 | protected ParseResult(Exception t) { 36 | this(t, false, null); 37 | } 38 | 39 | protected ParseResult(T value) { 40 | this(null, true, value); 41 | } 42 | 43 | public Optional toOptional() { 44 | if (success) return Optional.of(value); 45 | else return Optional.empty(); 46 | } 47 | 48 | public T get() throws CommandLineWrappedException { 49 | if (success) return value; 50 | throw new CommandLineWrappedException(rootCause); 51 | } 52 | 53 | public ParseResult map(Function f) { 54 | if (success) { 55 | return new ParseResult(f.apply(value)); 56 | } else { 57 | return new ParseResult(rootCause); 58 | } 59 | } 60 | 61 | private static ErrorType toErrorType(Exception e) { 62 | if (e instanceof InternalErrorException) return INTERNAL_ERROR; 63 | if (e instanceof InvalidCommandLineException) return INVALID_COMMAND_LINE; 64 | if (e instanceof InvalidOptionConfigurationException) return INVALID_OPTION_CONFIGURATION; 65 | if (e instanceof UnrecognizedSwitchException) return UNRECOGNIZED_SWITCH; 66 | if (e instanceof InvocationTargetException) return INVALID_CONFIGURATION; 67 | if (e instanceof IllegalAccessException) return INVALID_CONFIGURATION; 68 | if (e instanceof InstantiationException) return INVALID_CONFIGURATION; 69 | throw new RuntimeException("Implementation error: unhandled exception", e); 70 | } 71 | 72 | public ErrorType getErrorType() { 73 | return errorType; 74 | } 75 | 76 | public boolean isSuccess() { 77 | return success; 78 | } 79 | 80 | public Exception getRootCause() { 81 | return rootCause; 82 | } 83 | 84 | public String getErrorMessage() { 85 | return errorMessage; 86 | } 87 | } -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/AllAvailableArguments.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Indicates that this option takes all available arguments, ended by either the end of the argument list, 14 | * or by another switch. 15 | */ 16 | public @interface AllAvailableArguments { 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/ArgumentsUntilDelimiter.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Indicates that this option takes any number of arguments, which is terminated either by the end of the 14 | * argument list, or by a delimiter. All values encountered until the delimiter will be taken as arguments, 15 | * even if they starts with a hyphen. The delimiter is not kept as part of the argument list. 16 | * 17 | * @Param the delimiter used to end the argument list 18 | */ 19 | public @interface ArgumentsUntilDelimiter { 20 | String value(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/Description.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Indicates that the given value is a human readable description of the option. This is currently not used, 14 | * but might be used in the future, for instance for automatically generated help texts. 15 | * 16 | * @Param A textual description of the option 17 | */ 18 | public @interface Description { 19 | String value(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/LongSwitch.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Specifies the long switch for this option 14 | * (All options might be indicated by two switches, one long and one short). An example is "--verbose" and "-v" 15 | * 16 | * @Param The switch string 17 | */ 18 | public @interface LongSwitch { 19 | String value(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/LooseArguments.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Indicates that the setter method handles arguments that are not arguments to a switch. 14 | * If this option is specified, LongSwitch and ShortSwitch can not be specified 15 | */ 16 | public @interface LooseArguments { 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/Multiple.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Indicates that an option may occur multiple times. The result of the different instances is gathered 14 | * in a list. This is done even if the result is already a list (for instance if the option takes multiple 15 | * arguments). For instance, the argument type of an option with @Multiple and @AllAvailable specified, will 16 | * need to have an argument type of List>. 17 | */ 18 | public @interface Multiple { 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/Option.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Indicates that the method is a setter for an option argument. Further annotations are needed to fully 14 | * specify an option. 15 | */ 16 | public @interface Option { 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/Required.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Indicates that this option is required. If no matching argument is provided during parsing, an exception will be thrown 14 | */ 15 | public @interface Required { 16 | } -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/ShortSwitch.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Specifies the shortcut switch for this option 14 | * (All options might be indicated by two switches, one long and one short). An example is "--verbose" and "-v" 15 | * 16 | * @Param The switch string 17 | */ 18 | public @interface ShortSwitch { 19 | String value(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/SingleArgument.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Indicates that the switch has a single argument of type String 14 | */ 15 | public @interface SingleArgument { 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/SubConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Specifies a complex argument type, represented by a full annotation option specification 14 | * 15 | * @param value The class that should be instantiated and used to parse the option value 16 | */ 17 | public @interface SubConfiguration { 18 | Class value(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/annotations/Toggle.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | 12 | /** 13 | * Indicate that this switch does not take any arguments, but is a toggle that is provided. 14 | * The toggle has a boolean value argument, which is intended to separate "on-switches" from 15 | * "off-switches" (for instance, --enable-logging, --disable-logging) 16 | * 17 | * @Param value A boolean argument that will be provided as argument to the associated setter 18 | */ 19 | public @interface Toggle { 20 | boolean value(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/ArgumentToken.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain; 2 | 3 | import com.github.jankroken.commandline.domain.internal.Token; 4 | 5 | public class ArgumentToken implements Token { 6 | private final String value; 7 | 8 | public ArgumentToken(String value) { 9 | this.value = value; 10 | } 11 | 12 | public String getValue() { 13 | return value; 14 | } 15 | 16 | public String getArgumentValue() { 17 | return value; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return ""; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/CommandLineException.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain; 2 | 3 | public abstract class CommandLineException extends RuntimeException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | protected CommandLineException() { 7 | } 8 | 9 | protected CommandLineException(String message) { 10 | super(message); 11 | } 12 | 13 | protected CommandLineException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | protected CommandLineException(Throwable cause) { 18 | super(cause); 19 | } 20 | 21 | protected CommandLineException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 22 | super(message, cause, enableSuppression, writableStackTrace); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/CommandLineWrappedException.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain; 2 | 3 | public class CommandLineWrappedException extends CommandLineException { 4 | public CommandLineWrappedException(Throwable cause) { 5 | super(cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/InternalErrorException.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain; 2 | 3 | public class InternalErrorException extends CommandLineException { 4 | private static final long serialVersionUID = 2L; 5 | 6 | public InternalErrorException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/InvalidCommandLineException.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain; 2 | 3 | public class InvalidCommandLineException extends CommandLineException { 4 | private static final long serialVersionUID = 2L; 5 | 6 | public InvalidCommandLineException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/InvalidOptionConfigurationException.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain; 2 | 3 | public class InvalidOptionConfigurationException extends CommandLineException { 4 | private static final long serialVersionUID = 2L; 5 | 6 | public InvalidOptionConfigurationException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/UnrecognizedSwitchException.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain; 2 | 3 | public class UnrecognizedSwitchException extends CommandLineException { 4 | private static final long serialVersionUID = 2L; 5 | private String _switch; 6 | 7 | public UnrecognizedSwitchException(Class configurationClass, String _switch) { 8 | super(configurationClass + ": " + _switch); 9 | this._switch = _switch; 10 | } 11 | 12 | public String getSwitch() { 13 | return _switch; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/ArgumentConsumption.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | public class ArgumentConsumption { 4 | private final ArgumentConsumptionType type; 5 | private String delimiter; 6 | private boolean toggleValue; 7 | private Class subsetClass; 8 | 9 | public ArgumentConsumption(ArgumentConsumptionType type) { 10 | this.type = type; 11 | } 12 | 13 | public ArgumentConsumption(ArgumentConsumptionType type, String delimiter) { 14 | this.type = type; 15 | this.delimiter = delimiter; 16 | } 17 | 18 | public ArgumentConsumption(ArgumentConsumptionType type, boolean toggleValue) { 19 | this.type = type; 20 | this.toggleValue = toggleValue; 21 | } 22 | 23 | public ArgumentConsumption(ArgumentConsumptionType type, Class subsetClass) { 24 | this.type = type; 25 | this.subsetClass = subsetClass; 26 | } 27 | 28 | public ArgumentConsumptionType getType() { 29 | return type; 30 | } 31 | 32 | public String getDelimiter() { 33 | return delimiter; 34 | } 35 | 36 | public Class getSubsetClass() { 37 | return subsetClass; 38 | } 39 | 40 | public boolean getToggleValue() { 41 | return toggleValue; 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/ArgumentConsumptionBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | import com.github.jankroken.commandline.domain.InternalErrorException; 4 | import com.github.jankroken.commandline.domain.InvalidOptionConfigurationException; 5 | 6 | import static com.github.jankroken.commandline.domain.internal.ArgumentConsumptionType.*; 7 | 8 | public class ArgumentConsumptionBuilder { 9 | private boolean no_arguments = false; 10 | private boolean toggle_value = false; 11 | private boolean single_argument = false; 12 | private boolean until_delimiter = false; 13 | private boolean all_available = false; 14 | private boolean sub_set = false; 15 | private boolean loose_args = false; 16 | private String delimiter = null; 17 | private Class subsetClass = null; 18 | 19 | public void addNoArgs(boolean value) { 20 | no_arguments = true; 21 | toggle_value = value; 22 | } 23 | 24 | public void addSingleArgument() { 25 | single_argument = true; 26 | } 27 | 28 | public void addUntilDelimiter(String delimiter) { 29 | until_delimiter = true; 30 | this.delimiter = delimiter; 31 | } 32 | 33 | public void addAllAvailable() { 34 | all_available = true; 35 | } 36 | 37 | public void addLooseArgs() { 38 | loose_args = true; 39 | } 40 | 41 | public void addSubSet(Class subsetClass) { 42 | this.sub_set = true; 43 | this.subsetClass = subsetClass; 44 | } 45 | 46 | public ArgumentConsumption getArgumentConsumption() { 47 | var argumentConsumptionTypeCounter = 0; 48 | if (no_arguments) argumentConsumptionTypeCounter++; 49 | if (single_argument) argumentConsumptionTypeCounter++; 50 | if (until_delimiter) argumentConsumptionTypeCounter++; 51 | if (all_available) argumentConsumptionTypeCounter++; 52 | if (sub_set) argumentConsumptionTypeCounter++; 53 | if (loose_args) argumentConsumptionTypeCounter++; 54 | if (argumentConsumptionTypeCounter == 0) { 55 | throw new InvalidOptionConfigurationException("No argument consumption type specified"); 56 | } 57 | if (argumentConsumptionTypeCounter > 1) { 58 | throw new InvalidOptionConfigurationException("Multiple argument consumption types specified"); 59 | } 60 | 61 | if (no_arguments) { 62 | return new ArgumentConsumption(NO_ARGS, toggle_value); 63 | } 64 | if (single_argument) { 65 | return new ArgumentConsumption(SINGLE_ARGUMENT); 66 | } 67 | if (until_delimiter) { 68 | return new ArgumentConsumption(UNTIL_DELIMITER, delimiter); 69 | } 70 | if (all_available) { 71 | return new ArgumentConsumption(ALL_AVAILABLE); 72 | } 73 | if (sub_set) { 74 | return new ArgumentConsumption(SUB_SET, subsetClass); 75 | } 76 | if (loose_args) { 77 | return new ArgumentConsumption(LOOSE_ARGS); 78 | } 79 | throw new InternalErrorException("Internal error: no matching argument consumption types"); 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/ArgumentConsumptionType.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | public enum ArgumentConsumptionType { 4 | NO_ARGS, 5 | SINGLE_ARGUMENT, 6 | ALL_AVAILABLE, 7 | UNTIL_DELIMITER, 8 | SUB_SET, 9 | LOOSE_ARGS 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/LongOrCompactTokenizer.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | import com.github.jankroken.commandline.domain.ArgumentToken; 4 | import com.github.jankroken.commandline.util.PeekIterator; 5 | 6 | import java.util.ArrayList; 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | import java.util.Objects; 10 | import java.util.regex.Pattern; 11 | 12 | public class LongOrCompactTokenizer implements Tokenizer { 13 | 14 | private static final Pattern SWITCH_PATTERN = Pattern.compile("-.*"); 15 | private static final Pattern LONG_STYLE_SWITCH_PATTERN = Pattern.compile("--..*"); 16 | private static final Pattern SHORT_STYLE_SWITCH_PATTERN = Pattern.compile("-[^-].*"); 17 | private final PeekIterator stringIterator; 18 | private boolean argumentEscapeEncountered; 19 | private String argumentTerminator; 20 | private final LinkedList splitTokens = new LinkedList<>(); 21 | 22 | public LongOrCompactTokenizer(final PeekIterator stringIterator) { 23 | this.stringIterator = stringIterator; 24 | } 25 | 26 | public void setArgumentTerminator(String argumentTerminator) { 27 | this.argumentTerminator = argumentTerminator; 28 | } 29 | 30 | public boolean hasNext() { 31 | return stringIterator.hasNext(); 32 | } 33 | 34 | // this is a mess - need to be cleaned up 35 | public Token peek() { 36 | if (!splitTokens.isEmpty()) { 37 | return splitTokens.peek(); 38 | } 39 | final var value = stringIterator.peek(); 40 | 41 | if (argumentEscapeEncountered) { 42 | return new ArgumentToken(value); 43 | } 44 | 45 | if (Objects.equals(value, argumentTerminator)) { 46 | return new ArgumentToken(value); 47 | } 48 | if (argumentTerminator != null) { 49 | return new ArgumentToken(value); 50 | } 51 | if (isArgumentEscape(value)) { 52 | argumentEscapeEncountered = true; 53 | return peek(); 54 | } 55 | if (isSwitch(value)) { 56 | if (isShortSwitchList(value)) { 57 | var tokens = splitSwitchTokens(value); 58 | return tokens.get(0); 59 | } else { 60 | return new SwitchToken(value.substring(2), value); 61 | } 62 | } else { 63 | return new ArgumentToken(value); 64 | } 65 | } 66 | 67 | // this is a mess - needs to be cleaned up 68 | public Token next() { 69 | if (!splitTokens.isEmpty()) { 70 | return splitTokens.remove(); 71 | } 72 | var value = stringIterator.next(); 73 | 74 | if (argumentEscapeEncountered) { 75 | return new ArgumentToken(value); 76 | } 77 | 78 | if (Objects.equals(value, argumentTerminator)) { 79 | argumentTerminator = null; 80 | return new ArgumentToken(value); 81 | } 82 | if (argumentTerminator != null) { 83 | return new ArgumentToken(value); 84 | } 85 | if (isArgumentEscape(value)) { 86 | argumentEscapeEncountered = true; 87 | return next(); 88 | } 89 | if (isSwitch(value)) { 90 | if (isShortSwitchList(value)) { 91 | splitTokens.addAll(splitSwitchTokens(value)); 92 | return splitTokens.remove(); 93 | } else { 94 | return new SwitchToken(value.substring(2), value); 95 | } 96 | } else { 97 | return new ArgumentToken(value); 98 | } 99 | } 100 | 101 | public void remove() { 102 | stringIterator.remove(); 103 | } 104 | 105 | private static boolean isSwitch(final String argument) { 106 | return SWITCH_PATTERN.matcher(argument).matches(); 107 | } 108 | 109 | private boolean isArgumentEscape(final String value) { 110 | return ("--".equals(value) && !argumentEscapeEncountered); 111 | } 112 | 113 | private boolean isLongSwitch(final String value) { 114 | return LONG_STYLE_SWITCH_PATTERN.matcher(value).matches(); 115 | } 116 | 117 | private boolean isShortSwitchList(final String value) { 118 | return SHORT_STYLE_SWITCH_PATTERN.matcher(value).matches(); 119 | } 120 | 121 | private List splitSwitchTokens(final String value) { 122 | final var tokens = new ArrayList(); 123 | for (var i = 1; i < value.length(); i++) { 124 | tokens.add(new SwitchToken(String.valueOf(value.charAt(i)), value)); 125 | } 126 | return tokens; 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/Occurrences.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | public enum Occurrences { 4 | SINGLE, 5 | MULTIPLE 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/OptionSet.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | import com.github.jankroken.commandline.domain.InvalidCommandLineException; 4 | import com.github.jankroken.commandline.domain.UnrecognizedSwitchException; 5 | 6 | import java.lang.reflect.InvocationTargetException; 7 | import java.util.List; 8 | 9 | public class OptionSet { 10 | private final List options; 11 | private final OptionSetLevel optionSetLevel; 12 | private final Object spec; 13 | 14 | public OptionSet(Object spec, OptionSetLevel optionSetLevel) { 15 | options = OptionSpecificationFactory.getOptionSpecifications(spec, spec.getClass()); 16 | this.optionSetLevel = optionSetLevel; 17 | this.spec = spec; 18 | } 19 | 20 | public OptionSpecification getOptionSpecification(SwitchToken _switch) { 21 | for (final var optionSpecification : options) { 22 | if (optionSpecification.getSwitch().matches(_switch.getValue())) { 23 | return optionSpecification; 24 | } 25 | } 26 | return null; 27 | } 28 | 29 | public OptionSpecification getLooseArgsOptionSpecification() { 30 | for (final var optionSpecification : options) { 31 | if (optionSpecification.isLooseArgumentsSpecification()) { 32 | return optionSpecification; 33 | } 34 | } 35 | return null; 36 | } 37 | 38 | public void consumeOptions(Tokenizer args) 39 | throws IllegalAccessException, InvocationTargetException, InstantiationException { 40 | while (args.hasNext()) { 41 | if (args.peek() instanceof SwitchToken) { 42 | var optionSpecification = getOptionSpecification((SwitchToken) args.peek()); 43 | if (optionSpecification == null) { 44 | switch (optionSetLevel) { 45 | case MAIN_OPTIONS: 46 | throw new UnrecognizedSwitchException(spec.getClass(), args.peek().getValue()); 47 | case SUB_GROUP: 48 | validateAndConsolidate(); 49 | return; 50 | } 51 | } else { 52 | args.next(); 53 | optionSpecification.activateAndConsumeArguments(args); 54 | } 55 | } else { 56 | var looseArgsOptionSpecification = getLooseArgsOptionSpecification(); 57 | if (looseArgsOptionSpecification != null) { 58 | looseArgsOptionSpecification.activateAndConsumeArguments(args); 59 | } else { 60 | switch (optionSetLevel) { 61 | case MAIN_OPTIONS: 62 | throw new InvalidCommandLineException("Invalid argument: " + args.peek()); 63 | case SUB_GROUP: 64 | validateAndConsolidate(); 65 | return; 66 | } 67 | } 68 | } 69 | } 70 | flush(); 71 | } 72 | 73 | private void flush() 74 | throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { 75 | for (final var option : options) { 76 | option.flush(); 77 | } 78 | } 79 | 80 | public void validateAndConsolidate() { 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/OptionSetLevel.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | public enum OptionSetLevel { 4 | MAIN_OPTIONS, 5 | SUB_GROUP 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/OptionSpecification.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | import com.github.jankroken.commandline.domain.InternalErrorException; 4 | import com.github.jankroken.commandline.domain.InvalidCommandLineException; 5 | import com.github.jankroken.commandline.domain.InvalidOptionConfigurationException; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | import java.lang.reflect.ParameterizedType; 10 | import java.lang.reflect.Type; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Objects; 14 | 15 | public class OptionSpecification { 16 | private final Method method; 17 | private boolean activated; 18 | private final Switch _switch; 19 | private final ArgumentConsumption argumentConsumption; 20 | private final boolean required; 21 | private final Occurrences occurrences; 22 | private final Object spec; 23 | private final ArrayList argumentBuffer = new ArrayList<>(); 24 | 25 | public OptionSpecification(Object spec, Method method, Switch _switch, ArgumentConsumption argumentConsumption, boolean required, Occurrences occurrences) { 26 | this.spec = spec; 27 | this._switch = _switch; 28 | this.method = method; 29 | this.argumentConsumption = argumentConsumption; 30 | this.required = required; 31 | this.occurrences = occurrences; 32 | validate(); 33 | } 34 | 35 | public boolean isLooseArgumentsSpecification() { 36 | return argumentConsumption.getType() == ArgumentConsumptionType.LOOSE_ARGS; 37 | } 38 | 39 | public void validate() { 40 | if (argumentConsumption.getType() != ArgumentConsumptionType.LOOSE_ARGS) { 41 | if (_switch == null || (_switch.getShortSwitch() == null && _switch.getLongSwitch() == null)) { 42 | throw createInvalidOptionSpecificationException("Option specified without switches"); 43 | } 44 | } 45 | validateType(); 46 | } 47 | 48 | private void validateType() { 49 | var types = method.getGenericParameterTypes(); 50 | if (types == null || types.length != 1) { 51 | throw createInvalidOptionSpecificationException("Wrong number of arguments, expecting exactly one"); 52 | } 53 | var type = types[0]; 54 | var listLevel = getListLevel(); 55 | verifyType(listLevel, getInnerArgumentType(), type, type); 56 | } 57 | 58 | @SuppressWarnings("unchecked") 59 | private void verifyType(int listLevel, Class innerClass, Type type, Type fullType) { 60 | if (listLevel > 0) { 61 | if (!((toClass(type)).isAssignableFrom(List.class))) { 62 | wrongType(fullType); 63 | } else { 64 | var innerType = ((ParameterizedType) type).getActualTypeArguments()[0]; 65 | verifyType(listLevel - 1, innerClass, innerType, fullType); 66 | } 67 | } else { 68 | if (!box(toClass(type)).isAssignableFrom(innerClass)) { 69 | wrongType(fullType); 70 | } 71 | } 72 | } 73 | 74 | private Class toClass(Type type) { 75 | if (type instanceof ParameterizedType) { 76 | return (Class) ((ParameterizedType) type).getRawType(); 77 | } else if (type instanceof Class) { 78 | return (Class) type; 79 | } else { 80 | throw createInternalErrorException("Don't know how to get the class from type " + type); 81 | } 82 | } 83 | 84 | @SuppressWarnings("unchecked") 85 | private static Class box(Class rawClass) { 86 | if (rawClass == boolean.class) { 87 | return Boolean.class; 88 | } else { 89 | return rawClass; 90 | } 91 | } 92 | 93 | private void wrongType(Type type) { 94 | throw createInvalidOptionSpecificationException("Wrong argument type, expected " + getExpectedTypeDescription() + " but found " + type); 95 | } 96 | 97 | private String getExpectedTypeDescription() { 98 | var sb = new StringBuilder(); 99 | final var listLevel = getListLevel(); 100 | for (var i = 0; i < listLevel; i++) { 101 | sb.append("List<"); 102 | } 103 | sb.append(getInnerArgumentType()); 104 | for (var i = 0; i < listLevel; i++) { 105 | sb.append(">"); 106 | } 107 | return sb.toString(); 108 | } 109 | 110 | private int getListLevel() { 111 | var listLevel = 0; 112 | if (occurrences == Occurrences.MULTIPLE) { 113 | listLevel++; 114 | } 115 | switch (argumentConsumption.getType()) { 116 | case NO_ARGS: 117 | case SINGLE_ARGUMENT: 118 | case SUB_SET: 119 | break; 120 | case ALL_AVAILABLE: 121 | case UNTIL_DELIMITER: 122 | listLevel++; 123 | break; 124 | } 125 | return listLevel; 126 | } 127 | 128 | private Class getInnerArgumentType() { 129 | switch (argumentConsumption.getType()) { 130 | case NO_ARGS: 131 | return Boolean.class; 132 | case SINGLE_ARGUMENT: 133 | return String.class; 134 | case SUB_SET: 135 | return argumentConsumption.getSubsetClass(); 136 | case ALL_AVAILABLE: 137 | return String.class; 138 | case UNTIL_DELIMITER: 139 | return String.class; 140 | case LOOSE_ARGS: 141 | return String.class; 142 | default: 143 | throw new RuntimeException("Internal error"); 144 | } 145 | } 146 | 147 | public Switch getSwitch() { 148 | return _switch; 149 | } 150 | 151 | public void activateAndConsumeArguments(Tokenizer args) 152 | throws InvocationTargetException, IllegalAccessException, InstantiationException { 153 | activated = true; 154 | switch (argumentConsumption.getType()) { 155 | case NO_ARGS: 156 | handleArguments(argumentConsumption.getToggleValue()); 157 | break; 158 | case SINGLE_ARGUMENT: 159 | if (!args.hasNext() || args.peek() instanceof SwitchToken) { 160 | throw createInvalidCommandLineException("Missing argument"); 161 | } 162 | final var argument = args.next().getValue(); 163 | handleArguments(argument); 164 | break; 165 | case ALL_AVAILABLE: 166 | final ArrayList allArguments = new ArrayList<>(); 167 | while (args.hasNext() && !(args.peek() instanceof SwitchToken)) { 168 | allArguments.add(args.next().getValue()); 169 | } 170 | handleArguments(allArguments); 171 | break; 172 | case UNTIL_DELIMITER: 173 | args.setArgumentTerminator(argumentConsumption.getDelimiter()); 174 | final var delimitedArguments = new ArrayList(); 175 | while (args.hasNext() && !Objects.equals(args.peek().getValue(), argumentConsumption.getDelimiter())) { 176 | delimitedArguments.add(args.next().getArgumentValue()); 177 | } 178 | if (args.hasNext()) args.next(); 179 | handleArguments(delimitedArguments); 180 | break; 181 | case SUB_SET: 182 | final Object subset; 183 | try { 184 | subset = argumentConsumption.getSubsetClass().getConstructor().newInstance(); 185 | } catch (NoSuchMethodException noSuchMethodException) { 186 | throw new RuntimeException(noSuchMethodException); 187 | } 188 | final var subsetOptions = new OptionSet(subset, OptionSetLevel.SUB_GROUP); 189 | subsetOptions.consumeOptions(args); 190 | handleArguments(subset); 191 | break; 192 | case LOOSE_ARGS: 193 | handleArguments(args.next().getValue()); 194 | break; 195 | default: 196 | throw createInternalErrorException("Not implemented: " + argumentConsumption.getType()); 197 | } 198 | } 199 | 200 | private void handleArguments(Object args) 201 | throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { 202 | if (occurrences == Occurrences.SINGLE) { 203 | method.invoke(spec, args); 204 | } else { 205 | argumentBuffer.add(args); 206 | } 207 | } 208 | 209 | 210 | public void flush() 211 | throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { 212 | if (required && !activated) { 213 | throw createInvalidCommandLineException("Required argument not specified"); 214 | } 215 | if (occurrences == Occurrences.MULTIPLE) { 216 | method.invoke(spec, argumentBuffer); 217 | } 218 | } 219 | 220 | private InvalidOptionConfigurationException createInvalidOptionSpecificationException(String description) { 221 | return new InvalidOptionConfigurationException(getOptionId() + ' ' + description); 222 | } 223 | 224 | private InvalidCommandLineException createInvalidCommandLineException(String description) { 225 | return new InvalidCommandLineException(getOptionId() + ' ' + description); 226 | } 227 | 228 | private RuntimeException createInternalErrorException(String description) { 229 | return new InternalErrorException(getOptionId() + ' ' + description); 230 | } 231 | 232 | public String getOptionId() { 233 | return "[" + 234 | spec.getClass().getName() + 235 | ":" + 236 | method.getName() + 237 | "]"; 238 | } 239 | 240 | } -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/OptionSpecificationBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | public class OptionSpecificationBuilder { 6 | 7 | private String longSwitch; 8 | private String shortSwitch; 9 | private final ArgumentConsumptionBuilder argumentConsumptionBuilder = new ArgumentConsumptionBuilder(); 10 | private Method method; 11 | private Occurrences occurrences = Occurrences.SINGLE; 12 | private boolean required; 13 | private Object spec; 14 | 15 | public void addMethod(Method method) { 16 | this.method = method; 17 | } 18 | 19 | public void addLongSwitch(String longSwitch) { 20 | this.longSwitch = longSwitch; 21 | } 22 | 23 | public void addShortSwitch(String shortSwitch) { 24 | this.shortSwitch = shortSwitch; 25 | } 26 | 27 | public void addToggle(boolean value) { 28 | argumentConsumptionBuilder.addNoArgs(value); 29 | } 30 | 31 | public void addSingleArgument() { 32 | argumentConsumptionBuilder.addSingleArgument(); 33 | } 34 | 35 | public void addAllAvailableArguments() { 36 | argumentConsumptionBuilder.addAllAvailable(); 37 | } 38 | 39 | public void addUntilDelimiter(String delimiter) { 40 | argumentConsumptionBuilder.addUntilDelimiter(delimiter); 41 | } 42 | 43 | public void addSubset(Class optionClass) { 44 | argumentConsumptionBuilder.addSubSet(optionClass); 45 | } 46 | 47 | public void addLooseArgs() { 48 | argumentConsumptionBuilder.addLooseArgs(); 49 | } 50 | 51 | public void addRequired() { 52 | required = true; 53 | } 54 | 55 | public void addOccurrences(Occurrences occurrences) { 56 | this.occurrences = occurrences; 57 | } 58 | 59 | public void addConfiguration(Object spec) { 60 | this.spec = spec; 61 | } 62 | 63 | public OptionSpecification getOptionSpecification() { 64 | var _switch = new Switch(longSwitch, shortSwitch); 65 | var argumentConsumption = argumentConsumptionBuilder.getArgumentConsumption(); 66 | return new OptionSpecification(spec, method, _switch, argumentConsumption, required, occurrences); 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/OptionSpecificationFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | import com.github.jankroken.commandline.annotations.*; 4 | import com.github.jankroken.commandline.util.Methods; 5 | 6 | import java.lang.reflect.Method; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | 11 | public class OptionSpecificationFactory { 12 | 13 | 14 | public static OptionSpecification getOptionSpecification(Object spec, Method method) { 15 | var builder = new OptionSpecificationBuilder(); 16 | builder.addMethod(method); 17 | builder.addConfiguration(spec); 18 | for (var annotation : method.getAnnotations()) { 19 | if (annotation instanceof Option) { 20 | } else if (annotation instanceof LongSwitch) { 21 | builder.addLongSwitch(((LongSwitch) annotation).value()); 22 | } else if (annotation instanceof ShortSwitch) { 23 | builder.addShortSwitch(((ShortSwitch) annotation).value()); 24 | } else if (annotation instanceof Toggle) { 25 | builder.addToggle(((Toggle) annotation).value()); 26 | } else if (annotation instanceof SingleArgument) { 27 | builder.addSingleArgument(); 28 | } else if (annotation instanceof AllAvailableArguments) { 29 | builder.addAllAvailableArguments(); 30 | } else if (annotation instanceof ArgumentsUntilDelimiter) { 31 | builder.addUntilDelimiter(((ArgumentsUntilDelimiter) annotation).value()); 32 | } else if (annotation instanceof SubConfiguration) { 33 | builder.addSubset(((SubConfiguration) annotation).value()); 34 | } else if (annotation instanceof Required) { 35 | builder.addRequired(); 36 | } else if (annotation instanceof Multiple) { 37 | builder.addOccurrences(Occurrences.MULTIPLE); 38 | } else if (annotation instanceof LooseArguments) { 39 | builder.addLooseArgs(); 40 | } else { 41 | // todo 42 | System.out.println("Unhandled annotation: " + annotation); 43 | } 44 | } 45 | return builder.getOptionSpecification(); 46 | 47 | } 48 | 49 | public static List getOptionSpecifications(Object spec, Class optionClass) { 50 | var methods = new Methods(optionClass).byAnnotation(Option.class); 51 | List optionSpecifications = new ArrayList<>(); 52 | for (var method : methods) { 53 | optionSpecifications.add(getOptionSpecification(spec, method)); 54 | } 55 | return optionSpecifications; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/SimpleTokenizer.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | import com.github.jankroken.commandline.domain.ArgumentToken; 4 | import com.github.jankroken.commandline.util.PeekIterator; 5 | 6 | import java.util.Objects; 7 | 8 | public class SimpleTokenizer implements Tokenizer { 9 | 10 | private final PeekIterator stringIterator; 11 | private boolean argumentEscapeEncountered; 12 | private String argumentTerminator; 13 | 14 | public SimpleTokenizer(PeekIterator stringIterator) { 15 | this.stringIterator = stringIterator; 16 | } 17 | 18 | public void setArgumentTerminator(String argumentTerminator) { 19 | this.argumentTerminator = argumentTerminator; 20 | } 21 | 22 | public boolean hasNext() { 23 | return stringIterator.hasNext(); 24 | } 25 | 26 | public Token peek() { 27 | final var value = stringIterator.peek(); 28 | return makeToken(value); 29 | } 30 | 31 | public Token next() { 32 | final var value = stringIterator.next(); 33 | return makeToken(value); 34 | } 35 | 36 | private Token makeToken(String value) { 37 | if (Objects.equals(value, argumentTerminator)) { 38 | argumentTerminator = null; 39 | return new ArgumentToken(value); 40 | } 41 | if (argumentTerminator != null) { 42 | return new ArgumentToken(value); 43 | } 44 | if (isArgumentEscape(value)) { 45 | argumentEscapeEncountered = true; 46 | return next(); 47 | } 48 | if (!argumentEscapeEncountered && isSwitch(value)) { 49 | return new SwitchToken(value.substring(1), value); 50 | } else { 51 | return new ArgumentToken(value); 52 | } 53 | } 54 | 55 | public void remove() { 56 | stringIterator.remove(); 57 | } 58 | 59 | 60 | private static boolean isSwitch(String argument) { 61 | return argument.matches("-.*"); 62 | } 63 | 64 | private boolean isArgumentEscape(String value) { 65 | return ("--".equals(value) && !argumentEscapeEncountered); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/Switch.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | public class Switch { 4 | private final String longSwitch; 5 | private final String shortSwitch; 6 | 7 | public Switch(String longSwitch, String shortSwitch) { 8 | this.longSwitch = longSwitch; 9 | this.shortSwitch = shortSwitch; 10 | } 11 | 12 | public String getLongSwitch() { 13 | return longSwitch; 14 | } 15 | 16 | public String getShortSwitch() { 17 | return shortSwitch; 18 | } 19 | 20 | public boolean matches(String _switch) { 21 | return (_switch.equals(longSwitch) || _switch.equals(shortSwitch)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/SwitchToken.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | public class SwitchToken implements Token { 4 | private final String value; 5 | private final String argumentValue; 6 | 7 | public SwitchToken(String value, String argumentValue) { 8 | this.value = value; 9 | this.argumentValue = argumentValue; 10 | } 11 | 12 | public String getValue() { 13 | return value; 14 | } 15 | 16 | public String getArgumentValue() { 17 | return argumentValue; 18 | } 19 | 20 | public String toString() { 21 | return ""; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/Token.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | public interface Token { 4 | String getValue(); 5 | 6 | String getArgumentValue(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/TokenQueue.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | import com.github.jankroken.commandline.util.PeekIterator; 4 | 5 | import java.util.LinkedList; 6 | 7 | public class TokenQueue { 8 | 9 | private PeekIterator iterator; 10 | private final LinkedList tokens = new LinkedList<>(); 11 | private PeekIterator stringIterator; 12 | private boolean argumentEscapeEncountered = false; 13 | private String argumentTerminator = null; 14 | private LinkedList splitTokens = new LinkedList<>(); 15 | 16 | public boolean hasMore() { 17 | return !tokens.isEmpty() && !iterator.hasNext(); 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/domain/internal/Tokenizer.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain.internal; 2 | 3 | import java.util.Iterator; 4 | 5 | public interface Tokenizer extends Iterator { 6 | 7 | void setArgumentTerminator(String argumentTerminator); 8 | 9 | boolean hasNext(); 10 | 11 | Token peek(); 12 | 13 | Token next(); 14 | 15 | void remove(); 16 | } -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/util/ArrayIterator.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.util; 2 | 3 | import java.util.Iterator; 4 | import java.util.NoSuchElementException; 5 | 6 | public class ArrayIterator implements Iterator { 7 | 8 | private final T[] array; 9 | private int index = 0; 10 | 11 | public ArrayIterator(T[] array) { 12 | this.array = array; 13 | } 14 | 15 | public boolean hasNext() { 16 | return array.length > index; 17 | } 18 | 19 | public T next() { 20 | if (array.length > index) { 21 | return array[index++]; 22 | } else { 23 | throw new NoSuchElementException(); 24 | } 25 | } 26 | 27 | public T peek() { 28 | if (array.length > index) { 29 | return array[index]; 30 | } else { 31 | throw new NoSuchElementException(); 32 | } 33 | } 34 | 35 | public void remove() { 36 | throw new UnsupportedOperationException(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/util/Constants.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.util; 2 | 3 | public class Constants { 4 | public static final String[] EMPTY_STRING_ARRAY = new String[]{}; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/util/Methods.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.util; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.Method; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class Methods { 9 | 10 | private final Class annotatedClass; 11 | 12 | public Methods(Class annotatedClass) { 13 | this.annotatedClass = annotatedClass; 14 | } 15 | 16 | public List byAnnotation(Class searchAnnotation) { 17 | List methods = new ArrayList<>(); 18 | methodLoop: 19 | for (final var method : annotatedClass.getMethods()) { 20 | if (method.isAnnotationPresent(searchAnnotation)) { 21 | methods.add(method); 22 | continue methodLoop; 23 | } 24 | } 25 | return methods; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/jankroken/commandline/util/PeekIterator.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.util; 2 | 3 | import java.util.Iterator; 4 | 5 | public class PeekIterator implements Iterator { 6 | 7 | private T nextValue; 8 | private final Iterator iterator; 9 | 10 | public PeekIterator(Iterator iterator) { 11 | this.iterator = iterator; 12 | } 13 | 14 | public boolean hasNext() { 15 | return nextValue != null || iterator.hasNext(); 16 | } 17 | 18 | public T next() { 19 | if (nextValue != null) { 20 | var value = nextValue; 21 | nextValue = null; 22 | return value; 23 | } else { 24 | return iterator.next(); 25 | } 26 | } 27 | 28 | public T peek() { 29 | if (nextValue == null) { 30 | nextValue = iterator.next(); 31 | } 32 | return nextValue; 33 | } 34 | 35 | public void remove() { 36 | throw new UnsupportedOperationException(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module commandline { 2 | exports com.github.jankroken.commandline; 3 | exports com.github.jankroken.commandline.annotations; 4 | exports com.github.jankroken.commandline.domain; 5 | } -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/domain/LongOrCompactTokenizerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain; 2 | 3 | import com.github.jankroken.commandline.domain.internal.LongOrCompactTokenizer; 4 | import com.github.jankroken.commandline.util.ArrayIterator; 5 | import com.github.jankroken.commandline.util.PeekIterator; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | public class LongOrCompactTokenizerTest { 11 | 12 | @Test 13 | public void simpleArgumentSplit() { 14 | final var args = new String[]{"-abcf", "hello.txt"}; 15 | final var peekIterator = new PeekIterator<>(new ArrayIterator<>(args)); 16 | final var tokenizer = new LongOrCompactTokenizer(peekIterator); 17 | assertThat(tokenizer.next().getValue()).isEqualTo("a"); 18 | assertThat(tokenizer.next().getValue()).isEqualTo("b"); 19 | assertThat(tokenizer.next().getValue()).isEqualTo("c"); 20 | assertThat(tokenizer.next().getValue()).isEqualTo("f"); 21 | assertThat(tokenizer.next().getValue()).isEqualTo("hello.txt"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/domain/OptionSpecificationFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.domain; 2 | 3 | import com.github.jankroken.commandline.domain.internal.OptionSpecificationFactory; 4 | import com.github.jankroken.commandline.happy.SimpleConfiguration; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class OptionSpecificationFactoryTest { 10 | 11 | @Test 12 | public void testOptionSpecificationFactory() { 13 | var conf = new SimpleConfiguration(); 14 | var specifications = OptionSpecificationFactory.getOptionSpecifications(conf, conf.getClass()); 15 | assertEquals(specifications.size(), 2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/error/InvalidArgumentsTests.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.error; 2 | 3 | import com.github.jankroken.commandline.ParseResult; 4 | import com.github.jankroken.commandline.domain.CommandLineWrappedException; 5 | import com.github.jankroken.commandline.domain.InvalidCommandLineException; 6 | import com.github.jankroken.commandline.domain.UnrecognizedSwitchException; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static com.github.jankroken.commandline.CommandLineParser.parse; 10 | import static com.github.jankroken.commandline.CommandLineParser.tryParse; 11 | import static com.github.jankroken.commandline.OptionStyle.SIMPLE; 12 | import static com.github.jankroken.commandline.util.Constants.EMPTY_STRING_ARRAY; 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 15 | 16 | 17 | public class InvalidArgumentsTests { 18 | 19 | @Test 20 | public void testMissingSwitches() { 21 | var args = EMPTY_STRING_ARRAY; 22 | assertThatThrownBy(() -> parse(RequiredConfiguration.class, args, SIMPLE)) 23 | .isInstanceOf(InvalidCommandLineException.class); 24 | } 25 | 26 | @Test 27 | public void testUnrecognizedSwitch() { 28 | var args = new String[]{"-invalidswitch", "-filename", "hello.txt"}; 29 | assertThatThrownBy(() -> parse(RequiredConfiguration.class, args, SIMPLE)) 30 | .isInstanceOf(UnrecognizedSwitchException.class); 31 | } 32 | 33 | @Test 34 | public void testMissingSwitchesParseResult() { 35 | var args = EMPTY_STRING_ARRAY; 36 | var result = tryParse(RequiredConfiguration.class, args, SIMPLE); 37 | assertThat(result.isSuccess()).isFalse(); 38 | assertThat(result.getErrorType()).isEqualTo(ParseResult.ErrorType.INVALID_COMMAND_LINE); 39 | } 40 | 41 | @Test 42 | public void testUnrecognizedSwitchParseResultGet() { 43 | var args = new String[]{"-invalidswitch", "-filename", "hello.txt"}; 44 | assertThatThrownBy(() -> tryParse(RequiredConfiguration.class, args, SIMPLE).get()) 45 | .isInstanceOf(CommandLineWrappedException.class); 46 | } 47 | 48 | 49 | } -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/error/InvalidConfigurationTests.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.error; 2 | 3 | import com.github.jankroken.commandline.domain.InvalidOptionConfigurationException; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.github.jankroken.commandline.CommandLineParser.parse; 7 | import static com.github.jankroken.commandline.OptionStyle.SIMPLE; 8 | import static com.github.jankroken.commandline.util.Constants.EMPTY_STRING_ARRAY; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | 12 | public class InvalidConfigurationTests { 13 | 14 | @Test 15 | public void testMissingSwitches() { 16 | assertThatThrownBy(() -> parse(MissingSwitchesConfiguration.class, EMPTY_STRING_ARRAY, SIMPLE)) 17 | .isInstanceOf(InvalidOptionConfigurationException.class); 18 | } 19 | 20 | @Test 21 | public void testMissingArgumentConsumption() { 22 | assertThatThrownBy(() -> parse(MissingArgumentConsumptionConfiguration.class, EMPTY_STRING_ARRAY, SIMPLE)) 23 | .isInstanceOf(InvalidOptionConfigurationException.class); 24 | } 25 | 26 | @Test 27 | public void testMultipleArgumentConsumptions() { 28 | assertThatThrownBy(() -> parse(MultipleConsumptionsConfiguration.class, EMPTY_STRING_ARRAY, SIMPLE)) 29 | .isInstanceOf(InvalidOptionConfigurationException.class); 30 | } 31 | 32 | @Test 33 | public void testInvalidType() { 34 | var args = new String[]{"-filename", "hello.txt"}; 35 | assertThatThrownBy(() -> parse(InvalidTypeConfiguration.class, args, SIMPLE)) 36 | .isInstanceOf(InvalidOptionConfigurationException.class); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/error/InvalidTypeConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.error; 2 | 3 | import com.github.jankroken.commandline.annotations.LongSwitch; 4 | import com.github.jankroken.commandline.annotations.Option; 5 | import com.github.jankroken.commandline.annotations.SingleArgument; 6 | 7 | public class InvalidTypeConfiguration { 8 | 9 | @Option 10 | @LongSwitch("filename") 11 | @SingleArgument 12 | public void setFilename(@SuppressWarnings("unused") int filename) { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/error/MissingArgumentConsumptionConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.error; 2 | 3 | import com.github.jankroken.commandline.annotations.LongSwitch; 4 | import com.github.jankroken.commandline.annotations.Option; 5 | 6 | public class MissingArgumentConsumptionConfiguration { 7 | 8 | @Option 9 | @LongSwitch("filename") 10 | public void setFilename(@SuppressWarnings("unused") String filename) { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/error/MissingSwitchesConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.error; 2 | 3 | import com.github.jankroken.commandline.annotations.Option; 4 | import com.github.jankroken.commandline.annotations.SingleArgument; 5 | 6 | public class MissingSwitchesConfiguration { 7 | 8 | @Option 9 | @SingleArgument 10 | public void setFilename(@SuppressWarnings("unused") String filename) { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/error/MultipleConsumptionsConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.error; 2 | 3 | import com.github.jankroken.commandline.annotations.LongSwitch; 4 | import com.github.jankroken.commandline.annotations.Option; 5 | import com.github.jankroken.commandline.annotations.SingleArgument; 6 | import com.github.jankroken.commandline.annotations.Toggle; 7 | 8 | public class MultipleConsumptionsConfiguration { 9 | 10 | @Option 11 | @LongSwitch("filename") 12 | @SingleArgument 13 | @Toggle(true) 14 | public void setFilename(@SuppressWarnings("unused") String filename) { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/error/RequiredConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.error; 2 | 3 | import com.github.jankroken.commandline.annotations.LongSwitch; 4 | import com.github.jankroken.commandline.annotations.Option; 5 | import com.github.jankroken.commandline.annotations.Required; 6 | import com.github.jankroken.commandline.annotations.SingleArgument; 7 | 8 | public class RequiredConfiguration { 9 | 10 | @Option 11 | @LongSwitch("filename") 12 | @SingleArgument 13 | @Required 14 | public void setFilename(@SuppressWarnings("unused") String filename) { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/AlbumConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import com.github.jankroken.commandline.annotations.LongSwitch; 4 | import com.github.jankroken.commandline.annotations.Option; 5 | import com.github.jankroken.commandline.annotations.SingleArgument; 6 | import com.github.jankroken.commandline.annotations.Toggle; 7 | 8 | public class AlbumConfiguration { 9 | 10 | private String artist; 11 | private String name; 12 | private String year; 13 | private boolean available; 14 | 15 | @Option 16 | @LongSwitch("artist") 17 | @SingleArgument 18 | public void setArtist(String artist) { 19 | this.artist = artist; 20 | } 21 | 22 | @Option 23 | @LongSwitch("name") 24 | @SingleArgument 25 | public void setName(String name) { 26 | this.name = name; 27 | } 28 | 29 | @Option 30 | @LongSwitch("year") 31 | @SingleArgument 32 | public void setYear(String year) { 33 | this.year = year; 34 | } 35 | 36 | @Option 37 | @LongSwitch("available") 38 | @Toggle(true) 39 | public void setAvailable(boolean available) { 40 | this.available = available; 41 | } 42 | 43 | public String getArtist() { 44 | return artist; 45 | } 46 | 47 | public String getName() { 48 | return name; 49 | } 50 | 51 | public String getYear() { 52 | return year; 53 | } 54 | 55 | public boolean isAvailable() { 56 | return available; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/BasicParserTest.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.github.jankroken.commandline.CommandLineParser.parse; 6 | import static com.github.jankroken.commandline.OptionStyle.SIMPLE; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | 10 | public class BasicParserTest { 11 | 12 | @Test 13 | public void testSimpleConfiguration() throws Exception { 14 | final var args = new String[]{"-f", "hello.txt", "-v"}; 15 | final var config = parse(SimpleConfiguration.class, args, SIMPLE); 16 | assertThat(config.getFilename()).isEqualTo("hello.txt"); 17 | assertThat(config.getVerbose()).isTrue(); 18 | } 19 | 20 | @Test 21 | public void testMultipleArgsConfiguration() throws Exception { 22 | final var args = new String[]{"-files", "hello.txt", "world.txt", "bye.txt", "-logfile", "hello.log"}; 23 | final var config = parse(MultipleArgsConfiguration.class, args, SIMPLE); 24 | assertThat(config.getFiles()).containsExactly("hello.txt", "world.txt", "bye.txt"); 25 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 26 | } 27 | 28 | @Test 29 | public void testDelimiterConfiguration() throws Exception { 30 | final var args = new String[]{"-exec", "ls", "-l", "*.txt", ";", "-logfile", "hello.log"}; 31 | final var config = parse(DelimiterConfiguration.class, args, SIMPLE); 32 | assertThat(config.getCommand()).containsExactly("ls", "-l", "*.txt"); 33 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 34 | } 35 | 36 | @Test 37 | public void testMultipleConfiguration() throws Exception { 38 | final var args = new String[]{"-verbose", "-file", "hello.txt", "-file", "world.txt"}; 39 | final var config = parse(MultipleConfiguration.class, args, SIMPLE); 40 | assertThat(config.getFiles()).containsExactly("hello.txt", "world.txt"); 41 | assertThat(config.getVerbose()).isTrue(); 42 | } 43 | 44 | @Test 45 | public void testSubConfiguration() throws Exception { 46 | final var args = new String[]{"-verbose", "-album", "-name", "Caustic Grip", "-artist", "Front Line Assembly", "-year", "1990", "-available", "-logfile", "hello.log"}; 47 | final var config = parse(SimpleSuperConfiguration.class, args, SIMPLE); 48 | final var album = config.getAlbum(); 49 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 50 | assertThat(config.getVerbose()).isTrue(); 51 | assertThat(album.getName()).isEqualTo("Caustic Grip"); 52 | assertThat(album.getArtist()).isEqualTo("Front Line Assembly"); 53 | assertThat(album.getYear()).isEqualTo("1990"); 54 | assertThat(album.isAvailable()).isTrue(); 55 | } 56 | 57 | @Test 58 | public void testMultipleSubConfiguration() throws Exception { 59 | final var args = new String[]{"-verbose", 60 | "-album", "-name", "Caustic Grip", "-artist", "Front Line Assembly", "-year", "1990", "-available", 61 | "-album", "-name", "Scintilla", "-artist", "Stendeck", "-year", "2011", "-available", 62 | "-logfile", "hello.log"}; 63 | final var config = parse(MultipleSubconfigsConfiguration.class, args, SIMPLE); 64 | final var verbose = config.getVerbose(); 65 | final var albums = config.getAlbums(); 66 | assertThat(albums.size()).isEqualTo(2); 67 | final var causticGrip = albums.get(0); 68 | final var scintilla = albums.get(1); 69 | assertThat(causticGrip.getName()).isEqualTo("Caustic Grip"); 70 | assertThat(causticGrip.getArtist()).isEqualTo("Front Line Assembly"); 71 | assertThat(causticGrip.getYear()).isEqualTo("1990"); 72 | assertThat(causticGrip.isAvailable()).isTrue(); 73 | assertThat(scintilla.getName()).isEqualTo("Scintilla"); 74 | assertThat(scintilla.getArtist()).isEqualTo("Stendeck"); 75 | assertThat(scintilla.getYear()).isEqualTo("2011"); 76 | assertThat(scintilla.isAvailable()).isTrue(); 77 | 78 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 79 | assertThat(verbose).isTrue(); 80 | } 81 | 82 | @Test 83 | public void testLooseArguments() throws Exception { 84 | final var args = new String[]{"-verbose", "zombies", "ate", "my", "-logfile", "hello.log", "raptors"}; 85 | final var config = parse(LooseArgsConfiguration.class, args, SIMPLE); 86 | assertThat(config.getVerbose()).isTrue(); 87 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 88 | assertThat(config.getArgs()).containsExactly("zombies", "ate", "my", "raptors"); 89 | } 90 | 91 | @Test 92 | public void testArgumentEscape() throws Exception { 93 | final var args = new String[]{"-verbose", "-logfile", "hello.log", "--", "-zombies", "-ate", "-my", "--", "-raptors"}; 94 | final var config = parse(LooseArgsConfiguration.class, args, SIMPLE); 95 | assertThat(config.getVerbose()).isTrue(); 96 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 97 | assertThat(config.getArgs()).containsExactly("-zombies", "-ate", "-my", "--", "-raptors"); 98 | } 99 | 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/DelimiterConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import com.github.jankroken.commandline.annotations.*; 4 | 5 | import java.util.List; 6 | 7 | 8 | public class DelimiterConfiguration { 9 | 10 | private List command; 11 | private String logfile; 12 | 13 | public List getCommand() { 14 | return command; 15 | } 16 | 17 | @Option 18 | @LongSwitch("exec") 19 | @ShortSwitch("e") 20 | @ArgumentsUntilDelimiter(";") 21 | public void setFilename(List command) { 22 | this.command = command; 23 | } 24 | 25 | public String getLogfile() { 26 | return logfile; 27 | } 28 | 29 | @Option 30 | @LongSwitch("logfile") 31 | @ShortSwitch("l") 32 | @SingleArgument 33 | public void setLogfile(String logfile) { 34 | this.logfile = logfile; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/LongOrCompactParserTest.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.github.jankroken.commandline.CommandLineParser.parse; 6 | import static com.github.jankroken.commandline.OptionStyle.LONG_OR_COMPACT; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | public class LongOrCompactParserTest { 10 | 11 | @Test 12 | public void testSimpleConfiguration() throws Exception { 13 | final var args = new String[]{"-vf", "hello.txt"}; 14 | final var config = parse(SimpleConfiguration.class, args, LONG_OR_COMPACT); 15 | assertThat(config.getFilename()).isEqualTo("hello.txt"); 16 | assertThat(config.getVerbose()).isTrue(); 17 | } 18 | 19 | @Test 20 | public void testMultipleArgsConfiguration() throws Exception { 21 | final var args = new String[]{"--files", "hello.txt", "world.txt", "bye.txt", "--logfile", "hello.log"}; 22 | final var config = parse(MultipleArgsConfiguration.class, args, LONG_OR_COMPACT); 23 | assertThat(config.getFiles()).containsExactly("hello.txt", "world.txt", "bye.txt"); 24 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 25 | } 26 | 27 | @Test 28 | public void testDelimiterConfiguration() throws Exception { 29 | final var args = new String[]{"--exec", "ls", "-l", "*.txt", ";", "--logfile", "hello.log"}; 30 | final var config = parse(DelimiterConfiguration.class, args, LONG_OR_COMPACT); 31 | assertThat(config.getCommand()).containsExactly("ls", "-l", "*.txt"); 32 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 33 | } 34 | 35 | @Test 36 | public void testMultipleConfiguration() throws Exception { 37 | final var args = new String[]{"-v", "-f", "hello.txt", "-f", "world.txt"}; 38 | final var config = parse(MultipleConfiguration.class, args, LONG_OR_COMPACT); 39 | assertThat(config.getFiles()).containsExactly("hello.txt", "world.txt"); 40 | assertThat(config.getVerbose()).isTrue(); 41 | } 42 | 43 | @Test 44 | public void testSubConfiguration() throws Exception { 45 | final var args = new String[]{"--verbose", "--album", "--name", "Caustic Grip", "--artist", "Front Line Assembly", "--year", "1990", "--available", "--logfile", "hello.log"}; 46 | final var config = parse(SimpleSuperConfiguration.class, args, LONG_OR_COMPACT); 47 | final var album = config.getAlbum(); 48 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 49 | assertThat(config.getVerbose()).isTrue(); 50 | assertThat(album.getName()).isEqualTo("Caustic Grip"); 51 | assertThat(album.getArtist()).isEqualTo("Front Line Assembly"); 52 | assertThat(album.getYear()).isEqualTo("1990"); 53 | assertThat(album.isAvailable()).isTrue(); 54 | } 55 | 56 | @Test 57 | public void testMultipleSubConfiguration() throws Exception { 58 | final var args = new String[]{"--verbose", 59 | "--album", "--name", "Caustic Grip", "--artist", "Front Line Assembly", "--year", "1990", "--available", 60 | "--album", "--name", "Scintilla", "--artist", "Stendeck", "--year", "2011", "--available", 61 | "--logfile", "hello.log"}; 62 | final var config = parse(MultipleSubconfigsConfiguration.class, args, LONG_OR_COMPACT); 63 | final var verbose = config.getVerbose(); 64 | final var albums = config.getAlbums(); 65 | assertThat(albums.size()).isEqualTo(2); 66 | final var causticGrip = albums.get(0); 67 | final var scintilla = albums.get(1); 68 | assertThat(causticGrip.getName()).isEqualTo("Caustic Grip"); 69 | assertThat(causticGrip.getArtist()).isEqualTo("Front Line Assembly"); 70 | assertThat(causticGrip.getYear()).isEqualTo("1990"); 71 | assertThat(causticGrip.isAvailable()).isTrue(); 72 | assertThat(scintilla.getName()).isEqualTo("Scintilla"); 73 | assertThat(scintilla.getArtist()).isEqualTo("Stendeck"); 74 | assertThat(scintilla.getYear()).isEqualTo("2011"); 75 | assertThat(scintilla.isAvailable()).isTrue(); 76 | 77 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 78 | assertThat(verbose).isTrue(); 79 | } 80 | 81 | @Test 82 | public void testLooseArguments() throws Exception { 83 | final var args = new String[]{"--verbose", "zombies", "ate", "my", "--logfile", "hello.log", "raptors"}; 84 | final var config = parse(LooseArgsConfiguration.class, args, LONG_OR_COMPACT); 85 | assertThat(config.getVerbose()).isTrue(); 86 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 87 | assertThat(config.getArgs()).containsExactly("zombies", "ate", "my", "raptors"); 88 | } 89 | 90 | @Test 91 | public void testArgumentEscape() throws Exception { 92 | final var args = new String[]{"--verbose", "--logfile", "hello.log", "--", "-zombies", "-ate", "-my", "--", "-raptors"}; 93 | final var config = parse(LooseArgsConfiguration.class, args, LONG_OR_COMPACT); 94 | assertThat(config.getVerbose()).isTrue(); 95 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 96 | assertThat(config.getArgs()).contains("-zombies", "-ate", "-my", "--", "-raptors"); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/LooseArgsConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import com.github.jankroken.commandline.annotations.*; 4 | 5 | import java.util.List; 6 | 7 | 8 | public class LooseArgsConfiguration { 9 | 10 | private String logfile; 11 | private boolean verbose; 12 | private List args; 13 | 14 | 15 | @Option 16 | @LongSwitch("logfile") 17 | @ShortSwitch("l") 18 | @SingleArgument 19 | public void setFilename(String logfile) { 20 | this.logfile = logfile; 21 | } 22 | 23 | @Option 24 | @LongSwitch("verbose") 25 | @ShortSwitch("v") 26 | @Toggle(true) 27 | public void setVerbose(boolean verbose) { 28 | this.verbose = verbose; 29 | } 30 | 31 | @Option 32 | @LooseArguments 33 | @Multiple 34 | public void setArgs(List args) { 35 | this.args = args; 36 | } 37 | 38 | public String getLogfile() { 39 | return logfile; 40 | } 41 | 42 | public boolean getVerbose() { 43 | return verbose; 44 | } 45 | 46 | public List getArgs() { 47 | return args; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/LooseArgumentsConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import com.github.jankroken.commandline.annotations.*; 4 | 5 | public class LooseArgumentsConfiguration { 6 | 7 | private String filename; 8 | private boolean verbose; 9 | 10 | public String getFilename() { 11 | return filename; 12 | } 13 | 14 | @Option 15 | @LongSwitch("filename") 16 | @ShortSwitch("f") 17 | @SingleArgument 18 | public void setFilename(String filename) { 19 | this.filename = filename; 20 | } 21 | 22 | public boolean getVerbose() { 23 | return verbose; 24 | } 25 | 26 | @Option 27 | @LongSwitch("verbose") 28 | @ShortSwitch("v") 29 | @Toggle(true) 30 | public void setVerbose(boolean verbose) { 31 | this.verbose = verbose; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/MultipleArgsConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import com.github.jankroken.commandline.annotations.*; 4 | 5 | import java.util.List; 6 | 7 | 8 | public class MultipleArgsConfiguration { 9 | 10 | private List files; 11 | private String logfile; 12 | 13 | public List getFiles() { 14 | return files; 15 | } 16 | 17 | @Option 18 | @LongSwitch("files") 19 | @ShortSwitch("f") 20 | @AllAvailableArguments 21 | public void setFiles(List files) { 22 | this.files = files; 23 | } 24 | 25 | public String getLogfile() { 26 | return logfile; 27 | } 28 | 29 | @Option 30 | @LongSwitch("logfile") 31 | @ShortSwitch("l") 32 | @SingleArgument 33 | public void setLogfile(String logfile) { 34 | this.logfile = logfile; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/MultipleConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import com.github.jankroken.commandline.annotations.*; 4 | 5 | import java.util.List; 6 | 7 | 8 | public class MultipleConfiguration { 9 | 10 | private List files; 11 | private boolean verbose; 12 | 13 | 14 | @Option 15 | @LongSwitch("file") 16 | @ShortSwitch("f") 17 | @SingleArgument 18 | @Multiple 19 | public void setFiles(List files) { 20 | this.files = files; 21 | } 22 | 23 | @Option 24 | @LongSwitch("verbose") 25 | @ShortSwitch("v") 26 | @Toggle(true) 27 | public void setVerbose(boolean verbose) { 28 | this.verbose = verbose; 29 | } 30 | 31 | public List getFiles() { 32 | return files; 33 | } 34 | 35 | public boolean getVerbose() { 36 | return verbose; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/MultipleSubconfigsConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import com.github.jankroken.commandline.annotations.*; 4 | 5 | import java.util.List; 6 | 7 | 8 | public class MultipleSubconfigsConfiguration { 9 | 10 | private String logfile; 11 | private boolean verbose; 12 | private List albums; 13 | 14 | 15 | @Option 16 | @LongSwitch("logfile") 17 | @ShortSwitch("l") 18 | @SingleArgument 19 | public void setFilename(String logfile) { 20 | this.logfile = logfile; 21 | } 22 | 23 | @Option 24 | @LongSwitch("verbose") 25 | @ShortSwitch("v") 26 | @Toggle(true) 27 | public void setVerbose(boolean verbose) { 28 | this.verbose = verbose; 29 | } 30 | 31 | @Option 32 | @LongSwitch("album") 33 | @ShortSwitch("a") 34 | @Multiple 35 | @SubConfiguration(AlbumConfiguration.class) 36 | public void setAlbum(List albums) { 37 | this.albums = albums; 38 | } 39 | 40 | public boolean getVerbose() { 41 | return verbose; 42 | } 43 | 44 | public String getLogfile() { 45 | return logfile; 46 | } 47 | 48 | public List getAlbums() { 49 | return albums; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/SimpleConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import com.github.jankroken.commandline.annotations.*; 4 | 5 | public class SimpleConfiguration { 6 | 7 | private String filename; 8 | private boolean verbose; 9 | 10 | public String getFilename() { 11 | return filename; 12 | } 13 | 14 | @Option 15 | @LongSwitch("filename") 16 | @ShortSwitch("f") 17 | @SingleArgument 18 | public void setFilename(String filename) { 19 | this.filename = filename; 20 | } 21 | 22 | public boolean getVerbose() { 23 | return verbose; 24 | } 25 | 26 | @Option 27 | @LongSwitch("verbose") 28 | @ShortSwitch("v") 29 | @Toggle(true) 30 | public void setVerbose(boolean verbose) { 31 | this.verbose = verbose; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/SimpleParserTest.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.github.jankroken.commandline.CommandLineParser.parse; 6 | import static com.github.jankroken.commandline.OptionStyle.SIMPLE; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | 10 | public class SimpleParserTest { 11 | 12 | @Test 13 | public void testSimpleConfiguration() throws Exception { 14 | final var args = new String[]{"-f", "hello.txt", "-v"}; 15 | final var config = parse(SimpleConfiguration.class, args, SIMPLE); 16 | assertThat(config.getFilename()).isEqualTo("hello.txt"); 17 | assertThat(config.getVerbose()).isTrue(); 18 | } 19 | 20 | @Test 21 | public void testMultipleArgsConfiguration() throws Exception { 22 | final var args = new String[]{"-files", "hello.txt", "world.txt", "bye.txt", "-logfile", "hello.log"}; 23 | final var config = parse(MultipleArgsConfiguration.class, args, SIMPLE); 24 | assertThat(config.getFiles()).containsExactly("hello.txt", "world.txt", "bye.txt"); 25 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 26 | } 27 | 28 | @Test 29 | public void testDelimiterConfiguration() throws Exception { 30 | final var args = new String[]{"-exec", "ls", "-l", "*.txt", ";", "-logfile", "hello.log"}; 31 | final var config = parse(DelimiterConfiguration.class, args, SIMPLE); 32 | assertThat(config.getCommand()).containsExactly("ls", "-l", "*.txt"); 33 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 34 | } 35 | 36 | @Test 37 | public void testMultipleConfiguration() throws Exception { 38 | final var args = new String[]{"-verbose", "-file", "hello.txt", "-file", "world.txt"}; 39 | final var config = parse(MultipleConfiguration.class, args, SIMPLE); 40 | assertThat(config.getFiles()).containsExactly("hello.txt", "world.txt"); 41 | assertThat(config.getVerbose()).isTrue(); 42 | } 43 | 44 | @Test 45 | public void testSubConfiguration() throws Exception { 46 | final var args = new String[]{"-verbose", "-album", "-name", "Caustic Grip", "-artist", "Front Line Assembly", "-year", "1990", "-available", "-logfile", "hello.log"}; 47 | final var config = parse(SimpleSuperConfiguration.class, args, SIMPLE); 48 | final var album = config.getAlbum(); 49 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 50 | assertThat(config.getVerbose()).isTrue(); 51 | assertThat(album.getName()).isEqualTo("Caustic Grip"); 52 | assertThat(album.getArtist()).isEqualTo("Front Line Assembly"); 53 | assertThat(album.getYear()).isEqualTo("1990"); 54 | assertThat(album.isAvailable()).isTrue(); 55 | } 56 | 57 | @Test 58 | public void testMultipleSubConfiguration() throws Exception { 59 | final var args = new String[]{"-verbose", 60 | "-album", "-name", "Caustic Grip", "-artist", "Front Line Assembly", "-year", "1990", "-available", 61 | "-album", "-name", "Scintilla", "-artist", "Stendeck", "-year", "2011", "-available", 62 | "-logfile", "hello.log"}; 63 | final var config = parse(MultipleSubconfigsConfiguration.class, args, SIMPLE); 64 | final var verbose = config.getVerbose(); 65 | final var albums = config.getAlbums(); 66 | assertThat(albums.size()).isEqualTo(2); 67 | final var causticGrip = albums.get(0); 68 | final var scintilla = albums.get(1); 69 | assertThat(causticGrip.getName()).isEqualTo("Caustic Grip"); 70 | assertThat(causticGrip.getArtist()).isEqualTo("Front Line Assembly"); 71 | assertThat(causticGrip.getYear()).isEqualTo("1990"); 72 | assertThat(causticGrip.isAvailable()).isTrue(); 73 | assertThat(scintilla.getName()).isEqualTo("Scintilla"); 74 | assertThat(scintilla.getArtist()).isEqualTo("Stendeck"); 75 | assertThat(scintilla.getYear()).isEqualTo("2011"); 76 | assertThat(scintilla.isAvailable()).isTrue(); 77 | 78 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 79 | assertThat(verbose).isTrue(); 80 | } 81 | 82 | @Test 83 | public void testLooseArguments() throws Exception { 84 | final var args = new String[]{"-verbose", "zombies", "ate", "my", "-logfile", "hello.log", "raptors"}; 85 | final var config = parse(LooseArgsConfiguration.class, args, SIMPLE); 86 | assertThat(config.getVerbose()).isTrue(); 87 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 88 | assertThat(config.getArgs()).containsExactly("zombies", "ate", "my", "raptors"); 89 | } 90 | 91 | @Test 92 | public void testArgumentEscape() throws Exception { 93 | final var args = new String[]{"-verbose", "-logfile", "hello.log", "--", "-zombies", "-ate", "-my", "--", "-raptors"}; 94 | final var config = parse(LooseArgsConfiguration.class, args, SIMPLE); 95 | assertThat(config.getVerbose()).isTrue(); 96 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 97 | assertThat(config.getArgs()).containsExactly("-zombies", "-ate", "-my", "--", "-raptors"); 98 | } 99 | 100 | @Test 101 | public void testEscapePrecedence() throws Exception { 102 | final var args = new String[]{"-exec", "ls", "--", "*.txt", ";", "-logfile", "hello.log"}; 103 | final var config = parse(DelimiterConfiguration.class, args, SIMPLE); 104 | assertThat(config.getCommand()).containsExactly("ls", "--", "*.txt"); 105 | assertThat(config.getLogfile()).isEqualTo("hello.log"); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/happy/SimpleSuperConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.happy; 2 | 3 | import com.github.jankroken.commandline.annotations.*; 4 | 5 | public class SimpleSuperConfiguration { 6 | 7 | private String logfile; 8 | private boolean verbose; 9 | private AlbumConfiguration album; 10 | 11 | 12 | @Option 13 | @LongSwitch("logfile") 14 | @ShortSwitch("l") 15 | @SingleArgument 16 | public void setFilename(String logfile) { 17 | this.logfile = logfile; 18 | } 19 | 20 | @Option 21 | @LongSwitch("verbose") 22 | @ShortSwitch("v") 23 | @Toggle(true) 24 | public void setVerbose(boolean verbose) { 25 | this.verbose = verbose; 26 | } 27 | 28 | @Option 29 | @LongSwitch("album") 30 | @ShortSwitch("a") 31 | @SubConfiguration(AlbumConfiguration.class) 32 | public void setAlbum(AlbumConfiguration album) { 33 | this.album = album; 34 | } 35 | 36 | public boolean getVerbose() { 37 | return verbose; 38 | } 39 | 40 | public String getLogfile() { 41 | return logfile; 42 | } 43 | 44 | public AlbumConfiguration getAlbum() { 45 | return album; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/util/ArrayIteratorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.NoSuchElementException; 6 | 7 | import static com.github.jankroken.commandline.util.Constants.EMPTY_STRING_ARRAY; 8 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | public class ArrayIteratorTest { 12 | 13 | @Test 14 | public void testEmpty() { 15 | var ai = new ArrayIterator<>(EMPTY_STRING_ARRAY); 16 | assertFalse(ai.hasNext()); 17 | } 18 | 19 | @Test 20 | public void testOneElement() { 21 | var oneElement = new String[]{"hello"}; 22 | var ai = new ArrayIterator<>(oneElement); 23 | assertTrue(ai.hasNext()); 24 | var hello = ai.next(); 25 | assertEquals(hello, "hello"); 26 | assertFalse(ai.hasNext()); 27 | } 28 | 29 | @Test 30 | public void testTwoElements() { 31 | var elements = new String[]{"hello", "world"}; 32 | var ai = new ArrayIterator<>(elements); 33 | assertTrue(ai.hasNext()); 34 | assertEquals(ai.next(), "hello"); 35 | assertTrue(ai.hasNext()); 36 | assertEquals(ai.next(), "world"); 37 | assertFalse(ai.hasNext()); 38 | } 39 | 40 | @Test 41 | public void testTwoElementsNoReadAhead() { 42 | var elements = new String[]{"hello", "world"}; 43 | var ai = new ArrayIterator<>(elements); 44 | assertEquals(ai.next(), "hello"); 45 | assertEquals(ai.next(), "world"); 46 | assertFalse(ai.hasNext()); 47 | } 48 | 49 | @Test 50 | public void testTwoElementsPeek() { 51 | var elements = new String[]{"hello", "world"}; 52 | var ai = new ArrayIterator<>(elements); 53 | assertEquals(ai.peek(), "hello"); 54 | assertEquals(ai.next(), "hello"); 55 | assertEquals(ai.peek(), "world"); 56 | assertEquals(ai.next(), "world"); 57 | assertFalse(ai.hasNext()); 58 | } 59 | 60 | 61 | @Test 62 | public void testReadPastEmpty() { 63 | var ai = new ArrayIterator<>(EMPTY_STRING_ARRAY); 64 | assertThatThrownBy(ai::next).isInstanceOf(NoSuchElementException.class); 65 | } 66 | 67 | @Test 68 | public void testPeekPastEmpty() { 69 | var ai = new ArrayIterator<>(EMPTY_STRING_ARRAY); 70 | assertThatThrownBy(ai::peek).isInstanceOf(NoSuchElementException.class); 71 | } 72 | 73 | @Test 74 | public void testReadPastTwo() { 75 | var elements = new String[]{"hello", "world"}; 76 | var ai = new ArrayIterator<>(elements); 77 | assertEquals(ai.next(), "hello"); 78 | assertEquals(ai.next(), "world"); 79 | assertThatThrownBy(ai::next).isInstanceOf(NoSuchElementException.class); 80 | } 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/util/MethodsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.util; 2 | 3 | import com.github.jankroken.commandline.annotations.Option; 4 | import com.github.jankroken.commandline.happy.SimpleConfiguration; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | public class MethodsTest { 10 | 11 | @Test 12 | public void testGetMethodsByAnnotation() { 13 | var methods = new Methods(SimpleConfiguration.class).byAnnotation(Option.class); 14 | assertThat(methods.size()).isEqualTo(2); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/jankroken/commandline/util/PeekIteratorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.jankroken.commandline.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.NoSuchElementException; 6 | 7 | import static com.github.jankroken.commandline.util.Constants.EMPTY_STRING_ARRAY; 8 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | public class PeekIteratorTest { 12 | 13 | private static PeekIterator createIterator(String[] args) { 14 | var ai = new ArrayIterator<>(args); 15 | return new PeekIterator<>(ai); 16 | } 17 | 18 | @Test 19 | public void testEmpty() { 20 | var ai = createIterator(EMPTY_STRING_ARRAY); 21 | assertFalse(ai.hasNext()); 22 | } 23 | 24 | @Test 25 | public void testOneElement() { 26 | var oneElement = new String[]{"hello"}; 27 | var ai = createIterator(oneElement); 28 | assertTrue(ai.hasNext()); 29 | var hello = ai.next(); 30 | assertEquals(hello, "hello"); 31 | assertFalse(ai.hasNext()); 32 | } 33 | 34 | @Test 35 | public void testTwoElements() { 36 | var elements = new String[]{"hello", "world"}; 37 | var ai = createIterator(elements); 38 | assertTrue(ai.hasNext()); 39 | assertEquals(ai.next(), "hello"); 40 | assertTrue(ai.hasNext()); 41 | assertEquals(ai.next(), "world"); 42 | assertFalse(ai.hasNext()); 43 | } 44 | 45 | @Test 46 | public void testTwoElementsNoReadAhead() { 47 | var elements = new String[]{"hello", "world"}; 48 | var ai = createIterator(elements); 49 | assertEquals(ai.next(), "hello"); 50 | assertEquals(ai.next(), "world"); 51 | assertFalse(ai.hasNext()); 52 | } 53 | 54 | @Test 55 | public void testTwoElementsPeek() { 56 | var elements = new String[]{"hello", "world"}; 57 | var ai = createIterator(elements); 58 | assertEquals(ai.peek(), "hello"); 59 | assertEquals(ai.next(), "hello"); 60 | assertEquals(ai.peek(), "world"); 61 | assertEquals(ai.next(), "world"); 62 | assertFalse(ai.hasNext()); 63 | } 64 | 65 | 66 | @Test 67 | public void testReadPastEmpty() { 68 | var ai = createIterator(EMPTY_STRING_ARRAY); 69 | assertThatThrownBy(ai::next).isInstanceOf(NoSuchElementException.class); 70 | } 71 | 72 | @Test 73 | public void testPeekPastEmpty() { 74 | var ai = createIterator(EMPTY_STRING_ARRAY); 75 | assertThatThrownBy(ai::peek).isInstanceOf(NoSuchElementException.class); 76 | } 77 | 78 | @Test 79 | public void testReadPastTwo() { 80 | var elements = new String[]{"hello", "world"}; 81 | var ai = createIterator(elements); 82 | assertEquals(ai.next(), "hello"); 83 | assertEquals(ai.next(), "world"); 84 | assertThatThrownBy(ai::next).isInstanceOf(NoSuchElementException.class); 85 | } 86 | 87 | } 88 | --------------------------------------------------------------------------------