├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── picocli-spring-boot-autoconfigure ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── kakawait │ │ │ └── spring │ │ │ └── boot │ │ │ └── picocli │ │ │ └── autoconfigure │ │ │ ├── ExitStatus.java │ │ │ ├── HelpAwarePicocliCommand.java │ │ │ ├── PicocliAutoConfiguration.java │ │ │ ├── PicocliCommand.java │ │ │ ├── PicocliCommandLineRunner.java │ │ │ ├── PicocliConfigurer.java │ │ │ └── PicocliConfigurerAdapter.java │ └── resources │ │ └── META-INF │ │ └── spring.factories │ └── test │ └── java │ └── com.kakawait.spring.boot.picocli.autoconfigure │ ├── PicocliAutoConfigurationTest.java │ └── PicocliCommandLineRunnerTest.java ├── picocli-spring-boot-sample ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── kakawait │ │ └── PicocliSpringBootSampleApplication.java │ └── resources │ ├── application.yml │ └── db │ └── migration │ ├── V1__init.sql │ └── V2__add.sql ├── picocli-spring-boot-starter ├── pom.xml └── src │ └── main │ └── resources │ └── META-INF │ └── spring.provides └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/java,maven,eclipse,intellij 3 | 4 | ### Eclipse ### 5 | 6 | .metadata 7 | bin/ 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .settings/ 15 | .loadpath 16 | .recommenders 17 | 18 | # External tool builders 19 | .externalToolBuilders/ 20 | 21 | # Locally stored "Eclipse launch configurations" 22 | *.launch 23 | 24 | # PyDev specific (Python IDE for Eclipse) 25 | *.pydevproject 26 | 27 | # CDT-specific (C/C++ Development Tooling) 28 | .cproject 29 | 30 | # Java annotation processor (APT) 31 | .factorypath 32 | 33 | # PDT-specific (PHP Development Tools) 34 | .buildpath 35 | 36 | # sbteclipse plugin 37 | .target 38 | 39 | # Tern plugin 40 | .tern-project 41 | 42 | # TeXlipse plugin 43 | .texlipse 44 | 45 | # STS (Spring Tool Suite) 46 | .springBeans 47 | 48 | # Code Recommenders 49 | .recommenders/ 50 | 51 | # Scala IDE specific (Scala & Java development for Eclipse) 52 | .cache-main 53 | .scala_dependencies 54 | .worksheet 55 | 56 | ### Eclipse Patch ### 57 | # Eclipse Core 58 | .project 59 | 60 | # JDT-specific (Eclipse Java Development Tools) 61 | .classpath 62 | 63 | ### Intellij ### 64 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 65 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 66 | 67 | # User-specific stuff: 68 | .idea/**/workspace.xml 69 | .idea/**/tasks.xml 70 | .idea/dictionaries 71 | 72 | # Sensitive or high-churn files: 73 | .idea/**/dataSources/ 74 | .idea/**/dataSources.ids 75 | .idea/**/dataSources.xml 76 | .idea/**/dataSources.local.xml 77 | .idea/**/sqlDataSources.xml 78 | .idea/**/dynamic.xml 79 | .idea/**/uiDesigner.xml 80 | 81 | # Gradle: 82 | .idea/**/gradle.xml 83 | .idea/**/libraries 84 | 85 | # CMake 86 | cmake-build-debug/ 87 | 88 | # Mongo Explorer plugin: 89 | .idea/**/mongoSettings.xml 90 | 91 | ## File-based project format: 92 | *.iws 93 | 94 | ## Plugin-specific files: 95 | 96 | # IntelliJ 97 | /out/ 98 | 99 | # mpeltonen/sbt-idea plugin 100 | .idea_modules/ 101 | 102 | # JIRA plugin 103 | atlassian-ide-plugin.xml 104 | 105 | # Cursive Clojure plugin 106 | .idea/replstate.xml 107 | 108 | # Crashlytics plugin (for Android Studio and IntelliJ) 109 | com_crashlytics_export_strings.xml 110 | crashlytics.properties 111 | crashlytics-build.properties 112 | fabric.properties 113 | 114 | ### Intellij Patch ### 115 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 116 | 117 | # *.iml 118 | # modules.xml 119 | # .idea/misc.xml 120 | # *.ipr 121 | 122 | # Sonarlint plugin 123 | .idea/sonarlint 124 | 125 | ### Java ### 126 | # Compiled class file 127 | *.class 128 | 129 | # Log file 130 | *.log 131 | 132 | # BlueJ files 133 | *.ctxt 134 | 135 | # Mobile Tools for Java (J2ME) 136 | .mtj.tmp/ 137 | 138 | # Package Files # 139 | *.jar 140 | *.war 141 | *.ear 142 | *.zip 143 | *.tar.gz 144 | *.rar 145 | 146 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 147 | hs_err_pid* 148 | 149 | ### Maven ### 150 | target/ 151 | pom.xml.tag 152 | pom.xml.releaseBackup 153 | pom.xml.versionsBackup 154 | pom.xml.next 155 | release.properties 156 | dependency-reduced-pom.xml 157 | buildNumber.properties 158 | .mvn/timing.properties 159 | 160 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 161 | !/.mvn/wrapper/maven-wrapper.jar 162 | 163 | # End of https://www.gitignore.io/api/java,maven,eclipse,intellij -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | install: true 4 | 5 | addons: 6 | sonarcloud: 7 | organization: "kakawait-github" 8 | branches: 9 | - master 10 | - develop 11 | - /^release\/.*$/ 12 | token: 13 | secure: "yghqOuzw0Hov/i82t2CF7MlS8ifAQI1St3Bx3ZQ2yCer2sx5gBSZuJ12sWBJPsfnECxh2XLCHklud1maR1NOJPcogYSzF0Mm+/ymowGDXzwBmeO6ulEIXHWNyG9QSdZCvZtyvYEdXqErntsN4MnGWiGm0526n0qAv2sQE77MDBVTupbXGkqmwYe3vcDXuoRLUWVat4gop5A1tkdlu5LWXqn5tzylCJzDZ7VXD2eR+Cf7n0k/KMYA2MSFDUMBZWZzzj9O1lwhSkZystqlPo7ZL8UPUL/CpqsObdSWfeZVJG0VxfAbJxlSoHNi+KbffjyStPmIRGjgDIr8aWUANcwxmW2G2VDn898ZhvD+C7n1BiDqbKgbJRrhM8aG4klW3odE0gMcLEO3mOuqzT7p8h4IeeZCIFdr9wwsInXNnAfwDISCDiPTacUmM/DKwVDSBZTNxvi+tS1mwwoqphn1xc6ePnTx4RF/pvxNjLbGBEzToVmAAX7ViiU4MS/RDGPbxA/b0qVsgZWF0v9pD4uSb0O++fNtTAPObAnGOB9RUs5FEBtzIxBtw51oV0eyS7CffMLF+dkcxLzo0hj7UCnUpzotee/ydVMIc/K83NJZGlxy02NgdDEi5pxGJOyJyxV0s5F2DINCl4kuliqgxxjlyVvEgQAJ8gObGQkhQdA8Ax5qoqM=" 14 | 15 | jdk: 16 | - oraclejdk8 17 | 18 | script: 19 | - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent package sonar:sonar 20 | 21 | cache: 22 | directories: 23 | - '$HOME/.m2/repository' 24 | - '$HOME/.sonar/cache' 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thibaud Lepretre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring boot Picocli starter 2 | 3 | [![Travis](https://img.shields.io/travis/kakawait/picocli-spring-boot-starter.svg)](https://travis-ci.org/kakawait/picocli-spring-boot-starter) 4 | [![SonarQube Coverage](https://img.shields.io/sonar/https/sonarcloud.io/com.kakawait%3Apicocli-spring-boot-starter-parent/coverage.svg)](https://sonarcloud.io/component_measures?id=com.kakawait%3Apicocli-spring-boot-starter-parent&metric=coverage) 5 | [![Maven Central](https://img.shields.io/maven-central/v/com.kakawait/picocli-spring-boot-starter.svg)](https://search.maven.org/#artifactdetails%7Ccom.kakawait%7Cpicocli-spring-boot-starter%7C0.2.0%7Cjar) 6 | [![license](https://img.shields.io/github/license/kakawait/picocli-spring-boot-starter.svg)](https://github.com/kakawait/picocli-spring-boot-starter/blob/master/LICENSE.md) 7 | [![Twitter Follow](https://img.shields.io/twitter/follow/thibaudlepretre.svg?style=social&label=%40thibaudlepretre)](https://twitter.com/intent/follow?screen_name=thibaudlepretre) 8 | 9 | > A Spring boot starter for [Picocli](http://picocli.info/) command line tools. That let you easily write CLI for Spring boot application! 10 | 11 | ## Features 12 | 13 | - Automatic integration with Spring [`CommandLineRunner`](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-command-line-runner) 14 | - Automatic `@Command` beans registration 15 | - Display usage on _help_ 16 | - Automatically run `@Command` if it implements `java.lang.Runnable` or `java.lang.Callable` 17 | - Flow control using `java.lang.Callable` and `ExitStatus` 18 | - Advance configuration through [PicocliConfigurerAdapter](https://github.com/kakawait/picocli-spring-boot-starter/blob/master/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliConfigurerAdapter.java) 19 | 20 | ## Setup 21 | 22 | Add the Spring boot starter to your project 23 | 24 | ```xml 25 | 26 | com.kakawait 27 | picocli-spring-boot-starter 28 | 0.2.0 29 | 30 | ``` 31 | 32 | ## Usage 33 | 34 | There is multiple ways to define a new picocli commands. 35 | 36 | You should start looking [sample application](https://github.com/kakawait/picocli-spring-boot-starter/blob/master/picocli-spring-boot-sample/src/main/java/com/kakawait/PicocliSpringBootSampleApplication.java) to get more advance sample. 37 | 38 | ### `@Command` beans 39 | 40 | First and simplest way, is to register a new bean with `@Command` annotation inside your _Spring_ context, example: 41 | 42 | ```java 43 | @Component 44 | @Command(name = "greeting") 45 | class GreetingCommand implements Runnable { 46 | 47 | @Parameters(paramLabel = "NAME", description = "name", arity = "0..1") 48 | String name; 49 | 50 | @Override 51 | public void run() { 52 | if (StringUtils.hasText(name)) { 53 | System.out.println("Hello " + name + "!"); 54 | } else { 55 | System.out.println("Hello world!"); 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | Your bean can implements `Runnable` or `Callable` (in order to control flow) or extends [`PicocliCommand`](https://github.com/kakawait/picocli-spring-boot-starter/blob/master/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommand.java) or [`HelpAwarePicocliCommand`](https://github.com/kakawait/picocli-spring-boot-starter/blob/master/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/HelpAwarePicocliCommand.java) (to magically have `-h/--help` option to display usage) if you need to execute something when command is called. 62 | 63 | In addition, for advance usage [`PicocliCommand`](https://github.com/kakawait/picocli-spring-boot-starter/blob/master/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommand.java) let you get access to 64 | 65 | ```java 66 | /** 67 | * Returns result of {@link CommandLine#parse(String...)}. 68 | * @return Picocli parsing result which results on collection of every command involve regarding your input. 69 | */ 70 | protected List getParsedCommands() { 71 | return parsedCommands; 72 | } 73 | 74 | /** 75 | * Returns the current {@link CommandLine}. 76 | * @return current {@link CommandLine} context 77 | */ 78 | protected CommandLine getContext() { 79 | return context; 80 | } 81 | 82 | /** 83 | * Returns the root {@link CommandLine}. 84 | * @return root {@link CommandLine} context that must contains (or equals) the {@link #getContext()}. 85 | */ 86 | protected CommandLine getRootContext() { 87 | return rootContext; 88 | } 89 | ``` 90 | 91 | That might be useful. 92 | 93 | #### Main command using beans 94 | 95 | Picocli is waiting for a _Main_ command, cf: `new CommandLine(mainCommand)`. To determine which `@Command` beans will be the _Main_ command, starter will apply the following logic: 96 | 97 | > _Main_ command will be the first `@Command` (if multiple found) bean with default `name` argument. 98 | 99 | For example 100 | 101 | ```java 102 | @Command 103 | class MainCommand {} 104 | ``` 105 | 106 | or 107 | 108 | ```java 109 | @Command(description = "main command") 110 | class MainCommand {} 111 | ``` 112 | 113 | But the following example will not be candidate for _Main_ command 114 | 115 | ```java 116 | @Command(name = "my_command") 117 | class MainCommand {} 118 | ``` 119 | 120 | #### Nested sub-commands using beans 121 | 122 | Picocli allows [_nested sub-commands_](http://picocli.info/#_nested_sub_subcommands), in order to describe a _nested sub-command_, starter is offering you nested classes scanning capability. 123 | 124 | That means, if you're defining **bean** structure like following: 125 | 126 | ```java 127 | @Component 128 | @Command(name = "flyway") 129 | class FlywayCommand extends HelpAwareContainerPicocliCommand { 130 | 131 | @Component 132 | @Command(name = "migrate") 133 | class MigrateCommand implements Runnable { 134 | 135 | private final Flyway flyway; 136 | 137 | public MigrateCommand(Flyway flyway) { 138 | this.flyway = flyway; 139 | } 140 | 141 | @Override 142 | public void run() { 143 | flyway.migrate(); 144 | } 145 | } 146 | 147 | @Component 148 | @Command(name = "repair") 149 | class RepairCommand implements Runnable { 150 | private final Flyway flyway; 151 | 152 | public RepairCommand(Flyway flyway) { 153 | this.flyway = flyway; 154 | } 155 | 156 | @Override 157 | public void run() { 158 | flyway.repair(); 159 | } 160 | } 161 | } 162 | ``` 163 | 164 | Will generate command line 165 | 166 | ``` 167 | Commands: 168 | flyway [-h, --help] 169 | migrate 170 | repair 171 | ``` 172 | 173 | Thus `java -jar .jar flyway migrate` will execute _Flyway_ migration. 174 | 175 | **ATTENTION** every classes must be a bean (`@Component`) with `@Command` annotation without forgetting to file `name` attribute. 176 | 177 | There is **no limitation** about nesting level. 178 | 179 | ### Additional configuration 180 | 181 | If you need to set additional configuration options simply register within _Spring_ application context instance of [`PicocliConfigurerAdapter`](https://github.com/kakawait/picocli-spring-boot-starter/blob/master/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliConfigurerAdapter.java) 182 | 183 | ```java 184 | @Configuration 185 | class CustomPicocliConfiguration extends PicocliConfigurerAdapter { 186 | @Override 187 | public void configure(CommandLine commandLine) { 188 | // Here you can configure Picocli commandLine 189 | // You can add additional sub-commands or register converters. 190 | } 191 | } 192 | ``` 193 | 194 | Otherwise you can define your own bean `CommandLine` but attention that will disable automatic `@Command` bean registration explained above. 195 | 196 | ## Exit status 197 | 198 | If you defined following command line: 199 | 200 | ```java 201 | @Component 202 | @Command 203 | class MainCommand extends HelpAwarePicocliCommand { 204 | @Option(names = {"-v", "--version"}, description = "display version info") 205 | boolean versionRequested; 206 | 207 | @Override 208 | public void run() { 209 | if (versionRequested) { 210 | System.out.println("0.1.0"); 211 | } 212 | } 213 | } 214 | 215 | @Component 216 | @Command(name = "greeting") 217 | static class GreetingCommand extends HelpAwarePicocliCommand { 218 | 219 | @Parameters(paramLabel = "NAME", description = "name", arity = "0..1") 220 | String name; 221 | 222 | @Override 223 | public void run() { 224 | if (StringUtils.hasText(name)) { 225 | System.out.println("Hello " + name + "!"); 226 | } else { 227 | System.out.println("Hello world!"); 228 | } 229 | } 230 | } 231 | ``` 232 | 233 | And you execute `java -jar .jar -v greeting Thibaud` the output will looks like: 234 | 235 | ``` 236 | 0.1.0 237 | Hello Thibaud! 238 | ``` 239 | 240 | While you wanted that `-v` will break execution and other involved commands not executed. To achieve that you must replace `run()` method with `ExitStatus call()`. 241 | 242 | ```java 243 | @Component 244 | @Command 245 | class MainCommand extends HelpAwarePicocliCommand { 246 | @Option(names = {"-v", "--version"}, description = "display version info") 247 | boolean versionRequested; 248 | 249 | @Override 250 | public ExitStatus call() { 251 | if (versionRequested) { 252 | System.out.println("0.1.0"); 253 | return ExitStatus.TERMINATION; 254 | } 255 | return ExitStatus.OK; 256 | } 257 | } 258 | ``` 259 | 260 | The main difference is `ExitStatus.TERMINATION` that will tell the starter to stop other executions. (`ExitStatus.OK` is default status). 261 | 262 | ## Help & usage 263 | 264 | Picocli [documentation](http://picocli.info/#_help_options) and principle about `help` argument is not exactly the same on this starter. 265 | 266 | Indeed Picocli only consider option with `help` argument like following: 267 | 268 | > if one of the command line arguments is a "help" option, picocli will stop parsing the remaining arguments and will not check for required options. 269 | 270 | While this starter in addition will force displaying _usage_ if `help` was requested. 271 | 272 | Thus following example from Picocli documentation: 273 | 274 | ```java 275 | @Option(names = {"-V", "--version"}, help = true, description = "display version info") 276 | boolean versionRequested; 277 | 278 | @Option(names = {"-h", "--help"}, help = true, description = "display this help message") 279 | boolean helpRequested; 280 | ``` 281 | 282 | If you run program with `-V` or `--version` that will display usage and stop execution. It may not what you need. Thus you have to think about `help` argument is to displaying usage and stop execution. 283 | 284 | Following example is much more starter compliant: 285 | 286 | ```java 287 | @Option(names = {"-V", "--version"}, help = false, description = "display version info") 288 | boolean versionRequested; 289 | 290 | @Option(names = {"-h", "--help"}, help = true, description = "display this help message") 291 | boolean helpRequested; 292 | ``` 293 | 294 | ## License 295 | 296 | MIT License 297 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.kakawait 7 | picocli-spring-boot-autoconfigure 8 | 0.2.0 9 | jar 10 | 11 | Picocli spring boot autoconfigure 12 | Spring boot autoconfigure for Picocli command line tools. Let you easily write CLI! 13 | https://github.com/kakawait/picocli-spring-boot-starter 14 | 15 | 16 | 17 | MIT License 18 | http://www.opensource.org/licenses/mit-license.php 19 | repo 20 | 21 | 22 | 23 | 24 | 25 | Thibaud Leprêtre 26 | thibaud.lepretre@gmail.com 27 | 28 | 29 | 30 | 31 | https://github.com/kakawait/picocli-spring-boot-starter 32 | scm:git:git@github.com:kakawait/picocli-spring-boot-starter.git 33 | scm:git:git@github.com:kakawait/picocli-spring-boot-starter.git 34 | 35 | 36 | 37 | UTF-8 38 | UTF-8 39 | 40 | 1.8 41 | 1.8 42 | 1.8 43 | 44 | 1.5.4.RELEASE 45 | 46 | 0.9.8 47 | 1.7.25 48 | 49 | 3.8.0 50 | 2.0.0.0 51 | 2.15.0 52 | 53 | 3.0.1 54 | 2.10.4 55 | 1.6 56 | 1.6.8 57 | 58 | 59 | 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-dependencies 64 | ${spring-boot.version} 65 | pom 66 | import 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-autoconfigure 75 | 76 | 77 | info.picocli 78 | picocli 79 | ${picocli.version} 80 | 81 | 82 | org.slf4j 83 | slf4j-api 84 | ${slf4j-api.version} 85 | 86 | 87 | 88 | org.springframework.boot 89 | spring-boot-configuration-processor 90 | true 91 | 92 | 93 | 94 | junit 95 | junit 96 | test 97 | 98 | 99 | 100 | org.assertj 101 | assertj-core 102 | ${assertj-core.version} 103 | test 104 | 105 | 106 | org.springframework.boot 107 | spring-boot-test 108 | ${spring-boot.version} 109 | test 110 | 111 | 112 | org.hamcrest 113 | java-hamcrest 114 | ${java-hamcrest.version} 115 | test 116 | 117 | 118 | org.mockito 119 | mockito-core 120 | ${mockito-core.version} 121 | test 122 | 123 | 124 | 125 | 126 | 127 | 128 | oss.sonatype.org 129 | Sonatype OSS Staging 130 | https://oss.sonatype.org/service/local/staging/deploy/maven2 131 | default 132 | 133 | 134 | oss.sonatype.org 135 | Sonatype OSS Snapshots 136 | https://oss.sonatype.org/content/repositories/snapshots 137 | default 138 | 139 | 140 | 141 | 142 | 143 | release 144 | 145 | gpg2 146 | 147 | 148 | 149 | 150 | org.apache.maven.plugins 151 | maven-source-plugin 152 | ${maven-source-plugin.version} 153 | 154 | 155 | attach-sources 156 | verify 157 | 158 | jar-no-fork 159 | 160 | 161 | 162 | 163 | 164 | org.apache.maven.plugins 165 | maven-javadoc-plugin 166 | ${maven-javadoc-plugin.version} 167 | 168 | 169 | attach-javadocs 170 | verify 171 | 172 | jar 173 | 174 | 175 | 176 | 177 | 178 | org.apache.maven.plugins 179 | maven-gpg-plugin 180 | ${maven-gpg-plugin.version} 181 | 182 | 183 | sign-artifacts 184 | verify 185 | 186 | sign 187 | 188 | 189 | 190 | 191 | 192 | org.sonatype.plugins 193 | nexus-staging-maven-plugin 194 | ${nexus-staging-maven-plugin.version} 195 | true 196 | 197 | oss.sonatype.org 198 | https://oss.sonatype.org/ 199 | true 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/ExitStatus.java: -------------------------------------------------------------------------------- 1 | package com.kakawait.spring.boot.picocli.autoconfigure; 2 | 3 | /** 4 | * @author Thibaud Leprêtre 5 | */ 6 | public enum ExitStatus { 7 | OK, 8 | TERMINATION 9 | } 10 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/HelpAwarePicocliCommand.java: -------------------------------------------------------------------------------- 1 | package com.kakawait.spring.boot.picocli.autoconfigure; 2 | 3 | import static picocli.CommandLine.Option; 4 | 5 | /** 6 | * @author Thibaud Leprêtre 7 | */ 8 | @SuppressWarnings("unused") 9 | public abstract class HelpAwarePicocliCommand extends PicocliCommand { 10 | @Option(names = {"-h", "--help"}, help = true, description = "Prints this help message and exits") 11 | private boolean helpRequested; 12 | } 13 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.kakawait.spring.boot.picocli.autoconfigure; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.aop.support.AopUtils; 6 | import org.springframework.boot.CommandLineRunner; 7 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 8 | import org.springframework.boot.autoconfigure.condition.ConditionMessage; 9 | import org.springframework.boot.autoconfigure.condition.ConditionOutcome; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 13 | import org.springframework.boot.autoconfigure.condition.SpringBootCondition; 14 | import org.springframework.context.ApplicationContext; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.ConditionContext; 17 | import org.springframework.context.annotation.Conditional; 18 | import org.springframework.context.annotation.Configuration; 19 | import org.springframework.context.annotation.ConfigurationCondition; 20 | import org.springframework.context.annotation.Import; 21 | import org.springframework.core.type.AnnotatedTypeMetadata; 22 | import org.springframework.util.ReflectionUtils; 23 | import picocli.CommandLine; 24 | 25 | import java.lang.reflect.Method; 26 | import java.util.ArrayList; 27 | import java.util.Collection; 28 | import java.util.HashMap; 29 | import java.util.LinkedHashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.Optional; 33 | 34 | import static picocli.CommandLine.Command; 35 | 36 | /** 37 | * @author Thibaud Leprêtre 38 | */ 39 | @Configuration 40 | @ConditionalOnClass(CommandLine.class) 41 | @Import(PicocliAutoConfiguration.CommandlineConfiguration.class) 42 | class PicocliAutoConfiguration { 43 | 44 | @Bean 45 | @ConditionalOnMissingBean(PicocliCommandLineRunner.class) 46 | @ConditionalOnBean(CommandLine.class) 47 | CommandLineRunner picocliCommandLineRunner(CommandLine cli) { 48 | return new PicocliCommandLineRunner(cli); 49 | } 50 | 51 | @ConditionalOnMissingBean(CommandLine.class) 52 | @Conditional(CommandCondition.class) 53 | static class CommandlineConfiguration { 54 | 55 | private final Logger logger = LoggerFactory.getLogger(CommandlineConfiguration.class); 56 | 57 | @Bean 58 | CommandLine picocliCommandLine(ApplicationContext applicationContext) { 59 | Collection commands = applicationContext.getBeansWithAnnotation(Command.class).values(); 60 | List mainCommands = getMainCommands(commands); 61 | Object mainCommand = mainCommands.isEmpty() ? new HelpAwarePicocliCommand() {} : mainCommands.get(0); 62 | if (mainCommands.size() > 1) { 63 | logger.warn("Multiple mains command founds [{}], selected first one {}", mainCommands, mainCommand); 64 | } 65 | commands.removeAll(mainCommands); 66 | 67 | CommandLine cli = new CommandLine(mainCommand); 68 | registerCommands(cli, commands); 69 | 70 | applicationContext.getBeansOfType(PicocliConfigurer.class).values().forEach(c -> c.configure(cli)); 71 | return cli; 72 | } 73 | 74 | private String getCommandName(Object command) { 75 | if (command == null) { 76 | return null; 77 | } 78 | return AopUtils.getTargetClass(command).getAnnotation(Command.class).name(); 79 | } 80 | 81 | private String getCommandName(Class commandClass) { 82 | if (commandClass == null) { 83 | return null; 84 | } 85 | return commandClass.getAnnotation(Command.class).name(); 86 | } 87 | 88 | private int getNestedLevel(Class clazz) { 89 | int level = 0; 90 | Class parent = clazz.getEnclosingClass(); 91 | while (parent != null && parent.isAnnotationPresent(Command.class)) { 92 | parent = parent.getEnclosingClass(); 93 | level += 1; 94 | } 95 | return level; 96 | } 97 | 98 | private Optional getParentClass(Class clazz) { 99 | Class parentClass = clazz.getEnclosingClass(); 100 | if (parentClass == null || !parentClass.isAnnotationPresent(Command.class)) { 101 | return Optional.empty(); 102 | } 103 | return Optional.of(parentClass); 104 | } 105 | 106 | private List getMainCommands(Collection candidates) { 107 | List mainCommands = new ArrayList<>(); 108 | for (Object candidate : candidates) { 109 | Class clazz = AopUtils.getTargetClass(candidate); 110 | Method method = ReflectionUtils.findMethod(Command.class, "name"); 111 | if (clazz.isAnnotationPresent(Command.class) 112 | && method != null 113 | && clazz.getAnnotation(Command.class).name().equals(method.getDefaultValue())) { 114 | mainCommands.add(candidate); 115 | } 116 | } 117 | return mainCommands; 118 | } 119 | 120 | private Map> findCommands(Collection commands) { 121 | Map> tree = new LinkedHashMap<>(); 122 | 123 | commands.stream() 124 | .sorted((o1, o2) -> { 125 | int l1 = getNestedLevel(AopUtils.getTargetClass(o1)); 126 | int l2 = getNestedLevel(AopUtils.getTargetClass(o2)); 127 | return Integer.compare(l1, l2); 128 | }) 129 | .forEach(o -> { 130 | Class clazz = AopUtils.getTargetClass(o); 131 | Optional parentClass = getParentClass(clazz); 132 | parentClass.ifPresent(c -> { 133 | List objects = tree.get(new Node(c, null, null)); 134 | if (objects != null) { 135 | objects.add(o); 136 | } 137 | }); 138 | tree.put(new Node(clazz, o, parentClass.orElse(null)), new ArrayList<>()); 139 | }); 140 | 141 | return tree; 142 | } 143 | 144 | private void registerCommands(CommandLine cli, Collection commands) { 145 | CommandLine current = cli; 146 | Map, CommandLine> parents = new HashMap<>(); 147 | for (Map.Entry> entry : findCommands(commands).entrySet()) { 148 | Node node = entry.getKey(); 149 | // Avoid parent "adopting" orphan node (I know is hard for orphan children but life is hard) 150 | if (node.getParent() != null && !node.getParent().equals(current.getCommand().getClass())) { 151 | logger.warn("Orphan command may be detected {}, skipped!", node.getObject()); 152 | continue; 153 | } 154 | List children = entry.getValue(); 155 | Object command = node.getObject(); 156 | String commandName = getCommandName(node.getClazz()); 157 | if (parents.containsKey(node.getParent())) { 158 | current = parents.get(node.getParent()); 159 | } else if (node.getParent() == null) { 160 | current = cli; 161 | } 162 | if (children.isEmpty()) { 163 | current.addSubcommand(commandName, command); 164 | } else { 165 | CommandLine sub = new CommandLine(command); 166 | current.addSubcommand(commandName, sub); 167 | for (Object child : children) { 168 | sub.addSubcommand(getCommandName(child), new CommandLine(child)); 169 | } 170 | current = sub; 171 | } 172 | parents.put(node.getClazz(), current); 173 | } 174 | } 175 | 176 | private static class Node { 177 | private final Class clazz; 178 | 179 | private final Object object; 180 | 181 | private final Class parent; 182 | 183 | Node(Class clazz, Object object, Class parent) { 184 | this.clazz = clazz; 185 | this.object = object; 186 | this.parent = parent; 187 | } 188 | 189 | Class getClazz() { 190 | return clazz; 191 | } 192 | 193 | Object getObject() { 194 | return object; 195 | } 196 | 197 | Class getParent() { 198 | return parent; 199 | } 200 | 201 | @Override 202 | public boolean equals(Object o) { 203 | if (this == o) return true; 204 | if (!(o instanceof Node)) return false; 205 | 206 | Node node = (Node) o; 207 | 208 | return clazz != null ? clazz.equals(node.clazz) : node.clazz == null; 209 | } 210 | 211 | @Override 212 | public int hashCode() { 213 | return clazz != null ? clazz.hashCode() : 0; 214 | } 215 | } 216 | } 217 | 218 | static class CommandCondition extends SpringBootCondition implements ConfigurationCondition { 219 | 220 | @Override 221 | public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { 222 | String[] commands = context.getBeanFactory().getBeanNamesForAnnotation(Command.class); 223 | ConditionMessage.Builder message = ConditionMessage.forCondition("@Command Condition"); 224 | if (commands.length == 0) { 225 | return ConditionOutcome.noMatch(message.didNotFind("@Command beans").atAll()); 226 | } else { 227 | return ConditionOutcome.match(message.found("@Command beans").items((Object[]) commands)); 228 | } 229 | } 230 | 231 | @Override 232 | public ConfigurationPhase getConfigurationPhase() { 233 | return ConfigurationPhase.REGISTER_BEAN; 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommand.java: -------------------------------------------------------------------------------- 1 | package com.kakawait.spring.boot.picocli.autoconfigure; 2 | 3 | import picocli.CommandLine; 4 | 5 | import java.util.List; 6 | import java.util.concurrent.Callable; 7 | 8 | /** 9 | * @author Thibaud Leprêtre 10 | */ 11 | public abstract class PicocliCommand implements Callable { 12 | 13 | private List parsedCommands; 14 | 15 | private CommandLine context; 16 | 17 | private CommandLine rootContext; 18 | 19 | @Override 20 | public ExitStatus call() throws Exception { 21 | run(); 22 | return ExitStatus.OK; 23 | } 24 | 25 | @SuppressWarnings("WeakerAccess") 26 | public void run() { 27 | } 28 | 29 | /** 30 | * Returns result of {@link CommandLine#parse(String...)}. 31 | * @return Picocli parsing result which results on collection of every command involve regarding your input. 32 | */ 33 | protected List getParsedCommands() { 34 | return parsedCommands; 35 | } 36 | 37 | /** 38 | * Returns the current {@link CommandLine}. 39 | * @return current {@link CommandLine} context 40 | */ 41 | protected CommandLine getContext() { 42 | return context; 43 | } 44 | 45 | /** 46 | * Returns the root {@link CommandLine}. 47 | * @return root {@link CommandLine} context that must contains (or equals) the {@link #getContext()}. 48 | */ 49 | protected CommandLine getRootContext() { 50 | return rootContext; 51 | } 52 | 53 | void setParsedCommands(List parsedCommands) { 54 | this.parsedCommands = parsedCommands; 55 | } 56 | 57 | void setContext(CommandLine context) { 58 | this.context = context; 59 | } 60 | 61 | void setRootContext(CommandLine rootContext) { 62 | this.rootContext = rootContext; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommandLineRunner.java: -------------------------------------------------------------------------------- 1 | package com.kakawait.spring.boot.picocli.autoconfigure; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.aop.support.AopUtils; 6 | import org.springframework.boot.CommandLineRunner; 7 | import org.springframework.util.ReflectionUtils; 8 | import picocli.CommandLine; 9 | import picocli.CommandLine.Option; 10 | 11 | import java.util.List; 12 | import java.util.Optional; 13 | import java.util.concurrent.Callable; 14 | import java.util.concurrent.atomic.AtomicBoolean; 15 | 16 | import static picocli.CommandLine.Help.Ansi; 17 | import static picocli.CommandLine.usage; 18 | 19 | /** 20 | * @author Thibaud Leprêtre 21 | */ 22 | public class PicocliCommandLineRunner implements CommandLineRunner { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(PicocliCommandLineRunner.class); 25 | 26 | private final CommandLine cli; 27 | 28 | PicocliCommandLineRunner(CommandLine cli) { 29 | this.cli = cli; 30 | } 31 | 32 | @Override 33 | public void run(String... args) throws Exception { 34 | List commands; 35 | try { 36 | commands = cli.parse(args); 37 | } catch (Exception ex) { 38 | System.err.println(ex.getMessage()); 39 | cli.usage(System.err, Ansi.AUTO); 40 | return; 41 | } 42 | if (isHelpRequested(cli.getCommand())) { 43 | cli.usage(System.out, Ansi.AUTO); 44 | return; 45 | } 46 | Optional helpCommand = commands 47 | .stream() 48 | .filter(this::isHelpRequested) 49 | .findFirst(); 50 | if (helpCommand.isPresent()) { 51 | usage(helpCommand.get(), System.out); 52 | return; 53 | } 54 | 55 | for (CommandLine commandLine : commands) { 56 | Object command = commandLine.getCommand(); 57 | Object result = null; 58 | 59 | if (command instanceof PicocliCommand) { 60 | PicocliCommand picocliCommand = (PicocliCommand) command; 61 | picocliCommand.setContext(commandLine); 62 | picocliCommand.setRootContext(cli); 63 | picocliCommand.setParsedCommands(commands); 64 | result = picocliCommand.call(); 65 | } else if (command instanceof Runnable) { 66 | ((Runnable) command).run(); 67 | } else if (command instanceof Callable) { 68 | result = ((Callable) command).call(); 69 | } else { 70 | logger.debug("Command {} is triggered but does not implement {} neither {}", 71 | command, Runnable.class, Callable.class); 72 | } 73 | 74 | if (result instanceof ExitStatus && result == ExitStatus.TERMINATION) { 75 | break; 76 | } 77 | } 78 | } 79 | 80 | public CommandLine getCommandLine() { 81 | return cli; 82 | } 83 | 84 | private boolean isHelpRequested(CommandLine commandLine) { 85 | Object command = commandLine.getCommand(); 86 | return isHelpRequested(command); 87 | } 88 | 89 | private boolean isHelpRequested(Object command) { 90 | AtomicBoolean result = new AtomicBoolean(false); 91 | ReflectionUtils.doWithFields(AopUtils.getTargetClass(command), 92 | f -> { 93 | f.setAccessible(true); 94 | if (f.getBoolean(command)) { 95 | result.set(true); 96 | } 97 | }, 98 | f -> f.isAnnotationPresent(Option.class) && f.getAnnotation(Option.class).help()); 99 | return result.get(); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.kakawait.spring.boot.picocli.autoconfigure; 2 | 3 | import picocli.CommandLine; 4 | 5 | /** 6 | * @author Thibaud Leprêtre 7 | */ 8 | public interface PicocliConfigurer { 9 | 10 | void configure(CommandLine commandLine); 11 | } 12 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliConfigurerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.kakawait.spring.boot.picocli.autoconfigure; 2 | 3 | import picocli.CommandLine; 4 | 5 | /** 6 | * @author Thibaud Leprêtre 7 | */ 8 | public abstract class PicocliConfigurerAdapter implements PicocliConfigurer { 9 | 10 | @Override 11 | public void configure(CommandLine commandLine) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.kakawait.spring.boot.picocli.autoconfigure.PicocliAutoConfiguration 2 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/test/java/com.kakawait.spring.boot.picocli.autoconfigure/PicocliAutoConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.kakawait.spring.boot.picocli.autoconfigure; 2 | 3 | import org.assertj.core.api.Condition; 4 | import org.assertj.core.api.iterable.Extractor; 5 | import org.junit.After; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.springframework.beans.factory.NoSuchBeanDefinitionException; 9 | import org.springframework.boot.test.rule.OutputCapture; 10 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.stereotype.Component; 14 | import picocli.CommandLine; 15 | 16 | import java.util.Collection; 17 | import java.util.regex.Pattern; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 21 | import static org.hamcrest.Matchers.matchesPattern; 22 | import static picocli.CommandLine.Command; 23 | 24 | /** 25 | * @author Thibaud Leprêtre 26 | */ 27 | public class PicocliAutoConfigurationTest { 28 | 29 | @Rule 30 | public OutputCapture outputCapture = new OutputCapture(); 31 | 32 | private AnnotationConfigApplicationContext context; 33 | 34 | @After 35 | public void tearDown() { 36 | if (this.context != null) { 37 | this.context.close(); 38 | } 39 | } 40 | 41 | @Test 42 | public void autoConfiguration_EmptyConfiguration_Skipped() { 43 | load(EmptyConfiguration.class); 44 | assertThatThrownBy(() -> context.getBean(CommandLine.class)).isInstanceOf(NoSuchBeanDefinitionException.class); 45 | assertThatThrownBy(() -> context.getBean(PicocliCommandLineRunner.class)) 46 | .isInstanceOf(NoSuchBeanDefinitionException.class); 47 | } 48 | 49 | @Test 50 | public void autoConfiguration_CommandLineBean_UsesUserDefinedBean() { 51 | load(CommandLineConfiguration.class); 52 | PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); 53 | assertThat(runner.getCommandLine()).isSameAs(context.getBean(CommandLine.class)); 54 | } 55 | 56 | @Test 57 | public void autoConfiguration_MissingMainCommand_ConfiguresDefaultHelpAwareCommand() throws Exception { 58 | load(SimpleConfiguration.class); 59 | PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); 60 | 61 | assertThat(runner.getCommandLine().getCommand()).isInstanceOf(HelpAwarePicocliCommand.class); 62 | 63 | runner.run("-h"); 64 | 65 | Pattern pattern = Pattern.compile(".*-h, --help\\s+Prints this help message and exits.*", Pattern.DOTALL); 66 | outputCapture.expect(matchesPattern(pattern)); 67 | } 68 | 69 | @Test 70 | public void autoConfiguration_BasicBeanDefinition_CreateSubCommands() { 71 | load(SimpleConfiguration.class); 72 | PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); 73 | Collection commands = context.getBeansWithAnnotation(Command.class).values(); 74 | 75 | assertThat(runner.getCommandLine().getSubcommands().values()) 76 | .hasSameSizeAs(commands) 77 | .extracting("interpreter.command") 78 | .containsExactlyElementsOf(commands) 79 | .doNotHave(new Condition<>(SimpleConfiguration.NoBeanCommand.class::isInstance, "NoBeanCommand")); 80 | } 81 | 82 | @Test 83 | public void autoConfiguration_NestedBeanDefinition_CreateNestedSubCommands() { 84 | load(NestedCommandConfiguration.class); 85 | PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); 86 | Collection commands = context.getBeansWithAnnotation(Command.class).values(); 87 | 88 | Extractor> extractor = input -> input.getSubcommands().values(); 89 | 90 | assertThat(commands).hasSize(5); 91 | assertThat(runner.getCommandLine().getSubcommands().values()) 92 | .hasSize(1) 93 | .haveExactly(1, new Condition<>(e -> { 94 | Class clazz = NestedCommandConfiguration.Level0Command.class; 95 | return e.getCommand().getClass().equals(clazz); 96 | }, "Class Level0Command")) 97 | .flatExtracting(extractor) 98 | .hasSize(2) 99 | .haveExactly(1, new Condition<>(e -> { 100 | Class clazz = NestedCommandConfiguration.Level0Command.Level1Command.class; 101 | return e.getCommand().getClass().equals(clazz); 102 | }, "Class Level1Command")) 103 | .haveExactly(1, new Condition<>(e -> { 104 | Class clazz = NestedCommandConfiguration.Level0Command.Level1bCommand.class; 105 | return e.getCommand().getClass().equals(clazz); 106 | }, "Class Level1bCommand")) 107 | .doNotHave(new Condition<>(e -> { 108 | Class clazz = NestedCommandConfiguration.Level0Command.NoBeanCommand.class; 109 | return e.getCommand().getClass().equals(clazz); 110 | }, "Class NoBeanCommand")) 111 | .flatExtracting(extractor) 112 | .hasSize(1) 113 | .haveExactly(1, new Condition<>(e -> { 114 | Class clazz = NestedCommandConfiguration.Level0Command.Level1Command.Level2Command.class; 115 | return e.getCommand().getClass().equals(clazz); 116 | }, "Class Level2Command")); 117 | } 118 | 119 | @Test 120 | public void autoConfiguration_MultipleMainCommands_RandomUses() { 121 | load(MainCommandsConflictConfiguration.class); 122 | PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); 123 | 124 | assertThat(runner.getCommandLine()) 125 | .is(new Condition<>( 126 | c -> c.getCommand() instanceof MainCommandsConflictConfiguration.MainCommand2, 127 | "Class MainCommand2")); 128 | assertThat(runner.getCommandLine().getSubcommands()).hasSize(0); 129 | } 130 | 131 | @Test 132 | public void autoConfiguration_WithPicocliConfigurerAdapter_Apply() { 133 | load(SimpleConfiguration.class, CustomPicocliConfigurerAdapter.class); 134 | PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); 135 | 136 | assertThat(runner.getCommandLine().getSeparator()).isEqualTo("¯\\_(ツ)_/¯"); 137 | } 138 | 139 | @Test 140 | public void autoConfiguration_WithMultiplePicocliConfigurerAdapters_ApplyAll() { 141 | load(SimpleConfiguration.class, CustomPicocliConfigurerAdapter.class, Custom2PicocliConfigurerAdapter.class); 142 | PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); 143 | 144 | assertThat(runner.getCommandLine().getSeparator()).isEqualTo("¯\\_(ツ)_/¯"); 145 | assertThat(runner.getCommandLine().getSubcommands()).containsKeys("¯\\_(ツ)_/¯"); 146 | } 147 | 148 | @Configuration 149 | static class EmptyConfiguration { 150 | } 151 | 152 | @Configuration 153 | static class CommandLineConfiguration { 154 | @Bean 155 | CommandLine commandLine() { 156 | return new CommandLine(new DummyCommand()); 157 | } 158 | 159 | @Command 160 | static class DummyCommand { 161 | } 162 | } 163 | 164 | @Configuration 165 | static class SimpleConfiguration { 166 | 167 | @Component 168 | @Command(name = "basic") 169 | static class BasicCommand {} 170 | 171 | @Component 172 | @Command(name = "extends command") 173 | static class ExtendsCommand extends HelpAwarePicocliCommand {} 174 | 175 | @Command(name = "No bean command, not considered") 176 | static class NoBeanCommand {} 177 | } 178 | 179 | @Configuration 180 | static class NestedCommandConfiguration { 181 | 182 | @Bean 183 | Level0Command.NoBeanCommand.OrphanCommand orphanCommand() { 184 | return new Level0Command.NoBeanCommand.OrphanCommand(); 185 | } 186 | 187 | @Component 188 | @Command(name = "level 0") 189 | static class Level0Command { 190 | 191 | @Component 192 | @Command(name = "level 1") 193 | static class Level1Command { 194 | 195 | @Component 196 | @Command(name = "level 2") 197 | static class Level2Command {} 198 | } 199 | 200 | @Component 201 | @Command(name = "level 1 b") 202 | static class Level1bCommand { 203 | 204 | } 205 | 206 | @Command(name = "No bean command, not considered") 207 | static class NoBeanCommand { 208 | 209 | @Command(name = "orphan command") 210 | static class OrphanCommand {} 211 | } 212 | } 213 | } 214 | 215 | @Configuration 216 | static class MainCommandsConflictConfiguration { 217 | 218 | @Component 219 | @Command 220 | static class MainCommand {} 221 | 222 | @Component 223 | @Command 224 | static class MainCommand2 {} 225 | } 226 | 227 | @Configuration 228 | static class CustomPicocliConfigurerAdapter extends PicocliConfigurerAdapter { 229 | @Override 230 | public void configure(CommandLine commandLine) { 231 | commandLine.setSeparator("¯\\_(ツ)_/¯"); 232 | } 233 | } 234 | 235 | @Configuration 236 | static class Custom2PicocliConfigurerAdapter extends PicocliConfigurerAdapter { 237 | @Override 238 | public void configure(CommandLine commandLine) { 239 | commandLine.addSubcommand("¯\\_(ツ)_/¯", new BasicCommand()); 240 | } 241 | 242 | @Command 243 | static class BasicCommand {} 244 | } 245 | 246 | private void load(Class... configs) { 247 | AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); 248 | context.register(configs); 249 | context.register(PicocliAutoConfiguration.class); 250 | context.refresh(); 251 | this.context = context; 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /picocli-spring-boot-autoconfigure/src/test/java/com.kakawait.spring.boot.picocli.autoconfigure/PicocliCommandLineRunnerTest.java: -------------------------------------------------------------------------------- 1 | package com.kakawait.spring.boot.picocli.autoconfigure; 2 | 3 | import net.bytebuddy.ByteBuddy; 4 | import net.bytebuddy.description.annotation.AnnotationDescription; 5 | import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; 6 | import org.junit.Before; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.InOrder; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.MockitoJUnitRunner; 13 | import org.springframework.boot.test.rule.OutputCapture; 14 | import picocli.CommandLine; 15 | import picocli.CommandLine.Command; 16 | 17 | import java.lang.reflect.InvocationTargetException; 18 | import java.util.Arrays; 19 | import java.util.Collections; 20 | import java.util.List; 21 | import java.util.concurrent.Callable; 22 | import java.util.regex.Pattern; 23 | 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | import static org.hamcrest.Matchers.matchesPattern; 26 | import static org.hamcrest.Matchers.not; 27 | import static org.mockito.ArgumentMatchers.any; 28 | import static org.mockito.ArgumentMatchers.anyString; 29 | import static org.mockito.Mockito.doAnswer; 30 | import static org.mockito.Mockito.inOrder; 31 | import static org.mockito.Mockito.times; 32 | import static org.mockito.Mockito.verifyNoMoreInteractions; 33 | import static org.mockito.Mockito.when; 34 | import static picocli.CommandLine.Help.Ansi; 35 | import static picocli.CommandLine.Option; 36 | import static picocli.CommandLine.ParameterException; 37 | import static picocli.CommandLine.usage; 38 | 39 | /** 40 | * @author Thibaud Leprêtre 41 | */ 42 | @RunWith(MockitoJUnitRunner.class) 43 | public class PicocliCommandLineRunnerTest { 44 | 45 | @Rule 46 | public OutputCapture outputCapture = new OutputCapture(); 47 | 48 | @Mock 49 | private CommandLine cli; 50 | 51 | private PicocliCommandLineRunner runner; 52 | 53 | @Before 54 | public void setup() { 55 | runner = new PicocliCommandLineRunner(cli); 56 | } 57 | 58 | @Test 59 | public void run_ExceptionDuringParsing_PrintUsageAndStop() throws Exception { 60 | when(cli.parse(any())).thenThrow(new ParameterException("Error when parsing")); 61 | 62 | runner.run("parsing error or something else"); 63 | 64 | InOrder inOrder = inOrder(cli); 65 | inOrder.verify(cli, times(1)).parse(anyString()); 66 | inOrder.verify(cli, times(1)).usage(System.err, Ansi.AUTO); 67 | verifyNoMoreInteractions(cli); 68 | } 69 | 70 | @Test 71 | public void run_MainCommandHelpRequested_PrintUsageAndStop() throws Exception { 72 | HelpCommand command = new HelpCommand(true); 73 | 74 | when(cli.getCommand()).thenReturn(command); 75 | doAnswer(invocation -> { 76 | usage(command, invocation.getArgument(0), ((Ansi) invocation.getArgument(1))); 77 | return null; 78 | }).when(cli).usage(System.out, Ansi.AUTO); 79 | 80 | runner.run("-h"); 81 | 82 | InOrder inOrder = inOrder(cli); 83 | inOrder.verify(cli, times(1)).parse("-h"); 84 | inOrder.verify(cli, times(1)).getCommand(); 85 | inOrder.verify(cli, times(1)).usage(System.out, Ansi.AUTO); 86 | verifyNoMoreInteractions(cli); 87 | 88 | Pattern pattern = Pattern.compile(".*Usage: main \\[-h\\].*", Pattern.DOTALL); 89 | outputCapture.expect(matchesPattern(pattern)); 90 | } 91 | 92 | @Test 93 | public void run_SubCommandHelpRequested_PrintUsageAndStop() throws Exception { 94 | when(cli.parse(any())).thenReturn(Collections.singletonList(new CommandLine(new HelpSubCommand(true)))); 95 | when(cli.getCommand()).thenReturn(new HelpCommand(false)); 96 | 97 | runner.run("subcommand -h"); 98 | 99 | InOrder inOrder = inOrder(cli); 100 | inOrder.verify(cli, times(1)).parse("subcommand -h"); 101 | inOrder.verify(cli, times(1)).getCommand(); 102 | verifyNoMoreInteractions(cli); 103 | 104 | Pattern pattern = Pattern.compile(".*Usage: subcommand \\[-h\\].*", Pattern.DOTALL); 105 | outputCapture.expect(matchesPattern(pattern)); 106 | } 107 | 108 | @Test 109 | public void run_NestedSubCommandHelpRequested_PrintUsageAndStop() throws Exception { 110 | List commandLines = Arrays.asList( 111 | new CommandLine(new HelpSubCommand(false)), 112 | new CommandLine(new HelpNestedSubCommand(true)) 113 | ); 114 | when(cli.parse(any())).thenReturn(commandLines); 115 | when(cli.getCommand()).thenReturn(new HelpCommand(false)); 116 | 117 | runner.run("subcommand nested-subcommand -h"); 118 | 119 | InOrder inOrder = inOrder(cli); 120 | inOrder.verify(cli, times(1)).parse("subcommand nested-subcommand -h"); 121 | inOrder.verify(cli, times(1)).getCommand(); 122 | verifyNoMoreInteractions(cli); 123 | 124 | Pattern pattern = Pattern.compile(".*Usage: nested-subcommand \\[-h\\].*", Pattern.DOTALL); 125 | outputCapture.expect(matchesPattern(pattern)); 126 | } 127 | 128 | @Test 129 | public void run_HelpRequestedConflict_PrintFirstChildrenUsageAndStop() throws Exception { 130 | List commandLines = Arrays.asList( 131 | new CommandLine(new HelpSubCommand(true)), 132 | new CommandLine(new HelpNestedSubCommand(true)) 133 | ); 134 | when(cli.parse(any())).thenReturn(commandLines); 135 | when(cli.getCommand()).thenReturn(new HelpCommand(false)); 136 | 137 | runner.run("subcommand -h nested-subcommand -h"); 138 | 139 | InOrder inOrder = inOrder(cli); 140 | inOrder.verify(cli, times(1)).parse("subcommand -h nested-subcommand -h"); 141 | inOrder.verify(cli, times(1)).getCommand(); 142 | verifyNoMoreInteractions(cli); 143 | 144 | Pattern pattern = Pattern.compile(".*Usage: subcommand \\[-h\\].*", Pattern.DOTALL); 145 | outputCapture.expect(matchesPattern(pattern)); 146 | } 147 | 148 | @Test 149 | public void run_MainRunnableCommand_Execute() throws Exception { 150 | Runnable mainCommand = makeRunnableCommand("main", () -> System.out.println("Main runnable command")); 151 | when(cli.parse(any())).thenReturn(Collections.singletonList(new CommandLine(mainCommand))); 152 | when(cli.getCommand()).thenReturn(mainCommand); 153 | 154 | runner.run(""); 155 | 156 | InOrder inOrder = inOrder(cli); 157 | inOrder.verify(cli, times(1)).parse(any()); 158 | inOrder.verify(cli, times(1)).getCommand(); 159 | verifyNoMoreInteractions(cli); 160 | 161 | Pattern pattern = Pattern.compile(".*Main runnable command.*", Pattern.DOTALL); 162 | outputCapture.expect(matchesPattern(pattern)); 163 | } 164 | 165 | @Test 166 | public void run_MainCallableCommand_Execute() throws Exception { 167 | Callable mainCommand = makeCallableCommand("test", (Callable) () -> { 168 | System.out.println("Main callable command"); 169 | return null; 170 | }); 171 | when(cli.parse(any())).thenReturn(Collections.singletonList(new CommandLine(mainCommand))); 172 | when(cli.getCommand()).thenReturn(mainCommand); 173 | 174 | runner.run(""); 175 | 176 | InOrder inOrder = inOrder(cli); 177 | inOrder.verify(cli, times(1)).parse(any()); 178 | inOrder.verify(cli, times(1)).getCommand(); 179 | verifyNoMoreInteractions(cli); 180 | 181 | Pattern pattern = Pattern.compile(".*Main callable command.*", Pattern.DOTALL); 182 | outputCapture.expect(matchesPattern(pattern)); 183 | } 184 | 185 | @Test 186 | public void run_MainPicocliCommand_InjectContextThenExecute() throws Exception { 187 | PicocliCommand mainCommand = makePicocliCommand("main", () -> System.out.println("Main picocli command")); 188 | 189 | List commandLines = Collections.singletonList(new CommandLine(mainCommand)); 190 | when(cli.parse(any())).thenReturn(commandLines); 191 | when(cli.getCommand()).thenReturn(mainCommand); 192 | 193 | runner.run(""); 194 | 195 | assertThat(mainCommand.getRootContext()).isSameAs(cli); 196 | assertThat(mainCommand.getContext()).isSameAs(commandLines.get(0)); 197 | assertThat(mainCommand.getParsedCommands()).isSameAs(commandLines); 198 | 199 | InOrder inOrder = inOrder(cli); 200 | inOrder.verify(cli, times(1)).parse(any()); 201 | inOrder.verify(cli, times(1)).getCommand(); 202 | verifyNoMoreInteractions(cli); 203 | 204 | Pattern pattern = Pattern.compile(".*Main picocli command.*", Pattern.DOTALL); 205 | outputCapture.expect(matchesPattern(pattern)); 206 | } 207 | 208 | @Test 209 | public void run_MultipleRunnableOrCallableCommands_AllExecuted() throws Exception { 210 | PicocliCommand mainCommand = makePicocliCommand("main", () -> System.out.println("Main picocli command")); 211 | Runnable subCommand = makeRunnableCommand("subcommand", () -> System.out.println("Sub runnable command")); 212 | Callable subSubCommand = makeCallableCommand("subsubcommand", (Callable) () -> { 213 | System.out.println("Sub sub callable command"); 214 | return null; 215 | }); 216 | List commandLines = Arrays.asList( 217 | new CommandLine(mainCommand), 218 | new CommandLine(subCommand), 219 | new CommandLine(subSubCommand) 220 | ); 221 | 222 | when(cli.parse(any())).thenReturn(commandLines); 223 | when(cli.getCommand()).thenReturn(mainCommand); 224 | 225 | runner.run("subcommand subsubcommand"); 226 | 227 | InOrder inOrder = inOrder(cli); 228 | inOrder.verify(cli, times(1)).parse(any()); 229 | inOrder.verify(cli, times(1)).getCommand(); 230 | verifyNoMoreInteractions(cli); 231 | 232 | Pattern pattern = Pattern 233 | .compile(".*Main picocli command.*Sub runnable command.*Sub sub callable command.*", Pattern.DOTALL); 234 | outputCapture.expect(matchesPattern(pattern)); 235 | } 236 | 237 | @Test 238 | public void run_FlowControlWithExitStatus_BreakOnTermination() throws Exception { 239 | PicocliCommand mainCommand = makePicocliCommand("main", (Callable) () -> { 240 | System.out.println("Hello"); 241 | return ExitStatus.OK; 242 | }); 243 | PicocliCommand subCommand = makePicocliCommand("subcommand", (Callable) () -> { 244 | System.out.println("World!"); 245 | return ExitStatus.TERMINATION; 246 | }); 247 | Runnable subSubCommand = makeRunnableCommand("subsubcommand", () -> System.out.println("Ignore me...")); 248 | List commandLines = Arrays.asList( 249 | new CommandLine(mainCommand), 250 | new CommandLine(subCommand), 251 | new CommandLine(subSubCommand) 252 | ); 253 | 254 | when(cli.parse(any())).thenReturn(commandLines); 255 | when(cli.getCommand()).thenReturn(mainCommand); 256 | 257 | runner.run("subcommand subsubcommand"); 258 | 259 | InOrder inOrder = inOrder(cli); 260 | inOrder.verify(cli, times(1)).parse(any()); 261 | inOrder.verify(cli, times(1)).getCommand(); 262 | verifyNoMoreInteractions(cli); 263 | 264 | outputCapture.expect(matchesPattern(Pattern.compile(".*Hello.*World!\\n*", Pattern.DOTALL))); 265 | outputCapture.expect(not(matchesPattern(Pattern.compile(".*Ignore me\\.\\.\\..*")))); 266 | } 267 | 268 | @Test 269 | public void run_NorRunnableNorCallableCommand_Nothing() throws Exception { 270 | Object mainCommand = new EmptyCommand(); 271 | when(cli.parse(any())).thenReturn(Collections.singletonList(new CommandLine(mainCommand))); 272 | when(cli.getCommand()).thenReturn(mainCommand); 273 | 274 | runner.run(""); 275 | 276 | InOrder inOrder = inOrder(cli); 277 | inOrder.verify(cli, times(1)).parse(any()); 278 | inOrder.verify(cli, times(1)).getCommand(); 279 | verifyNoMoreInteractions(cli); 280 | } 281 | 282 | private AnnotationDescription getCommandAnnotationDescription(String commandName) { 283 | return AnnotationDescription 284 | .Builder 285 | .ofType(Command.class) 286 | .define("name", commandName) 287 | .build(); 288 | } 289 | 290 | private Runnable makeRunnableCommand(String commandName, Runnable runnable) 291 | throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { 292 | return make(commandName, DelegateRunnable.class).getDeclaredConstructor(Runnable.class).newInstance(runnable); 293 | } 294 | 295 | private Callable makeCallableCommand(String commandName, Callable callable) 296 | throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 297 | return make(commandName, DelegateCallable.class).getDeclaredConstructor(Callable.class).newInstance(callable); 298 | } 299 | 300 | private PicocliCommand makePicocliCommand(String commandName, Runnable runnable) 301 | throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 302 | return make(commandName, DelegatePicocliCommand.class) 303 | .getDeclaredConstructor(Runnable.class) 304 | .newInstance(runnable); 305 | } 306 | 307 | private PicocliCommand makePicocliCommand(String commandName, Callable callable) 308 | throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 309 | return make(commandName, DelegatePicocliCommand.class) 310 | .getDeclaredConstructor(Callable.class) 311 | .newInstance(callable); 312 | } 313 | 314 | private Class make(String commandName, Class type) { 315 | return new ByteBuddy() 316 | .subclass(type, ConstructorStrategy.Default.IMITATE_SUPER_CLASS) 317 | .annotateType(getCommandAnnotationDescription(commandName)) 318 | .make() 319 | .load(getClass().getClassLoader()) 320 | .getLoaded(); 321 | } 322 | 323 | private static class DelegateRunnable implements Runnable { 324 | private final Runnable delegate; 325 | 326 | public DelegateRunnable(Runnable delegate) { 327 | this.delegate = delegate; 328 | } 329 | 330 | @Override 331 | public void run() { 332 | delegate.run(); 333 | } 334 | } 335 | 336 | private static class DelegateCallable implements Callable { 337 | private final Callable delegate; 338 | 339 | public DelegateCallable(Callable delegate) { 340 | this.delegate = delegate; 341 | } 342 | 343 | @Override 344 | public V call() throws Exception { 345 | return delegate.call(); 346 | } 347 | } 348 | 349 | private static class DelegatePicocliCommand extends PicocliCommand { 350 | private Callable delegateCallable; 351 | 352 | private Runnable delegateRunnable; 353 | 354 | public DelegatePicocliCommand(Callable delegate) { 355 | this.delegateCallable = delegate; 356 | } 357 | 358 | public DelegatePicocliCommand(Runnable delegate) { 359 | this.delegateRunnable = delegate; 360 | } 361 | 362 | @Override 363 | public ExitStatus call() throws Exception { 364 | if (delegateCallable == null) { 365 | return super.call(); 366 | } 367 | return delegateCallable.call(); 368 | } 369 | 370 | @Override 371 | public void run() { 372 | delegateRunnable.run(); 373 | } 374 | } 375 | 376 | @Command(name = "main") 377 | private static class HelpCommand { 378 | @Option(names = {"-h", "--help"}, help = true, description = "Prints this help message and exits") 379 | private boolean helpRequested; 380 | 381 | HelpCommand(boolean helpRequested) { 382 | this.helpRequested = helpRequested; 383 | } 384 | } 385 | 386 | @Command(name = "subcommand") 387 | private static class HelpSubCommand extends HelpCommand { 388 | HelpSubCommand(boolean helpRequested) { 389 | super(helpRequested); 390 | } 391 | } 392 | 393 | @Command(name = "nested-subcommand") 394 | private static class HelpNestedSubCommand extends HelpCommand { 395 | HelpNestedSubCommand(boolean helpRequested) { 396 | super(helpRequested); 397 | } 398 | } 399 | 400 | @Command 401 | private static class EmptyCommand {} 402 | } 403 | -------------------------------------------------------------------------------- /picocli-spring-boot-sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.kakawait 8 | picocli-spring-boot-sample 9 | 0.2.0 10 | 11 | 12 | UTF-8 13 | UTF-8 14 | 15 | 1.8 16 | 1.8 17 | 1.8 18 | 22.0 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-parent 24 | 1.5.4.RELEASE 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-jdbc 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-actuator 40 | 41 | 42 | com.kakawait 43 | picocli-spring-boot-starter 44 | ${project.version} 45 | 46 | 47 | com.google.guava 48 | guava 49 | ${guava.version} 50 | 51 | 52 | 53 | 54 | org.flywaydb 55 | flyway-core 56 | 57 | 58 | 59 | 60 | com.h2database 61 | h2 62 | runtime 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-maven-plugin 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /picocli-spring-boot-sample/src/main/java/com/kakawait/PicocliSpringBootSampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.kakawait; 2 | 3 | import com.google.common.base.CaseFormat; 4 | import com.kakawait.spring.boot.picocli.autoconfigure.ExitStatus; 5 | import com.kakawait.spring.boot.picocli.autoconfigure.HelpAwarePicocliCommand; 6 | import com.kakawait.spring.boot.picocli.autoconfigure.PicocliConfigurerAdapter; 7 | import org.flywaydb.core.Flyway; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.boot.SpringApplication; 11 | import org.springframework.boot.actuate.health.HealthIndicator; 12 | import org.springframework.boot.autoconfigure.SpringBootApplication; 13 | import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | import org.springframework.stereotype.Component; 17 | import org.springframework.util.StringUtils; 18 | import picocli.CommandLine; 19 | import picocli.CommandLine.Help.Ansi; 20 | import picocli.CommandLine.Option; 21 | import picocli.CommandLine.Parameters; 22 | 23 | import java.util.Map; 24 | import java.util.regex.Matcher; 25 | import java.util.regex.Pattern; 26 | 27 | import static picocli.CommandLine.Command; 28 | 29 | /** 30 | * Picocli spring boot starter sample 31 | * 32 | * This sample will create following CLI 33 | * 34 | *
 35 |  * {@code
 36 |  * Usage: 
[-vh] 37 | * -v, --version display version info 38 | * -h, --help Prints this help message and exits 39 | * Commands: 40 | * flyway [-h, --help] 41 | * migrate 42 | * repair 43 | * greeting [-h, --help] [NAME] 44 | * health [-h, --help] 45 | * db 46 | * disk-space 47 | * } 48 | *
49 | * Thus running following commands should output following: 50 | *
 51 |  * {@code
 52 |  * $> java -jar .jar -v
 53 |  * 0.1.0
 54 |  *
 55 |  * $> java -jar .jar -h
 56 |  * Usage: 
[-vh] 57 | * -v, --version display version info 58 | * -h, --help Prints this help message and exits 59 | * Commands: 60 | * flyway 61 | * greeting 62 | * health 63 | * 64 | * $> java -jar .jar flyway 65 | * Usage: flyway [-h] 66 | * -h, --help Prints this help message and exits 67 | * Commands: 68 | * migrate 69 | * repair 70 | * 71 | * $> java -jar .jar flyway migrate 72 | * 2017-07-06 11:21:14.560 INFO 77637 --- [main] o.f.core.internal.util.VersionPrinter : Flyway 3.2.1 by Boxfuse 73 | * 2017-07-06 11:21:14.567 INFO 77637 --- [main] o.f.c.i.dbsupport.DbSupportFactory : Database: jdbc:h2:mem:testdb (H2 1.4) 74 | * 2017-07-06 11:21:14.601 INFO 77637 --- [main] o.f.core.internal.command.DbValidate : Validated 2 migrations (execution time 00:00.013s) 75 | * 2017-07-06 11:21:14.621 INFO 77637 --- [main] o.f.c.i.metadatatable.MetaDataTableImpl : Creating Metadata table: "PUBLIC"."schema_version" 76 | * 2017-07-06 11:21:14.638 INFO 77637 --- [main] o.f.core.internal.command.DbMigrate : Current version of schema "PUBLIC": << Empty Schema >> 77 | * 2017-07-06 11:21:14.638 INFO 77637 --- [main] o.f.core.internal.command.DbMigrate : Migrating schema "PUBLIC" to version 1 - init 78 | * 2017-07-06 11:21:14.666 INFO 77637 --- [main] o.f.core.internal.command.DbMigrate : Migrating schema "PUBLIC" to version 2 - add 79 | * 2017-07-06 11:21:14.672 INFO 77637 --- [main] o.f.core.internal.command.DbMigrate : Successfully applied 2 migrations to schema "PUBLIC" (execution time 00:00.053s). 80 | * 81 | * $> java -jar .jar health disk-space 82 | * UP {total=420143575040, free=41032192000, threshold=10485760} 83 | * 84 | * $> java -jar .jar health db 85 | * UP {database=H2, hello=1} 86 | * } 87 | *
88 | * @author Thibaud Leprêtre 89 | */ 90 | @SpringBootApplication 91 | public class PicocliSpringBootSampleApplication { 92 | 93 | public static void main(String[] args) { 94 | SpringApplication.run(PicocliSpringBootSampleApplication.class, args); 95 | } 96 | 97 | /** 98 | * Disable flyway automatic migration on startup. 99 | * I will be piloted using CLI 100 | * @return No operation flyway migration strategy 101 | */ 102 | @Bean 103 | FlywayMigrationStrategy flywayMigrationStrategy() { 104 | return flyway -> {}; 105 | } 106 | 107 | @Configuration 108 | static class PicocliConfiguration extends PicocliConfigurerAdapter { 109 | 110 | private static final Logger logger = LoggerFactory.getLogger(PicocliConfiguration.class); 111 | 112 | private static final Pattern HEALTH_PATTERN = Pattern.compile("^(\\w+?)HealthIndicator$"); 113 | 114 | private final Map healthIndicators; 115 | 116 | public PicocliConfiguration(Map healthIndicators) { 117 | this.healthIndicators = healthIndicators; 118 | } 119 | 120 | @Override 121 | public void configure(CommandLine cli) { 122 | CommandLine healthCli = new CommandLine(new HelpAwareContainerPicocliCommand() {}); 123 | for (Map.Entry entry : healthIndicators.entrySet()) { 124 | Matcher matcher = HEALTH_PATTERN.matcher(entry.getKey()); 125 | if (matcher.matches()) { 126 | String name = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, matcher.group(1)); 127 | healthCli.addSubcommand(name, new PrintCommand(entry.getValue().health())); 128 | } else { 129 | logger.warn("Unable to determine a correct name for given indicator: \"{}\", skip it!", 130 | entry.getKey()); 131 | } 132 | } 133 | cli.addSubcommand("health", healthCli); 134 | } 135 | 136 | @Command 137 | private static class PrintCommand implements Runnable { 138 | private final Object object; 139 | 140 | PrintCommand(Object object) { 141 | this.object = object; 142 | } 143 | 144 | @Override 145 | public void run() { 146 | System.out.println(object); 147 | } 148 | } 149 | } 150 | 151 | @Component 152 | @Command 153 | static class MainCommand extends HelpAwarePicocliCommand { 154 | @Option(names = {"-v", "--version"}, description = "display version info") 155 | boolean versionRequested; 156 | 157 | @Override 158 | public ExitStatus call() { 159 | if (versionRequested) { 160 | System.out.println("0.1.0"); 161 | return ExitStatus.TERMINATION; 162 | } 163 | return ExitStatus.OK; 164 | } 165 | } 166 | 167 | @Component 168 | @Command(name = "greeting") 169 | static class GreetingCommand extends HelpAwarePicocliCommand { 170 | 171 | @Parameters(paramLabel = "NAME", description = "name", arity = "0..1") 172 | String name; 173 | 174 | @Override 175 | public void run() { 176 | if (StringUtils.hasText(name)) { 177 | System.out.println("Hello " + name + "!"); 178 | } else { 179 | System.out.println("Hello world!"); 180 | } 181 | } 182 | } 183 | 184 | @Component 185 | @Command(name = "flyway") 186 | static class FlywayCommand extends HelpAwareContainerPicocliCommand { 187 | 188 | @Component 189 | @Command(name = "migrate") 190 | static class MigrateCommand implements Runnable { 191 | 192 | private final Flyway flyway; 193 | 194 | public MigrateCommand(Flyway flyway) { 195 | this.flyway = flyway; 196 | } 197 | 198 | @Override 199 | public void run() { 200 | flyway.migrate(); 201 | } 202 | } 203 | 204 | @Component 205 | @Command(name = "repair") 206 | static class RepairCommand implements Runnable { 207 | private final Flyway flyway; 208 | 209 | public RepairCommand(Flyway flyway) { 210 | this.flyway = flyway; 211 | } 212 | 213 | @Override 214 | public void run() { 215 | flyway.repair(); 216 | } 217 | } 218 | } 219 | 220 | @Command 221 | private static abstract class HelpAwareContainerPicocliCommand extends HelpAwarePicocliCommand { 222 | @Override 223 | public ExitStatus call() { 224 | if (getParsedCommands().get(getParsedCommands().size() - 1).getCommand().equals(this)) { 225 | getContext().usage(System.out, Ansi.AUTO); 226 | return ExitStatus.TERMINATION; 227 | } 228 | return ExitStatus.OK; 229 | } 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /picocli-spring-boot-sample/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | web-environment: false 4 | logging: 5 | level: 6 | ROOT: off 7 | org.flywaydb: info 8 | -------------------------------------------------------------------------------- /picocli-spring-boot-sample/src/main/resources/db/migration/V1__init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE person ( 2 | id BIGINT AUTO_INCREMENT, 3 | first_name VARCHAR(255) NOT NULL, 4 | last_name VARCHAR(255) NOT NULL, 5 | PRIMARY KEY(id) 6 | ); 7 | 8 | insert into person (first_name, last_name) values ('Thibaud', 'Lepretre'); 9 | -------------------------------------------------------------------------------- /picocli-spring-boot-sample/src/main/resources/db/migration/V2__add.sql: -------------------------------------------------------------------------------- 1 | insert into person (first_name, last_name) values ('Remko', 'Popma'); 2 | -------------------------------------------------------------------------------- /picocli-spring-boot-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.kakawait 7 | picocli-spring-boot-starter 8 | 0.2.0 9 | jar 10 | 11 | Picocli spring boot starter 12 | Spring boot starter for Picocli command line tools. Let you easily write CLI! 13 | https://github.com/kakawait/picocli-spring-boot-starter 14 | 15 | 16 | 17 | MIT License 18 | http://www.opensource.org/licenses/mit-license.php 19 | repo 20 | 21 | 22 | 23 | 24 | 25 | Thibaud Leprêtre 26 | thibaud.lepretre@gmail.com 27 | 28 | 29 | 30 | 31 | https://github.com/kakawait/picocli-spring-boot-starter 32 | scm:git:git@github.com:kakawait/picocli-spring-boot-starter.git 33 | scm:git:git@github.com:kakawait/picocli-spring-boot-starter.git 34 | 35 | 36 | 37 | UTF-8 38 | UTF-8 39 | 40 | 1.8 41 | 1.8 42 | 1.8 43 | 44 | 3.0.1 45 | 2.10.4 46 | 1.6 47 | 1.6.8 48 | 49 | 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-dependencies 55 | 1.5.4.RELEASE 56 | pom 57 | import 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter 66 | 67 | 68 | com.kakawait 69 | picocli-spring-boot-autoconfigure 70 | ${project.version} 71 | 72 | 73 | 74 | 75 | 76 | oss.sonatype.org 77 | Sonatype OSS Staging 78 | https://oss.sonatype.org/service/local/staging/deploy/maven2 79 | default 80 | 81 | 82 | oss.sonatype.org 83 | Sonatype OSS Snapshots 84 | https://oss.sonatype.org/content/repositories/snapshots 85 | default 86 | 87 | 88 | 89 | 90 | 91 | release 92 | 93 | gpg2 94 | 95 | 96 | 97 | 98 | org.apache.maven.plugins 99 | maven-source-plugin 100 | ${maven-source-plugin.version} 101 | 102 | 103 | attach-sources 104 | verify 105 | 106 | jar-no-fork 107 | 108 | 109 | 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-javadoc-plugin 114 | ${maven-javadoc-plugin.version} 115 | 116 | 117 | attach-javadocs 118 | verify 119 | 120 | jar 121 | 122 | 123 | 124 | 125 | 126 | org.apache.maven.plugins 127 | maven-gpg-plugin 128 | ${maven-gpg-plugin.version} 129 | 130 | 131 | sign-artifacts 132 | verify 133 | 134 | sign 135 | 136 | 137 | 138 | 139 | 140 | org.sonatype.plugins 141 | nexus-staging-maven-plugin 142 | ${nexus-staging-maven-plugin.version} 143 | true 144 | 145 | oss.sonatype.org 146 | https://oss.sonatype.org/ 147 | true 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /picocli-spring-boot-starter/src/main/resources/META-INF/spring.provides: -------------------------------------------------------------------------------- 1 | provides: picocli 2 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.kakawait 8 | picocli-spring-boot-starter-parent 9 | pom 10 | 0.2.0 11 | 12 | Picocli spring boot starter parent 13 | Spring boot starter for Picocli command line parser that will simplify your CommandLineRunner 14 | https://github.com/kakawait/picocli-spring-boot-starter 15 | 16 | 17 | 18 | MIT License 19 | http://www.opensource.org/licenses/mit-license.php 20 | repo 21 | 22 | 23 | 24 | 25 | 26 | Thibaud Leprêtre 27 | thibaud.lepretre@gmail.com 28 | 29 | 30 | 31 | 32 | https://github.com/kakawait/picocli-spring-boot-starter 33 | scm:git:git@github.com:kakawait/picocli-spring-boot-starter.git 34 | scm:git:git@github.com:kakawait/picocli-spring-boot-starter.git 35 | 36 | 37 | 38 | true 39 | 40 | 41 | 42 | picocli-spring-boot-starter 43 | picocli-spring-boot-autoconfigure 44 | 45 | 46 | 47 | --------------------------------------------------------------------------------