From a803073efc202cc9fc0fe1124cdf21b44ed462d9 Mon Sep 17 00:00:00 2001 From: Andreas Deininger Date: Sat, 17 Oct 2020 11:29:18 +0200 Subject: [PATCH 1/6] Manual: Mixee example code: add missing @Command annotation on subcommand --- docs/index.adoc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/index.adoc b/docs/index.adoc index dbbd38757..80629cd64 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -4954,7 +4954,7 @@ You pass in the name of the command and the annotated object to populate with th The specified name is used by the parser to recognize subcommands in the command line arguments. .Java -[source,java,,role="primary"] +[source,java,role="primary"] ---- CommandLine commandLine = new CommandLine(new Git()) .addSubcommand("status", new GitStatus()) @@ -6356,6 +6356,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParseResult; import picocli.CommandLine.Spec; import static picocli.CommandLine.Spec.Target.MIXEE; @@ -6420,6 +6426,7 @@ class MyApp implements Runnable { } } +@Command(name = "sub") class Sub implements Runnable { private static Logger logger = LogManager.getLogger(); @@ -6443,7 +6450,6 @@ class Sub implements Runnable { } ---- - With this, the `-v` option can be specified on the top-level command as well as the subcommands, so all of the below are valid invocations: ---- From 2112f07b3aece943088f1fbda8d9c2643fa0d583 Mon Sep 17 00:00:00 2001 From: Andreas Deininger Date: Sat, 17 Oct 2020 11:38:59 +0200 Subject: [PATCH 2/6] Manual: LoggingMixin sample: add tab with Kotlin source code --- docs/index.adoc | 110 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/docs/index.adoc b/docs/index.adoc index 80629cd64..e865b76d0 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -6349,20 +6349,15 @@ The example below shows a class that uses Log4j for logging, and has a "global" The `@Spec(MIXEE)`-annotated field allows the mixin to climb the command hierarchy and store the "verbosity" value in a single place. When the application is started, a custom execution strategy is used to configure the log level from that single value. -[source,java] +.Java +[source,java,role="primary"] ---- -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.*; import org.apache.logging.log4j.core.config.Configurator; import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.Mixin; +import picocli.CommandLine.*; import picocli.CommandLine.Model.CommandSpec; -import picocli.CommandLine.Option; -import picocli.CommandLine.ParseResult; -import picocli.CommandLine.Spec; import static picocli.CommandLine.Spec.Target.MIXEE; class LoggingMixin { @@ -6450,6 +6445,103 @@ class Sub implements Runnable { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +import org.apache.logging.log4j.* +import org.apache.logging.log4j.core.config.Configurator + +import picocli.CommandLine +import picocli.CommandLine.* +import picocli.CommandLine.Model.CommandSpec + +class LoggingMixin { + @Spec(Spec.Target.MIXEE) + private lateinit var mixee : CommandSpec// spec of the command where the @Mixin is used + var verbosity = BooleanArray(0) + + /** + * Sets the specified verbosity on the LoggingMixin of the top-level command. + * @param verbosity the new verbosity value + */ + @Option( + names = ["-v", "--verbose"], description = [ + "Specify multiple -v options to increase verbosity.", + "For example, `-v -v -v` or `-vvv`"] + ) + fun setVerbose(verbosity: BooleanArray) { + // Each subcommand that mixes in the LoggingMixin has its own instance + // of this class, so there may be many LoggingMixin instances. + // We want to store the verbosity value in a single, central place, + // so we find the top-level command, + // and store the verbosity level on our top-level command's LoggingMixin. + (mixee.root().userObject() as MyApp).loggingMixin.verbosity = verbosity + } +} + +@Command(name = "app", subcommands = [Sub::class]) +class MyApp : Runnable { + @Mixin + lateinit var loggingMixin: LoggingMixin + override fun run() { + logger.trace("Starting... (trace) from app") + logger.debug("Starting... (debug) from app") + logger.info ("Starting... (info) from app") + logger.warn ("Starting... (warn) from app") + } + + private fun calcLogLevel(): Level { + return when (loggingMixin.verbosity.size) { + 0 -> Level.WARN + 1 -> Level.INFO + 2 -> Level.DEBUG + else -> Level.TRACE + } + } + + // A reference to this method can be used as a custom execution strategy + // that first configures Log4j based on the specified verbosity level, + // and then delegates to the default execution strategy. + private fun executionStrategy(parseResult: ParseResult): Int { + Configurator.setRootLevel(calcLogLevel()) // configure log4j + return RunLast().execute(parseResult) // default execution strategy + } + + companion object { + private val logger: Logger = LogManager.getLogger(MyApp::class.java) + @JvmStatic + fun main(args: Array) { + val app = MyApp() + CommandLine(app) + .setExecutionStrategy { parseResult: ParseResult -> app.executionStrategy(parseResult) } + .execute(*args) + } + } +} + +@Command(name = "sub") +class Sub : Runnable { + @Mixin + lateinit var loggingMixin: LoggingMixin + private val logger: Logger = LogManager.getLogger() + + override fun run() { + logger.trace("Hi (tracing) from app sub") + logger.debug("Hi (debugging) from app sub") + logger.info ("Hi (info) from app sub") + logger.warn ("Hi (warning) from app sub") + } + + @Command + fun subsubmethod(@Mixin loggingMixin: LoggingMixin?) { + logger.trace("Hi (tracing) from app sub subsubmethod") + logger.debug("Hi (debugging) from app sub subsubmethod") + logger.info ("Hi (info) from app sub subsubmethod") + logger.warn ("Hi (warning) from app sub subsubmethod") + } +} +---- + With this, the `-v` option can be specified on the top-level command as well as the subcommands, so all of the below are valid invocations: ---- From 61bf1359201b84a9c8f60e236b2948d1a26524c4 Mon Sep 17 00:00:00 2001 From: Andreas Deininger Date: Sat, 17 Oct 2020 21:51:41 +0200 Subject: [PATCH 3/6] Manual: add tabs with Kotlin code for samples (chapter 18: Reuse) --- docs/index.adoc | 164 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 7 deletions(-) diff --git a/docs/index.adoc b/docs/index.adoc index e865b76d0..2c3d2f6b4 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -6175,7 +6175,8 @@ Here is an example mixin class `MyLogger`. It has an option `-v` that can be specified multiple times to increase verbosity. The `MyLogger` class also provides some methods like `debug` and `trace` that can be used to print messages to the standard error stream. -[source,java] +.Java +[source,java,role="primary"] ---- public class MyLogger { @@ -6203,9 +6204,40 @@ public class MyLogger { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class MyLogger { + + @Option(names = ["-v", "--verbose"], + description = ["Increase verbosity. Specify multiple times to increase (-vvv)."]) + var verbosity = BooleanArray(0) + + fun info(pattern: String, vararg params: Any) { + log(0, pattern, *params) + } + + fun debug(pattern: String, vararg params: Any) { + log(1, pattern, *params) + } + + fun trace(pattern: String, vararg params: Any) { + log(2, pattern, *params) + } + + private fun log(level: Int, pattern: String, vararg params: Any) { + if (verbosity.size > level) { + System.err.printf(pattern, *params) + } + } +} +---- + + An application could use the mixin like this: -[source,java] +.Java +[source,java,role="primary"] ---- class MyApp implements Runnable { @@ -6223,6 +6255,29 @@ class MyApp implements Runnable { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class MyApp : Runnable { + + @Mixin + lateinit var myLogger: MyLogger + + override fun run() { + myLogger.info("Hello info%n") + myLogger.debug("Hello debug%n") + myLogger.trace("Hello trace%n") + } + + companion object { + @JvmStatic + fun main(args: Array) { + exitProcess(CommandLine(MyApp()).execute(*args)) + } + } +} +---- + ==== Accessing the Mixee from a Mixin Sometimes you need to write a Mixin class that accesses the mixee (the command where it is mixed in). @@ -6233,7 +6288,8 @@ By default, the `CommandSpec` of the enclosing class is injected into a `@Spec`- This can be useful when a mixin contains logic that is common to many commands. For example: -[source,java] +.Java +[source,java,role="primary"] ---- class AdvancedMixin { @Spec(Spec.Target.MIXEE) CommandSpec mixee; @@ -6257,6 +6313,35 @@ class AdvancedMixin { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +import java.util.function.IntConsumer +import java.util.function.IntSupplier +// ... + +class AdvancedMixin { + @Spec(Spec.Target.MIXEE) + lateinit var mixee: CommandSpec + + /** + * When the -x option is specified on any subcommand, + * multiply its value with another integer supplied by this subcommand + * and set the result on the top-level command. + * @param x the value of the -x option + */ + @Option(names = ["-x"]) + fun setValue(x: Int) { + // get another value from the command we are mixed into + val y = (mixee.userObject() as IntSupplier).asInt + + val product = x * y + + // set the result on the top-level (root) command + (mixee.root().userObject() as IntConsumer).accept(product) + } +} +---- ==== Accessing the Parent Command from a Mixin @@ -6264,7 +6349,8 @@ Since picocli 4.2, <> field This injects a reference to the parent command of the mixee into the mixin. Here is an example implementation: -[source,java] +.Java +[source,java,role="primary"] ---- // Define a mixin that delegates to the parent command. class MyMixin { @@ -6304,6 +6390,51 @@ class Sub implements Runnable{ } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +// Define a mixin that delegates to the parent command. +class MyMixin { + @ParentCommand + lateinit var top: Top + + @Option(names = ["-v", "--verbose"]) + fun setVerbose(verbose: Boolean) { top.verbose = verbose } +} + +// An example Top-level command with a subcommand. +// The `-v` option can be specified on the top-level command as well as its subcommands. +@Command(subcommands = [Sub::class]) +class Top : Runnable { + @Option(names = ["-v", "--verbose"]) + var verbose = false + + fun verbose(pattern: String, vararg params: Any) { + if (verbose) { + System.out.printf(pattern, *params) + } + } + + override fun run() { verbose("Hello from top%n") } + + companion object { + @JvmStatic + fun main(args: Array) { + CommandLine(Top()).execute(*args) + } + } +} + +// Subcommand classes can mix in the `-v` option with a @Mixin-annotated field. +@Command(name = "sub") +class Sub : Runnable { + @Mixin + lateinit var mymixin: MyMixin + + override fun run() { mymixin.top.verbose("Hello from sub%n") } +} +---- + With this, the `-v` option can be specified on the top-level command as well as its subcommands, so all of the below are valid invocations: ---- @@ -6317,17 +6448,28 @@ All of these invocations will print some output, since the `-v` option was speci ==== Adding Mixins Programmatically The below example shows how a mixin can be added programmatically with the `CommandLine.addMixin` method. -[source,java] +.Java +[source,java,role="primary"] ---- CommandLine commandLine = new CommandLine(new MyCommand()); ReusableOptions mixin = new ReusableOptions(); -commandline.addMixin("myMixin", mixin); +commandLine.addMixin("myMixin", mixin); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val commandLine = CommandLine(MyCommand()) + +val mixin = ReusableOptions() +commandLine.addMixin("myMixin", mixin) ---- Programmatically added mixins can be accessed via the map returned by `CommandLine.getMixins`. Continuing from the previous example: -[source,java] +.Java +[source,java,role="primary"] ---- commandLine.parseArgs("-vvv"); @@ -6336,7 +6478,15 @@ assert mixin == commandLine.getMixins().get("myMixin"); assert mixin.verbosity.length == 3; ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +commandLine.parseArgs("-vvv") +// the options defined in ReusableOptions have been added to the zip command +assert(mixin === commandLine.mixins["myMixin"]) +assert(mixin.verbosity.size == 3) +---- === Use Case: Configure Log Level with a Global Option From dd69d79a16dde6270bcd83f66e65c285abdb9d1d Mon Sep 17 00:00:00 2001 From: Andreas Deininger Date: Sun, 18 Oct 2020 09:22:56 +0200 Subject: [PATCH 4/6] Manual: add tabs with Kotlin code for samples (chapter 13: Version help) --- docs/index.adoc | 155 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 133 insertions(+), 22 deletions(-) diff --git a/docs/index.adoc b/docs/index.adoc index 2c3d2f6b4..cb2e2db73 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -3580,21 +3580,35 @@ was requested, and invoke `CommandLine::usage` or `CommandLine::printVersionHelp == Version Help === Static Version Information ==== Command `version` Attribute -Since v0.9.8, applications can specify version information in the `version` attribute of the `@Command` annotation. +As of picocli 0.9.8, applications can specify version information in the `version` attribute of the `@Command` annotation. -[source,java] +.Java +[source,java,role="primary"] ---- @Command(version = "1.0") class VersionedCommand { @Option(names = { "-V", "--version" }, versionHelp = true, description = "print version information and exit") boolean versionRequested; - ... + /* ... */ } +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Command(version = ["1.0"]) +class VersionedCommand { + @Option(names = ["-V", "--version"], versionHelp = true, + description = ["print version information and exit"]) + var versionRequested = false + /* ... */ } ---- The `CommandLine.printVersionHelp(PrintStream)` method extracts the version information from this annotation and prints it to the specified `PrintStream`. -[source,java] + +.Java +[source,java,role="primary"] ---- CommandLine commandLine = new CommandLine(new VersionedCommand()); commandLine.parseArgs(args); @@ -3604,14 +3618,34 @@ if (commandLine.isVersionHelpRequested()) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val commandLine = CommandLine(VersionedCommand()) +commandLine.parseArgs(*args) +if (commandLine.isVersionHelpRequested) { + commandLine.printVersionHelp(System.out) + return +} +---- + ==== Multi-line Version Info The `version` may specify multiple Strings. Each will be printed on a separate line. -[source,java] +.Java +[source,java,role="primary"] ---- @Command(version = { "Versioned Command 1.0", "Build 12345", "(c) 2017" }) -class VersionedCommand { ... } +class VersionedCommand { /* ... */ } ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Command(version = ["Versioned Command 1.0", "Build 12345", "(c) 2017"]) +class VersionedCommand { /* */ } +---- + The `CommandLine.printVersionHelp(PrintStream)` method will print the above as: ---- @@ -3621,16 +3655,30 @@ Build 12345 ---- ==== Version Info With Variables -Since 4.0, the version strings may contain <>. For example: -[source,java] +As of picocli 4.0, the version strings may contain <>. For example: + +.Java +[source,java,role="primary"] ---- @Command(version = { "Versioned Command 1.0", "Picocli " + picocli.CommandLine.VERSION, "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", "OS: ${os.name} ${os.version} ${os.arch}"}) -class VersionedCommand { ... } +class VersionedCommand { /* ... */ } +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Command(version = [ + "Versioned Command 1.0", + "Picocli " + picocli.CommandLine.VERSION, + "JVM: \${java.version} (\${java.vendor} \${java.vm.name} \${java.vm.version})", + "OS: \${os.name} \${os.version} \${os.arch}"]) +class VersionedCommand { /* */ } ---- + Depending on your environment, that may print something like: ---- @@ -3644,48 +3692,88 @@ OS: Linux 4.4.0-17134-Microsoft amd64 The version strings may contain <> to show ANSI styles and colors. For example: -[source,java] +.Java +[source,java,role="primary"] ---- @Command(version = { "@|yellow Versioned Command 1.0|@", "@|blue Build 12345|@", "@|red,bg(white) (c) 2017|@" }) -class VersionedCommand { ... } +class VersionedCommand { /* ... */ } ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Command(version = [ + "@|yellow Versioned Command 1.0|@", + "@|blue Build 12345|@", + "@|red,bg(white) (c) 2017|@"]) +class VersionedCommand { /* */ } +---- + The markup will be rendered as ANSI escape codes on supported systems. image:VersionInfoWithColors.png[Screenshot of version information containing markup with Ansi styles and colors] ==== Version Info With Format Specifiers -From picocli 1.0, the `version` may contain <>: +As of picocli 1.0, the `version` may contain <>: -[source,java] +.Java +[source,java,role="primary"] ---- @Command(version = { "Versioned Command 1.0", "Build %1$s", "(c) 2017, licensed to %2$s" }) -class VersionedCommand { ... } +class VersionedCommand { /* ... */ } +---- + +.Kotlin +[source,kotlin,role="secondary"] ---- +@Command(version = [ + "Versioned Command 1.0", + "Build %1\$s", + "(c) 2017, licensed to %2\$s"]) +class VersionedCommand { /* ... */ } +---- + Format argument values can be passed to the `printVersionHelp` method: -[source,java] +.Java +[source,java,role="primary"] ---- String[] args = {"1234", System.getProperty("user.name")}; new CommandLine(new VersionedCommand()) .printVersionHelp(System.out, Help.Ansi.AUTO, args); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val args = arrayOf("1234", System.getProperty("user.name")) +CommandLine(VersionedCommand()).printVersionHelp(System.out, Help.Ansi.AUTO, *args) +---- + === Dynamic Version Information ==== Command `versionProvider` Attribute -From picocli 2.2, the `@Command` annotation supports a `versionProvider` attribute. +As of picocli 2.2, the `@Command` annotation supports a `versionProvider` attribute. Applications may specify a `IVersionProvider` implementation in this attribute, and picocli will instantiate this class and invoke it to collect version information. -[source,java] +.Java +[source,java,role="primary"] ---- @Command(versionProvider = com.my.custom.ManifestVersionProvider.class) -class App { ... } +class App { /* ... */ } +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Command(versionProvider = com.my.custom.ManifestVersionProvider::class) +class App { /* ... */ } ---- This is useful when the version of an application should be detected dynamically at runtime. @@ -3718,14 +3806,24 @@ The version strings returned from the `IVersionProvider` may contain < { + return arrayOf("\${COMMAND-FULL-NAME} version 1.0") + } +} ---- The above example version provider will show the fully qualified command name (that is, preceded by its parent fully qualified command name) @@ -3735,14 +3833,15 @@ This is one way to create a version provider that can be reused across multiple ==== Injecting `CommandSpec` Into a `IVersionProvider` -From picocli 4.2.0, `IVersionProvider` implementations can have `@Spec`-annotated fields. If such a field +As of picocli 4.2.0, `IVersionProvider` implementations can have `@Spec`-annotated fields. If such a field exists, picocli will inject the `CommandSpec` of the command that uses this version provider. This gives the version provider access to the full command hierarchy, and may make it easier to implement version providers that can be reused among multiple commands. For example: -[source,java] +.Java +[source,java,role="primary"] ---- class MyVersionProvider implements IVersionProvider { @Spec CommandSpec spec; @@ -3753,6 +3852,18 @@ class MyVersionProvider implements IVersionProvider { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class MyVersionProvider : IVersionProvider { + @Spec + lateinit var spec: CommandSpec + + override fun getVersion(): Array { + return arrayOf("Version info for " + spec.qualifiedName()) + } +} +---- == Usage Help === Compact Example @@ -3915,7 +4026,7 @@ For commands with <>, the string `[COMMA Usage: [OPTIONS] FILES [COMMAND] ---- -From picocli 4.0, this can be customized with the `synopsisSubcommandLabel` attribute. +As of picocli 4.0, this can be customized with the `synopsisSubcommandLabel` attribute. For example, to clarify that a <>, an application may specify `COMMAND`, without the `[` and `]` brackets: From 4babb9b78f24b90a19e1310b1166a05b193cb8cf Mon Sep 17 00:00:00 2001 From: Andreas Deininger Date: Sun, 18 Oct 2020 11:52:38 +0200 Subject: [PATCH 5/6] User manual, chapter 4.2 add missing semicolon in Java code sample --- docs/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.adoc b/docs/index.adoc index cb2e2db73..959ab9e06 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -1130,7 +1130,7 @@ NOTE: Java 8 lambdas make it easy to register custom converters: [source,java] ---- -CommandLine cl = new CommandLine(app) +CommandLine cl = new CommandLine(app); cl.registerConverter(Locale.class, s -> new Locale.Builder().setLanguageTag(s).build()); cl.registerConverter(Cipher.class, s -> Cipher.getInstance(s)); ---- From 4a742c0d01126951ccfb4ce32cba0f2dbc3fc35c Mon Sep 17 00:00:00 2001 From: Andreas Deininger Date: Sun, 18 Oct 2020 12:51:11 +0200 Subject: [PATCH 6/6] Manual: add tabs with Kotlin code for samples (chapter 4: Strongly Typed Everything) --- docs/index.adoc | 225 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 202 insertions(+), 23 deletions(-) diff --git a/docs/index.adoc b/docs/index.adoc index 959ab9e06..09e464a6c 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -223,7 +223,7 @@ import java.util.concurrent.Callable class Checksum extends Callable[Int] { @Parameters(index = "0", description = Array("The file whose checksum to calculate.")) - private val file : File = null + private val file: File = null @Option(names = Array("-a", "--algorithm"), description = Array("MD5, SHA-1, SHA-256, ...")) private var algorithm = "MD5" @@ -438,16 +438,16 @@ class Tar { ---- class Tar : Runnable { @Option(names = ["-c"], description = ["create a new archive"]) - var create : Boolean = false; + var create: Boolean = false; @Option(names = ["-f", "--file"], paramLabel = "ARCHIVE", description = ["the archive file"]) - lateinit var archive : File; + lateinit var archive: File; @Parameters(paramLabel = "FILE", description = ["one ore more files to archive"]) - lateinit var files : Array; + lateinit var files: Array; @Option(names = ["-h", "--help"], usageHelp = true, description = ["display a help message"]) - private var helpRequested : Boolean = false; + private var helpRequested: Boolean = false; } ---- @@ -1128,16 +1128,30 @@ Custom type converters can be registered with the `CommandLine.registerConverter NOTE: Java 8 lambdas make it easy to register custom converters: -[source,java] +.Java +[source,java,role="primary"] ---- CommandLine cl = new CommandLine(app); cl.registerConverter(Locale.class, s -> new Locale.Builder().setLanguageTag(s).build()); cl.registerConverter(Cipher.class, s -> Cipher.getInstance(s)); ---- +.Kotlin +[source,java,role="secondary"] +---- +val cl = CommandLine(app) +cl.registerConverter(Locale::class.java) { + s: String? -> Locale.Builder().setLanguageTag(s).build() +} +cl.registerConverter(Cipher::class.java) { + Cipher.getInstance(it) +} +---- + After registering custom converters, call the `execute(String...)` or `parseArgs(String...)` method on the `CommandLine` instance where the converters are registered. (The static `populateCommand` method cannot be used.) For example: -[source,java] +.Java +[source,java,role="primary"] ---- class App { @Parameters java.util.Locale locale; @@ -1145,7 +1159,24 @@ class App { } ---- -[source,java] +.Kotlin +[source,kotlin,role="secondary"] +---- +import java.util.Locale +import javax.crypto.Cipher +// ... + +class App { + @Parameters + lateinit var locale: Locale + + @Option(names = ["-a"]) + lateinit var cipher: Cipher +} +---- + +.Java +[source,kotlin,role="primary"] ---- App app = new App(); CommandLine commandLine = new CommandLine(app) @@ -1157,6 +1188,23 @@ assert app.locale.toLanguageTag().equals("en-GB"); assert app.cipher.getAlgorithm().equals("AES/CBC/NoPadding"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val app = App() +val commandLine = CommandLine(app) + .registerConverter(Locale::class.java) { + s: String? -> Locale.Builder().setLanguageTag(s).build() + } + .registerConverter(Cipher::class.java) { + Cipher.getInstance(it) + } + +commandLine.parseArgs("-a", "AES/CBC/NoPadding", "en-GB") +assert(app.locale.toLanguageTag() == "en-GB") +assert(app.cipher.algorithm == "AES/CBC/NoPadding") +---- + CAUTION: _Note on subcommands:_ the specified converter will be registered with the `CommandLine` object and all subcommands (and nested sub-subcommands) that were added _before_ the converter was registered. Subcommands added later will not have the converter added automatically. @@ -1181,7 +1229,8 @@ Invalid value for option '--socket-address': Invalid format: must be 'host:port' Below is an example custom converter that throws a `TypeConversionException`: -[source,java] +.Java +[source,java,role="primary"] ---- import java.net.InetSocketAddress; @@ -1191,7 +1240,7 @@ class InetSocketAddressConverter implements ITypeConverter { int pos = value.lastIndexOf(':'); if (pos < 0) { throw new TypeConversionException( - "Invalid format: must be 'host:port' but was '" + value + "'"); + "Invalid format: must be 'host:port' but was '" + value + "'"); } String adr = value.substring(0, pos); int port = Integer.parseInt(value.substring(pos + 1)); @@ -1200,6 +1249,26 @@ class InetSocketAddressConverter implements ITypeConverter { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +import java.net.InetSocketAddress +// ... + +class InetSocketAddressConverter : ITypeConverter { + override fun convert(value: String): InetSocketAddress { + val pos = value.lastIndexOf(':') + if (pos < 0) { + throw CommandLine.TypeConversionException( + "Invalid format: must be 'host:port' but was '$value'") + } + val adr = value.substring(0, pos) + val port = value.substring(pos + 1).toInt() + return InetSocketAddress(adr, port) + } +} +---- + The `picocli-examples` module on GitHub has a minimal working https://github.com/remkop/picocli/blob/master/picocli-examples/src/main/java/picocli/examples/typeconverter/InetSocketAddressConverterDemo.java[example] which you can run in our https://www.jdoodle.com/embed/v0/2mxo?stdin=1&arg=1[online-editor^]. === Option-specific Type Converters @@ -1209,7 +1278,8 @@ For example, for a specific field you may want to use a converter that maps the Example usage: -[source,java] +.Java +[source,java,role="primary"] ---- class App { @Option(names = "--sqlType", converter = SqlTypeConverter.class) @@ -1217,8 +1287,19 @@ class App { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class App { + @Option(names = ["--sqlType"], converter = [SqlTypeConverter::class]) + var sqlType = 0 +} +---- + Example implementation: -[source,java] + +.Java +[source,java,role="primary"] ---- class SqlTypeConverter implements ITypeConverter { public Integer convert(String value) throws Exception { @@ -1228,7 +1309,25 @@ class SqlTypeConverter implements ITypeConverter { case "BINARY" : return Types.BINARY; case "BIT" : return Types.BIT; case "BLOB" : return Types.BLOB; - ... + // ... + } + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +class SqlTypeConverter : ITypeConverter { + @Throws(Exception::class) + override fun convert(value: String): Int { + when (value) { + "ARRAY" -> return Types.ARRAY + "BIGINT" -> return Types.BIGINT + "BINARY" -> return Types.BINARY + "BIT" -> return Types.BIT + "BLOB" -> return Types.BLOB + // ... } } } @@ -1249,7 +1348,8 @@ Multiple parameters can be captured together in a single array or `Collection` f The array or collection elements can be any type for which a <> is registered. For example: -[source,java] +.Java +[source,java,role="primary"] ---- import java.util.regex.Pattern; import java.io.File; @@ -1263,7 +1363,24 @@ class Convert { } ---- -[source,java] +.Kotlin +[source,kotlin,role="secondary"] +---- +import java.io.File +import java.util.regex.Pattern +// ... + +class Convert { + @Option(names = ["-patterns"], description = ["the regex patterns to use"]) + lateinit var patterns: Array + + @Parameters( /* type = [File::class], */ description = ["the files to convert"]) + lateinit var files: List // picocli infers type from the generic type +} +---- + +.Java +[source,java,role="primary"] ---- String[] args = { "-patterns", "a*b", "-patterns", "[a-e][i-u]", "file1.txt", "file2.txt" }; Convert convert = CommandLine.populateCommand(new Convert(), args); @@ -1272,6 +1389,16 @@ Convert convert = CommandLine.populateCommand(new Convert(), args); // convert.files now has two File objects ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val args = arrayOf("-patterns", "a*b", "-patterns", "[a-e][i-u]", "file1.txt", "file2.txt") +val convert = CommandLine.populateCommand(Convert(), *args) + +// convert.patterns now has two Pattern objects +// convert.files now has two File objects +---- + NOTE: If a collection is returned from a type converter, the _contents_ of the collection are added to the field or method parameter, not the collection itself. If the field or method parameter is `null`, picocli will instantiate it when the option or positional parameter is successfully matched. @@ -1290,7 +1417,8 @@ as long as a <> is registered for both the Key and value types are inferred from the map's generic type parameters. For example: -[source,java] +.Java +[source,java,role="primary"] ---- import java.net.InetAddress; import java.net.Proxy.Type; @@ -1304,6 +1432,24 @@ class MapDemo { Map timeout; } ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +import java.net.InetAddress +import java.net.Proxy +import java.util.concurrent.TimeUnit +// ... + +class MapDemo { + @Option(names = ["-p", "--proxyHost"]) + lateinit var proxies: Map + + @Option(names = ["-u", "--timeUnit"]) + lateinit var timeout: Map +} +---- + Map options may be specified multiple times with different key-value pairs. (See <>.) [source,bash] @@ -1325,7 +1471,8 @@ The field's type can be an interface or an abstract class. The `type` attribute can be used to control for each field what concrete class the string value should be converted to. For example: -[source,java] +.Java +[source,java,role="primary"] ---- class App { @Option(names = "--big", type = BigDecimal.class) // concrete Number subclass @@ -1339,11 +1486,28 @@ class App { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class App { + @Option(names = ["--big"], type = [BigDecimal::class]) // concrete Number subclass + lateinit var big: Array // array type with abstract component class + + @Option(names = ["--small"], type = [Short::class]) // other Number subclass + lateinit var small: Array + + @Parameters(type = [StringBuilder::class]) // StringBuilder implements CharSequence + lateinit var address: CharSequence // interface type +} +---- + ==== Maps and Collections with Abstract Elements For raw maps and collections, or when using generics with unbounded wildcards like `Map`, or when the type parameters are themselves abstract classes like `List` or `Map`, there is not enough information to convert to a stronger type. By default, the raw String values are added as is to such collections. The `type` attribute can be specified to convert to a stronger type than String. For example: -[source,java] + +.Java +[source,java,role="primary"] ---- class TypeDemo { @Option(names = "-x") // not enough information to convert @@ -1357,11 +1521,26 @@ class TypeDemo { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class TypeDemo { + @Option(names = ["-x"]) // not enough information to convert + lateinit var weaklyTyped: Map<*, *> // String keys and values are added as is + + @Option(names = ["-y"], type = [Short::class, BigDecimal::class]) + lateinit var stronglyTyped: Map + + @Option(names = ["-s"], type = [CharBuffer::class]) + lateinit var text: List +} +---- + === Enum Types It is encouraged to use `enum` types for options or positional parameters with a limited set of valid values. Not only will picocli validate the input, it allows you to <> in the usage help message with `@Option(description = "Valid values: ${COMPLETION-CANDIDATES}")`. It also allows command line completion to suggest completion candidates for the values of this option. -Enum value matching is case-sensitive by default, but from 3.4 this can be controlled with `CommandLine::setCaseInsensitiveEnumValuesAllowed` and `CommandSpec::caseInsensitiveEnumValuesAllowed`. +Enum value matching is case-sensitive by default, but as of picocli 3.4 this can be controlled with `CommandLine::setCaseInsensitiveEnumValuesAllowed` and `CommandSpec::caseInsensitiveEnumValuesAllowed`. == Default Values It is possible to define a default value for an option or positional parameter, that is assigned when the user did not specify this option or positional parameter on the command line. @@ -5162,7 +5341,7 @@ class Foo : Callable { override fun call(): Int { println("hi from foo, x=$x") - var ok : Boolean = true + var ok: Boolean = true return if (ok) 0 else 1 // exit code } } @@ -5179,7 +5358,7 @@ class Bar : Callable { } @Command(name = "baz", description = ["I'm a subcommand of `bar`"]) - fun baz( @Option(names = ["-z"]) z : Int) : Int { + fun baz( @Option(names = ["-z"]) z: Int) : Int { println("hi from baz, z=$z") return 45 } @@ -5944,7 +6123,7 @@ class Git { class Git : Runnable { @Option(names = ["--git-dir"], description = ["Set the path to the repository."]) - lateinit var gitDir : File + lateinit var gitDir: File } ---- @@ -6718,7 +6897,7 @@ import picocli.CommandLine.Model.CommandSpec class LoggingMixin { @Spec(Spec.Target.MIXEE) - private lateinit var mixee : CommandSpec// spec of the command where the @Mixin is used + private lateinit var mixee: CommandSpec// spec of the command where the @Mixin is used var verbosity = BooleanArray(0) /**