Updated kotlinx.cli to version 0.3.1

This commit is contained in:
Elena Lepilkina
2020-11-25 11:20:40 +03:00
committed by Stanislav Erokhin
parent 2d30796483
commit cb642bca69
11 changed files with 247 additions and 82 deletions
@@ -143,12 +143,12 @@ open class ArgParser(
/** /**
* Used prefix form for full option form. * Used prefix form for full option form.
*/ */
private val optionFullFormPrefix = if (prefixStyle == OptionPrefixStyle.JVM) "-" else "--" protected val optionFullFormPrefix = if (prefixStyle == OptionPrefixStyle.JVM) "-" else "--"
/** /**
* Used prefix form for short option form. * Used prefix form for short option form.
*/ */
private val optionShortFromPrefix = "-" protected val optionShortFromPrefix = "-"
/** /**
* Name with all commands that should be executed. * Name with all commands that should be executed.
@@ -160,6 +160,21 @@ open class ArgParser(
*/ */
protected var treatAsOption = true protected var treatAsOption = true
/**
* Arguments which should be parsed with subcommands.
*/
private val subcommandsArguments = mutableListOf<String>()
/**
* Options which should be parsed with subcommands.
*/
private val subcommandsOptions = mutableListOf<String>()
/**
* Subcommand used in commmand line arguments.
*/
private var usedSubcommand: Subcommand? = null
/** /**
* The way an option/argument has got its value. * The way an option/argument has got its value.
*/ */
@@ -229,7 +244,7 @@ open class ArgParser(
if (prefixStyle == OptionPrefixStyle.GNU && shortName != null) if (prefixStyle == OptionPrefixStyle.GNU && shortName != null)
require(shortName.length == 1) { require(shortName.length == 1) {
""" """
GNU standart for options allow to use short form whuch consists of one character. GNU standart for options allow to use short form which consists of one character.
For more information, please, see https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html For more information, please, see https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
""".trimIndent() """.trimIndent()
} }
@@ -359,7 +374,9 @@ open class ArgParser(
*/ */
private fun treatAsArgument(arg: String, argumentsQueue: ArgumentsQueue) { private fun treatAsArgument(arg: String, argumentsQueue: ArgumentsQueue) {
if (!saveAsArg(arg, argumentsQueue)) { if (!saveAsArg(arg, argumentsQueue)) {
printError("Too many arguments! Couldn't process argument $arg!") usedSubcommand?.let {
(if (treatAsOption) subcommandsOptions else subcommandsArguments).add(arg)
} ?: printError("Too many arguments! Couldn't process argument $arg!")
} }
} }
@@ -416,9 +433,12 @@ open class ArgParser(
* *
* @param argValue argument value with all information about option. * @param argValue argument value with all information about option.
*/ */
private fun saveOptionWithoutParameter(argValue: ParsingValue<*, *>) { internal fun saveOptionWithoutParameter(argValue: ParsingValue<*, *>) {
// Boolean flags. // Boolean flags.
if (argValue.descriptor.fullName == "help") { if (argValue.descriptor.fullName == "help") {
usedSubcommand?.let {
it.parse(listOf("${it.optionFullFormPrefix}${argValue.descriptor.fullName}"))
}
println(makeUsage()) println(makeUsage())
exitProcess(0) exitProcess(0)
} }
@@ -569,43 +589,42 @@ open class ArgParser(
} }
val argumentsQueue = ArgumentsQueue(arguments.map { it.value.descriptor as ArgDescriptor<*, *> }) val argumentsQueue = ArgumentsQueue(arguments.map { it.value.descriptor as ArgDescriptor<*, *> })
usedSubcommand = null
subcommandsOptions.clear()
subcommandsArguments.clear()
val argIterator = args.listIterator() val argIterator = args.listIterator()
try { try {
while (argIterator.hasNext()) { while (argIterator.hasNext()) {
val arg = argIterator.next() val arg = argIterator.next()
// Check for subcommands. // Check for subcommands.
@OptIn(ExperimentalCli::class) if (arg !in subcommands) {
subcommands.forEach { (name, subcommand) -> // Parse arguments from command line.
if (arg == name) { if (treatAsOption && arg.startsWith('-')) {
// Use parser for this subcommand. // Candidate in being option.
subcommand.parse(args.slice(argIterator.nextIndex() until args.size)) // Option is found.
subcommand.execute() if (!(recognizeAndSaveOptionShortForm(arg, argIterator) ||
parsingState = ArgParserResult(name) recognizeAndSaveOptionFullForm(arg, argIterator))
) {
return parsingState!! // State is changed so next options are arguments.
} if (!treatAsOption) {
} // Argument is found.
// Parse arguments from command line. treatAsArgument(argIterator.next(), argumentsQueue)
if (treatAsOption && arg.startsWith('-')) { } else {
// Candidate in being option. usedSubcommand?.let { subcommandsOptions.add(arg) } ?: run {
// Option is found. // Try save as argument.
if (!(recognizeAndSaveOptionShortForm(arg, argIterator) || if (!saveAsArg(arg, argumentsQueue)) {
recognizeAndSaveOptionFullForm(arg, argIterator))) { printError("Unknown option $arg")
// State is changed so next options are arguments. }
if (!treatAsOption) { }
// Argument is found.
treatAsArgument(argIterator.next(), argumentsQueue)
} else {
// Try save as argument.
if (!saveAsArg(arg, argumentsQueue)) {
printError("Unknown option $arg")
} }
} }
} else {
// Argument is found.
treatAsArgument(arg, argumentsQueue)
} }
} else { } else {
// Argument is found. usedSubcommand = subcommands[arg]
treatAsArgument(arg, argumentsQueue)
} }
} }
// Postprocess results of parsing. // Postprocess results of parsing.
@@ -618,6 +637,14 @@ open class ArgParser(
printError("Value for ${value.descriptor.textDescription} should be always provided in command line.") printError("Value for ${value.descriptor.textDescription} should be always provided in command line.")
} }
} }
// Parse arguments for subcommand.
usedSubcommand?.let {
it.parse(subcommandsOptions + listOfNotNull("--".takeUnless { treatAsOption }) + subcommandsArguments)
it.execute()
parsingState = ArgParserResult(it.name)
return parsingState!!
}
} catch (exception: ParsingException) { } catch (exception: ParsingException) {
printError(exception.message!!) printError(exception.message!!)
} }
@@ -70,16 +70,45 @@ abstract class ArgType<T : Any>(val hasParameter: kotlin.Boolean) {
?: throw ParsingException("Option $name is expected to be double number. $value is provided.") ?: throw ParsingException("Option $name is expected to be double number. $value is provided.")
} }
companion object {
/**
* Helper for arguments that have limited set of possible values represented as enumeration constants.
*/
inline fun <reified T: Enum<T>> Choice(
noinline toVariant: (kotlin.String) -> T = {
enumValues<T>().find { e -> e.toString().equals(it, ignoreCase = true) } ?:
throw IllegalArgumentException("No enum constant $it")
},
noinline toString: (T) -> kotlin.String = { it.toString().toLowerCase() }): Choice<T> {
return Choice(enumValues<T>().toList(), toVariant, toString)
}
}
/** /**
* Type for arguments that have limited set of possible values. * Type for arguments that have limited set of possible values.
*/ */
class Choice(val values: List<kotlin.String>) : ArgType<kotlin.String>(true) { class Choice<T: Any>(choices: List<T>,
override val description: kotlin.String val toVariant: (kotlin.String) -> T,
get() = "{ Value should be one of $values }" val variantToString: (T) -> kotlin.String = { it.toString() }): ArgType<T>(true) {
private val choicesMap: Map<kotlin.String, T> = choices.associateBy { variantToString(it) }
override fun convert(value: kotlin.String, name: kotlin.String): kotlin.String = init {
if (value in values) value require(choicesMap.size == choices.size) {
else throw ParsingException("Option $name is expected to be one of $values. $value is provided.") "Command line representations of enum choices are not distinct"
}
}
override val description: kotlin.String
get() {
return "{ Value should be one of ${choicesMap.keys} }"
}
override fun convert(value: kotlin.String, name: kotlin.String) =
try {
toVariant(value)
} catch (e: Exception) {
throw ParsingException("Option $name is expected to be one of ${choicesMap.keys}. $value is provided.")
}
} }
} }
@@ -159,7 +159,7 @@ class MultipleArgument<T : Any, DefaultRequired: DefaultRequiredType> internal c
fun <T : Any, TResult, DefaultRequired: DefaultRequiredType> fun <T : Any, TResult, DefaultRequired: DefaultRequiredType>
AbstractSingleArgument<T, TResult, DefaultRequired>.multiple(number: Int): MultipleArgument<T, DefaultRequired> { AbstractSingleArgument<T, TResult, DefaultRequired>.multiple(number: Int): MultipleArgument<T, DefaultRequired> {
require(number >= 2) { "multiple() modifier with value less than 2 is unavailable. It's already set to 1." } require(number >= 2) { "multiple() modifier with value less than 2 is unavailable. It's already set to 1." }
val newArgument = with(delegate.cast<ParsingValue<T, T>>().descriptor as ArgDescriptor) { val newArgument = with((delegate.cast<ParsingValue<T, T>>()).descriptor as ArgDescriptor) {
MultipleArgument<T, DefaultRequired>(ArgDescriptor(type, fullName, number, description, listOfNotNull(defaultValue), MultipleArgument<T, DefaultRequired>(ArgDescriptor(type, fullName, number, description, listOfNotNull(defaultValue),
required, deprecatedWarning), owner) required, deprecatedWarning), owner)
} }
@@ -172,7 +172,7 @@ fun <T : Any, TResult, DefaultRequired: DefaultRequiredType>
*/ */
fun <T : Any, TResult, DefaultRequired: DefaultRequiredType> AbstractSingleArgument<T, TResult, DefaultRequired>.vararg(): fun <T : Any, TResult, DefaultRequired: DefaultRequiredType> AbstractSingleArgument<T, TResult, DefaultRequired>.vararg():
MultipleArgument<T, DefaultRequired> { MultipleArgument<T, DefaultRequired> {
val newArgument = with(delegate.cast<ParsingValue<T, T>>().descriptor as ArgDescriptor) { val newArgument = with((delegate.cast<ParsingValue<T, T>>()).descriptor as ArgDescriptor) {
MultipleArgument<T, DefaultRequired>(ArgDescriptor(type, fullName, null, description, listOfNotNull(defaultValue), MultipleArgument<T, DefaultRequired>(ArgDescriptor(type, fullName, null, description, listOfNotNull(defaultValue),
required, deprecatedWarning), owner) required, deprecatedWarning), owner)
} }
@@ -189,7 +189,7 @@ fun <T : Any, TResult, DefaultRequired: DefaultRequiredType> AbstractSingleArgum
* @param value the default value. * @param value the default value.
*/ */
fun <T: Any> SingleNullableArgument<T>.default(value: T): SingleArgument<T, DefaultRequiredType.Default> { fun <T: Any> SingleNullableArgument<T>.default(value: T): SingleArgument<T, DefaultRequiredType.Default> {
val newArgument = with(delegate.cast<ParsingValue<T, T>>().descriptor as ArgDescriptor) { val newArgument = with((delegate.cast<ParsingValue<T, T>>()).descriptor as ArgDescriptor) {
SingleArgument<T, DefaultRequiredType.Default>(ArgDescriptor(type, fullName, number, description, value, SingleArgument<T, DefaultRequiredType.Default>(ArgDescriptor(type, fullName, number, description, value,
false, deprecatedWarning), owner) false, deprecatedWarning), owner)
} }
@@ -208,7 +208,7 @@ fun <T: Any> SingleNullableArgument<T>.default(value: T): SingleArgument<T, Defa
fun <T: Any> MultipleArgument<T, DefaultRequiredType.None>.default(value: Collection<T>): fun <T: Any> MultipleArgument<T, DefaultRequiredType.None>.default(value: Collection<T>):
MultipleArgument<T, DefaultRequiredType.Default> { MultipleArgument<T, DefaultRequiredType.Default> {
require (value.isNotEmpty()) { "Default value for argument can't be empty collection." } require (value.isNotEmpty()) { "Default value for argument can't be empty collection." }
val newArgument = with(delegate.cast<ParsingValue<T, List<T>>>().descriptor as ArgDescriptor) { val newArgument = with((delegate.cast<ParsingValue<T, List<T>>>()).descriptor as ArgDescriptor) {
MultipleArgument<T, DefaultRequiredType.Default>(ArgDescriptor(type, fullName, number, description, value.toList(), MultipleArgument<T, DefaultRequiredType.Default>(ArgDescriptor(type, fullName, number, description, value.toList(),
required, deprecatedWarning), owner) required, deprecatedWarning), owner)
} }
@@ -224,7 +224,7 @@ fun <T: Any> MultipleArgument<T, DefaultRequiredType.None>.default(value: Collec
* Note that only trailing arguments can be optional, i.e. no required arguments can follow optional ones. * Note that only trailing arguments can be optional, i.e. no required arguments can follow optional ones.
*/ */
fun <T: Any> SingleArgument<T, DefaultRequiredType.Required>.optional(): SingleNullableArgument<T> { fun <T: Any> SingleArgument<T, DefaultRequiredType.Required>.optional(): SingleNullableArgument<T> {
val newArgument = with(delegate.cast<ParsingValue<T, T>>().descriptor as ArgDescriptor) { val newArgument = with((delegate.cast<ParsingValue<T, T>>()).descriptor as ArgDescriptor) {
SingleNullableArgument(ArgDescriptor(type, fullName, number, description, defaultValue, SingleNullableArgument(ArgDescriptor(type, fullName, number, description, defaultValue,
false, deprecatedWarning), owner) false, deprecatedWarning), owner)
} }
@@ -240,7 +240,7 @@ fun <T: Any> SingleArgument<T, DefaultRequiredType.Required>.optional(): SingleN
* Note that only trailing arguments can be optional: no required arguments can follow the optional ones. * Note that only trailing arguments can be optional: no required arguments can follow the optional ones.
*/ */
fun <T: Any> MultipleArgument<T, DefaultRequiredType.Required>.optional(): MultipleArgument<T, DefaultRequiredType.None> { fun <T: Any> MultipleArgument<T, DefaultRequiredType.Required>.optional(): MultipleArgument<T, DefaultRequiredType.None> {
val newArgument = with(delegate.cast<ParsingValue<T, List<T>>>().descriptor as ArgDescriptor) { val newArgument = with((delegate.cast<ParsingValue<T, List<T>>>()).descriptor as ArgDescriptor) {
MultipleArgument<T, DefaultRequiredType.None>(ArgDescriptor(type, fullName, number, description, MultipleArgument<T, DefaultRequiredType.None>(ArgDescriptor(type, fullName, number, description,
defaultValue?.toList() ?: listOf(), false, deprecatedWarning), owner) defaultValue?.toList() ?: listOf(), false, deprecatedWarning), owner)
} }
@@ -17,7 +17,7 @@ import kotlin.annotation.AnnotationTarget.*
* annotating that usage with the [UseExperimental] annotation, e.g. `@UseExperimental(ExperimentalCli::class)`, * annotating that usage with the [UseExperimental] annotation, e.g. `@UseExperimental(ExperimentalCli::class)`,
* or by using the compiler argument `-Xuse-experimental=kotlinx.cli.ExperimentalCli`. * or by using the compiler argument `-Xuse-experimental=kotlinx.cli.ExperimentalCli`.
*/ */
@RequiresOptIn("This API is experimental. It may be changed in the future without notice.", level = RequiresOptIn.Level.WARNING) @RequiresOptIn("This API is experimental. It may be changed in the future without notice.", RequiresOptIn.Level.WARNING)
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
@Target( @Target(
CLASS, CLASS,
@@ -103,7 +103,7 @@ class MultipleOption<T : Any, OptionType : MultipleOptionType, DefaultType: Defa
*/ */
fun <T : Any, TResult, DefaultType: DefaultRequiredType> AbstractSingleOption<T, TResult, DefaultType>.multiple(): fun <T : Any, TResult, DefaultType: DefaultRequiredType> AbstractSingleOption<T, TResult, DefaultType>.multiple():
MultipleOption<T, MultipleOptionType.Repeated, DefaultType> { MultipleOption<T, MultipleOptionType.Repeated, DefaultType> {
val newOption = with(delegate.cast<ParsingValue<T, T>>().descriptor as OptionDescriptor) { val newOption = with((delegate.cast<ParsingValue<T, T>>()).descriptor as OptionDescriptor) {
MultipleOption<T, MultipleOptionType.Repeated, DefaultType>( MultipleOption<T, MultipleOptionType.Repeated, DefaultType>(
OptionDescriptor( OptionDescriptor(
optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
@@ -122,7 +122,7 @@ fun <T : Any, TResult, DefaultType: DefaultRequiredType> AbstractSingleOption<T,
*/ */
fun <T : Any, DefaultType: DefaultRequiredType> MultipleOption<T, MultipleOptionType.Delimited, DefaultType>.multiple(): fun <T : Any, DefaultType: DefaultRequiredType> MultipleOption<T, MultipleOptionType.Delimited, DefaultType>.multiple():
MultipleOption<T, MultipleOptionType.RepeatedDelimited, DefaultRequiredType> { MultipleOption<T, MultipleOptionType.RepeatedDelimited, DefaultRequiredType> {
val newOption = with(delegate.cast<ParsingValue<T, List<T>>>().descriptor as OptionDescriptor) { val newOption = with((delegate.cast<ParsingValue<T, List<T>>>()).descriptor as OptionDescriptor) {
if (multiple) { if (multiple) {
error("Try to use modifier multiple() twice on option ${fullName ?: ""}") error("Try to use modifier multiple() twice on option ${fullName ?: ""}")
} }
@@ -145,7 +145,7 @@ fun <T : Any, DefaultType: DefaultRequiredType> MultipleOption<T, MultipleOption
* @param value the default value. * @param value the default value.
*/ */
fun <T : Any> SingleNullableOption<T>.default(value: T): SingleOption<T, DefaultRequiredType.Default> { fun <T : Any> SingleNullableOption<T>.default(value: T): SingleOption<T, DefaultRequiredType.Default> {
val newOption = with(delegate.cast<ParsingValue<T, T>>().descriptor as OptionDescriptor) { val newOption = with((delegate.cast<ParsingValue<T, T>>()).descriptor as OptionDescriptor) {
SingleOption<T, DefaultRequiredType.Default>( SingleOption<T, DefaultRequiredType.Default>(
OptionDescriptor( OptionDescriptor(
optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
@@ -167,7 +167,7 @@ fun <T : Any> SingleNullableOption<T>.default(value: T): SingleOption<T, Default
fun <T : Any, OptionType : MultipleOptionType> fun <T : Any, OptionType : MultipleOptionType>
MultipleOption<T, OptionType, DefaultRequiredType.None>.default(value: Collection<T>): MultipleOption<T, OptionType, DefaultRequiredType.None>.default(value: Collection<T>):
MultipleOption<T, OptionType, DefaultRequiredType.Default> { MultipleOption<T, OptionType, DefaultRequiredType.Default> {
val newOption = with(delegate.cast<ParsingValue<T, List<T>>>().descriptor as OptionDescriptor) { val newOption = with((delegate.cast<ParsingValue<T, List<T>>>()).descriptor as OptionDescriptor) {
require(value.isNotEmpty()) { "Default value for option can't be empty collection." } require(value.isNotEmpty()) { "Default value for option can't be empty collection." }
MultipleOption<T, OptionType, DefaultRequiredType.Default>( MultipleOption<T, OptionType, DefaultRequiredType.Default>(
OptionDescriptor( OptionDescriptor(
@@ -185,7 +185,7 @@ fun <T : Any, OptionType : MultipleOptionType>
* Requires the option to be always provided in command line. * Requires the option to be always provided in command line.
*/ */
fun <T : Any> SingleNullableOption<T>.required(): SingleOption<T, DefaultRequiredType.Required> { fun <T : Any> SingleNullableOption<T>.required(): SingleOption<T, DefaultRequiredType.Required> {
val newOption = with(delegate.cast<ParsingValue<T, T>>().descriptor as OptionDescriptor) { val newOption = with((delegate.cast<ParsingValue<T, T>>()).descriptor as OptionDescriptor) {
SingleOption<T, DefaultRequiredType.Required>( SingleOption<T, DefaultRequiredType.Required>(
OptionDescriptor( OptionDescriptor(
optionFullFormPrefix, optionShortFromPrefix, type, fullName, optionFullFormPrefix, optionShortFromPrefix, type, fullName,
@@ -204,7 +204,7 @@ fun <T : Any> SingleNullableOption<T>.required(): SingleOption<T, DefaultRequire
fun <T : Any, OptionType : MultipleOptionType> fun <T : Any, OptionType : MultipleOptionType>
MultipleOption<T, OptionType, DefaultRequiredType.None>.required(): MultipleOption<T, OptionType, DefaultRequiredType.None>.required():
MultipleOption<T, OptionType, DefaultRequiredType.Required> { MultipleOption<T, OptionType, DefaultRequiredType.Required> {
val newOption = with(delegate.cast<ParsingValue<T, List<T>>>().descriptor as OptionDescriptor) { val newOption = with((delegate.cast<ParsingValue<T, List<T>>>()).descriptor as OptionDescriptor) {
MultipleOption<T, OptionType, DefaultRequiredType.Required>( MultipleOption<T, OptionType, DefaultRequiredType.Required>(
OptionDescriptor( OptionDescriptor(
optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
@@ -228,7 +228,7 @@ fun <T : Any, OptionType : MultipleOptionType>
fun <T : Any, DefaultRequired: DefaultRequiredType> AbstractSingleOption<T, *, DefaultRequired>.delimiter( fun <T : Any, DefaultRequired: DefaultRequiredType> AbstractSingleOption<T, *, DefaultRequired>.delimiter(
delimiterValue: String): delimiterValue: String):
MultipleOption<T, MultipleOptionType.Delimited, DefaultRequired> { MultipleOption<T, MultipleOptionType.Delimited, DefaultRequired> {
val newOption = with(delegate.cast<ParsingValue<T, T>>().descriptor as OptionDescriptor) { val newOption = with((delegate.cast<ParsingValue<T, T>>()).descriptor as OptionDescriptor) {
MultipleOption<T, MultipleOptionType.Delimited, DefaultRequired>( MultipleOption<T, MultipleOptionType.Delimited, DefaultRequired>(
OptionDescriptor( OptionDescriptor(
optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
@@ -252,7 +252,7 @@ fun <T : Any, DefaultRequired: DefaultRequiredType> AbstractSingleOption<T, *, D
fun <T : Any, DefaultRequired: DefaultRequiredType> MultipleOption<T, MultipleOptionType.Repeated, DefaultRequired>.delimiter( fun <T : Any, DefaultRequired: DefaultRequiredType> MultipleOption<T, MultipleOptionType.Repeated, DefaultRequired>.delimiter(
delimiterValue: String): delimiterValue: String):
MultipleOption<T, MultipleOptionType.RepeatedDelimited, DefaultRequired> { MultipleOption<T, MultipleOptionType.RepeatedDelimited, DefaultRequired> {
val newOption = with(delegate.cast<ParsingValue<T, List<T>>>().descriptor as OptionDescriptor) { val newOption = with((delegate.cast<ParsingValue<T, List<T>>>()).descriptor as OptionDescriptor) {
MultipleOption<T, MultipleOptionType.RepeatedDelimited, DefaultRequired>( MultipleOption<T, MultipleOptionType.RepeatedDelimited, DefaultRequired>(
OptionDescriptor( OptionDescriptor(
optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
@@ -3,6 +3,8 @@
* that can be found in the LICENSE file. * that can be found in the LICENSE file.
*/ */
@file:Suppress("UNUSED_VARIABLE")
package kotlinx.cli package kotlinx.cli
import kotlinx.cli.ArgParser import kotlinx.cli.ArgParser
@@ -0,0 +1,9 @@
package kotlinx.cli
enum class DataSourceEnum {
LOCAL,
STAGING,
PRODUCTION;
override fun toString(): String = name.toLowerCase()
}
@@ -3,10 +3,10 @@
* that can be found in the LICENSE file. * that can be found in the LICENSE file.
*/ */
@file:Suppress("UNUSED_VARIABLE")
package kotlinx.cli package kotlinx.cli
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlin.test.* import kotlin.test.*
class ErrorTests { class ErrorTests {
@@ -42,15 +42,31 @@ class ErrorTests {
assertTrue("Option number is expected to be integer number. out.txt is provided." in exception.message!!) assertTrue("Option number is expected to be integer number. out.txt is provided." in exception.message!!)
} }
enum class RenderEnum {
TEXT,
HTML;
}
@Test @Test
fun testWrongChoice() { fun testWrongChoice() {
val argParser = ArgParser("testParser") val argParser = ArgParser("testParser")
val useShortForm by argParser.option(ArgType.Boolean, "short", "s", "Show short version of report").default(false) val useShortForm by argParser.option(ArgType.Boolean, "short", "s", "Show short version of report").default(false)
val renders by argParser.option(ArgType.Choice(listOf("text", "html")), val renders by argParser.option(ArgType.Choice<RenderEnum>(),
"renders", "r", "Renders for showing information").multiple().default(listOf("text")) "renders", "r", "Renders for showing information").multiple().default(listOf(RenderEnum.TEXT))
val exception = assertFailsWith<IllegalStateException> { val exception = assertFailsWith<IllegalStateException> {
argParser.parse(arrayOf("-r", "xml")) argParser.parse(arrayOf("-r", "xml"))
} }
assertTrue("Option renders is expected to be one of [text, html]. xml is provided." in exception.message!!) assertTrue("Option renders is expected to be one of [text, html]. xml is provided." in exception.message!!)
} }
@Test
fun testWrongEnumChoice() {
val argParser = ArgParser("testParser")
val sources by argParser.option(ArgType.Choice<DataSourceEnum>(),
"sources", "s", "Data sources").multiple().default(listOf(DataSourceEnum.PRODUCTION))
val exception = assertFailsWith<IllegalStateException> {
argParser.parse(arrayOf("-s", "debug"))
}
assertTrue("Option sources is expected to be one of [local, staging, production]. debug is provided." in exception.message!!)
}
} }
@@ -3,16 +3,20 @@
* that can be found in the LICENSE file. * that can be found in the LICENSE file.
*/ */
@file:OptIn(ExperimentalCli::class) @file:OptIn(ExperimentalCli::class)
@file:Suppress("UNUSED_VARIABLE")
package kotlinx.cli package kotlinx.cli
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand
import kotlin.math.exp
import kotlin.test.* import kotlin.test.*
class HelpTests { class HelpTests {
enum class Renders {
TEXT,
HTML,
TEAMCITY,
STATISTICS,
METRICS
}
@Test @Test
fun testHelpMessage() { fun testHelpMessage() {
val argParser = ArgParser("test") val argParser = ArgParser("test")
@@ -23,8 +27,10 @@ class HelpTests {
val epsValue by argParser.option(ArgType.Double, "eps", "e", "Meaningful performance changes").default(1.0) val epsValue by argParser.option(ArgType.Double, "eps", "e", "Meaningful performance changes").default(1.0)
val useShortForm by argParser.option(ArgType.Boolean, "short", "s", val useShortForm by argParser.option(ArgType.Boolean, "short", "s",
"Show short version of report").default(false) "Show short version of report").default(false)
val renders by argParser.option(ArgType.Choice(listOf("text", "html", "teamcity", "statistics", "metrics")), val renders by argParser.option(ArgType.Choice(listOf("text", "html", "teamcity", "statistics", "metrics"), { it }),
shortName = "r", description = "Renders for showing information").multiple().default(listOf("text")) shortName = "r", description = "Renders for showing information").multiple().default(listOf("text"))
val sources by argParser.option(ArgType.Choice<DataSourceEnum>(),
"sources", "ds", "Data sources").multiple().default(listOf(DataSourceEnum.PRODUCTION))
val user by argParser.option(ArgType.String, shortName = "u", description = "User access information for authorization") val user by argParser.option(ArgType.String, shortName = "u", description = "User access information for authorization")
argParser.parse(arrayOf("main.txt")) argParser.parse(arrayOf("main.txt"))
val helpOutput = argParser.makeUsage().trimIndent() val helpOutput = argParser.makeUsage().trimIndent()
@@ -40,33 +46,47 @@ Options:
--eps, -e [$epsDefault] -> Meaningful performance changes { Double } --eps, -e [$epsDefault] -> Meaningful performance changes { Double }
--short, -s [false] -> Show short version of report --short, -s [false] -> Show short version of report
--renders, -r [text] -> Renders for showing information { Value should be one of [text, html, teamcity, statistics, metrics] } --renders, -r [text] -> Renders for showing information { Value should be one of [text, html, teamcity, statistics, metrics] }
--sources, -ds [production] -> Data sources { Value should be one of [local, staging, production] }
--user, -u -> User access information for authorization { String } --user, -u -> User access information for authorization { String }
--help, -h -> Usage info --help, -h -> Usage info
""".trimIndent() """.trimIndent()
assertEquals(expectedOutput, helpOutput) assertEquals(expectedOutput, helpOutput)
} }
enum class MetricType {
SAMPLES,
GEOMEAN;
override fun toString() = name.toLowerCase()
}
@Test @Test
fun testHelpForSubcommands() { fun testHelpForSubcommands() {
class Summary: Subcommand("summary", "Get summary information") { class Summary: Subcommand("summary", "Get summary information") {
val exec by option(ArgType.Choice(listOf("samples", "geomean")), val exec by option(ArgType.Choice<MetricType>(),
description = "Execution time way of calculation").default("geomean") description = "Execution time way of calculation").default(MetricType.GEOMEAN)
val execSamples by option(ArgType.String, "exec-samples", val execSamples by option(ArgType.String, "exec-samples",
description = "Samples used for execution time metric (value 'all' allows use all samples)").delimiter(",") description = "Samples used for execution time metric (value 'all' allows use all samples)").delimiter(",")
val execNormalize by option(ArgType.String, "exec-normalize", val execNormalize by option(ArgType.String, "exec-normalize",
description = "File with golden results which should be used for normalization") description = "File with golden results which should be used for normalization")
val compile by option(ArgType.Choice(listOf("samples", "geomean")), val compile by option(ArgType.Choice<MetricType>(),
description = "Compile time way of calculation").default("geomean") description = "Compile time way of calculation").default(MetricType.GEOMEAN)
val compileSamples by option(ArgType.String, "compile-samples", val compileSamples by option(ArgType.String, "compile-samples",
description = "Samples used for compile time metric (value 'all' allows use all samples)").delimiter(",") description = "Samples used for compile time metric (value 'all' allows use all samples)").delimiter(",")
val compileNormalize by option(ArgType.String, "compile-normalize", val compileNormalize by option(ArgType.String, "compile-normalize",
description = "File with golden results which should be used for normalization") description = "File with golden results which should be used for normalization")
val codesize by option(ArgType.Choice(listOf("samples", "geomean")), val codesize by option(ArgType.Choice<MetricType>(),
description = "Code size way of calculation").default("geomean") description = "Code size way of calculation").default(MetricType.GEOMEAN)
val codesizeSamples by option(ArgType.String, "codesize-samples", val codesizeSamples by option(ArgType.String, "codesize-samples",
description = "Samples used for code size metric (value 'all' allows use all samples)").delimiter(",") description = "Samples used for code size metric (value 'all' allows use all samples)").delimiter(",")
val codesizeNormalize by option(ArgType.String, "codesize-normalize", val codesizeNormalize by option(ArgType.String, "codesize-normalize",
description = "File with golden results which should be used for normalization") description = "File with golden results which should be used for normalization")
val source by option(ArgType.Choice<DataSourceEnum>(),
description = "Data source").default(DataSourceEnum.PRODUCTION)
val sourceSamples by option(ArgType.String, "source-samples",
description = "Samples used for code size metric (value 'all' allows use all samples)").delimiter(",")
val sourceNormalize by option(ArgType.String, "source-normalize",
description = "File with golden results which should be used for normalization")
val user by option(ArgType.String, shortName = "u", description = "User access information for authorization") val user by option(ArgType.String, shortName = "u", description = "User access information for authorization")
val mainReport by argument(ArgType.String, description = "Main report for analysis") val mainReport by argument(ArgType.String, description = "Main report for analysis")
@@ -94,6 +114,9 @@ Options:
--codesize [geomean] -> Code size way of calculation { Value should be one of [samples, geomean] } --codesize [geomean] -> Code size way of calculation { Value should be one of [samples, geomean] }
--codesize-samples -> Samples used for code size metric (value 'all' allows use all samples) { String } --codesize-samples -> Samples used for code size metric (value 'all' allows use all samples) { String }
--codesize-normalize -> File with golden results which should be used for normalization { String } --codesize-normalize -> File with golden results which should be used for normalization { String }
--source [production] -> Data source { Value should be one of [local, staging, production] }
--source-samples -> Samples used for code size metric (value 'all' allows use all samples) { String }
--source-normalize -> File with golden results which should be used for normalization { String }
--user, -u -> User access information for authorization { String } --user, -u -> User access information for authorization { String }
--help, -h -> Usage info --help, -h -> Usage info
""".trimIndent() """.trimIndent()
@@ -3,6 +3,8 @@
* that can be found in the LICENSE file. * that can be found in the LICENSE file.
*/ */
@file:Suppress("UNUSED_VARIABLE")
package kotlinx.cli package kotlinx.cli
import kotlin.test.* import kotlin.test.*
@@ -38,6 +40,13 @@ class OptionsTests {
assertEquals("input.txt", input) assertEquals("input.txt", input)
} }
enum class Renders {
TEXT,
HTML,
XML,
JSON
}
@Test @Test
fun testGNUPrefix() { fun testGNUPrefix() {
val argParser = ArgParser("testParser", prefixStyle = ArgParser.OptionPrefixStyle.GNU) val argParser = ArgParser("testParser", prefixStyle = ArgParser.OptionPrefixStyle.GNU)
@@ -74,26 +83,37 @@ class OptionsTests {
fun testMultipleOptions() { fun testMultipleOptions() {
val argParser = ArgParser("testParser") val argParser = ArgParser("testParser")
val useShortForm by argParser.option(ArgType.Boolean, "short", "s", "Show short version of report").default(false) val useShortForm by argParser.option(ArgType.Boolean, "short", "s", "Show short version of report").default(false)
val renders by argParser.option(ArgType.Choice(listOf("text", "html", "xml", "json")), val renders by argParser.option(ArgType.Choice<Renders>(),
"renders", "r", "Renders for showing information").multiple().default(listOf("text")) "renders", "r", "Renders for showing information").multiple().default(listOf(Renders.TEXT))
argParser.parse(arrayOf("-s", "-r", "text", "-r", "json")) val sources by argParser.option(ArgType.Choice<DataSourceEnum>(),
"sources", "ds", "Data sources").multiple().default(listOf(DataSourceEnum.PRODUCTION))
argParser.parse(arrayOf("-s", "-r", "text", "-r", "json", "-ds", "local", "-ds", "production"))
assertEquals(true, useShortForm) assertEquals(true, useShortForm)
assertEquals(2, renders.size) assertEquals(2, renders.size)
val (firstRender, secondRender) = renders val (firstRender, secondRender) = renders
assertEquals("text", firstRender) assertEquals(Renders.TEXT, firstRender)
assertEquals("json", secondRender) assertEquals(Renders.JSON, secondRender)
assertEquals(2, sources.size)
val (firstSource, secondSource) = sources
assertEquals(DataSourceEnum.LOCAL, firstSource)
assertEquals(DataSourceEnum.PRODUCTION, secondSource)
} }
@Test @Test
fun testDefaultOptions() { fun testDefaultOptions() {
val argParser = ArgParser("testParser") val argParser = ArgParser("testParser")
val useShortForm by argParser.option(ArgType.Boolean, "short", "s", "Show short version of report").default(false) val useShortForm by argParser.option(ArgType.Boolean, "short", "s", "Show short version of report").default(false)
val renders by argParser.option(ArgType.Choice(listOf("text", "html", "xml", "json")), val renders by argParser.option(ArgType.Choice<Renders>(),
"renders", "r", "Renders for showing information").multiple().default(listOf("text")) "renders", "r", "Renders for showing information").multiple().default(listOf(Renders.TEXT))
val sources by argParser.option(ArgType.Choice<DataSourceEnum>(),
"sources", "ds", "Data sources").multiple().default(listOf(DataSourceEnum.PRODUCTION))
val output by argParser.option(ArgType.String, "output", "o", "Output file") val output by argParser.option(ArgType.String, "output", "o", "Output file")
argParser.parse(arrayOf("-o", "out.txt")) argParser.parse(arrayOf("-o", "out.txt"))
assertEquals(false, useShortForm) assertEquals(false, useShortForm)
assertEquals("text", renders[0]) assertEquals(Renders.TEXT, renders[0])
assertEquals(DataSourceEnum.PRODUCTION, sources[0])
} }
@Test @Test
@@ -101,20 +121,26 @@ class OptionsTests {
val argParser = ArgParser("testParser") val argParser = ArgParser("testParser")
val useShortFormOption = argParser.option(ArgType.Boolean, "short", "s", "Show short version of report").default(false) val useShortFormOption = argParser.option(ArgType.Boolean, "short", "s", "Show short version of report").default(false)
var useShortForm by useShortFormOption var useShortForm by useShortFormOption
val rendersOption = argParser.option(ArgType.Choice(listOf("text", "html", "xml", "json")), val rendersOption = argParser.option(ArgType.Choice<Renders>(),
"renders", "r", "Renders for showing information").multiple().default(listOf("text")) "renders", "r", "Renders for showing information").multiple().default(listOf(Renders.TEXT))
var renders by rendersOption var renders by rendersOption
val sourcesOption = argParser.option(ArgType.Choice<DataSourceEnum>(),
"sources", "ds", "Data sources").multiple().default(listOf(DataSourceEnum.PRODUCTION))
var sources by sourcesOption
val outputOption = argParser.option(ArgType.String, "output", "o", "Output file") val outputOption = argParser.option(ArgType.String, "output", "o", "Output file")
var output by outputOption var output by outputOption
argParser.parse(arrayOf("-o", "out.txt")) argParser.parse(arrayOf("-o", "out.txt"))
output = null output = null
useShortForm = true useShortForm = true
renders = listOf() renders = listOf()
sources = listOf()
assertEquals(true, useShortForm) assertEquals(true, useShortForm)
assertEquals(null, output) assertEquals(null, output)
assertEquals(0, renders.size) assertEquals(0, renders.size)
assertEquals(0, sources.size)
assertEquals(ArgParser.ValueOrigin.REDEFINED, outputOption.valueOrigin) assertEquals(ArgParser.ValueOrigin.REDEFINED, outputOption.valueOrigin)
assertEquals(ArgParser.ValueOrigin.REDEFINED, useShortFormOption.valueOrigin) assertEquals(ArgParser.ValueOrigin.REDEFINED, useShortFormOption.valueOrigin)
assertEquals(ArgParser.ValueOrigin.REDEFINED, rendersOption.valueOrigin) assertEquals(ArgParser.ValueOrigin.REDEFINED, rendersOption.valueOrigin)
assertEquals(ArgParser.ValueOrigin.REDEFINED, sourcesOption.valueOrigin)
} }
} }
@@ -28,7 +28,7 @@ class SubcommandsTests {
} }
val action = Summary() val action = Summary()
argParser.subcommands(action) argParser.subcommands(action)
argParser.parse(arrayOf("-o", "out.txt", "summary", "-i", "2", "3", "5")) argParser.parse(arrayOf("summary", "-o", "out.txt", "-i", "2", "3", "5"))
assertEquals("out.txt", output) assertEquals("out.txt", output)
assertEquals(-10, action.result) assertEquals(-10, action.result)
} }
@@ -100,4 +100,37 @@ class SubcommandsTests {
argParser.parse(arrayOf("calc", "-i", "summary", "2", "3", "5")) argParser.parse(arrayOf("calc", "-i", "summary", "2", "3", "5"))
assertEquals(-10, action.result) assertEquals(-10, action.result)
} }
@Test
fun testCommonDefaultInSubcommand() {
val parser = ArgParser("testParser")
val output by parser.option(ArgType.String, "output", "o", "Output file")
.default("any_file")
class Summary: Subcommand("summary", "Calculate summary") {
val invert by option(ArgType.Boolean, "invert", "i", "Invert results").default(false)
val addendums by argument(ArgType.Int, "addendums", description = "Addendums").vararg()
var result: Int = 0
override fun execute() {
result = addendums.sum()
result = if (invert) -1 * result else result
println("result is: $result and will output to $output")
}
}
class Multiply: Subcommand("mul", "Multiply") {
val numbers by argument(ArgType.Int, description = "Addendums").vararg()
var result: Int = 0
override fun execute() {
result = numbers.reduce{ acc, it -> acc * it }
}
}
val summary = Summary()
val multiple = Multiply()
parser.subcommands(summary, multiple)
parser.parse(arrayOf("summary", "1", "2", "4"))
assertEquals("any_file", output)
assertEquals(7, summary.result)
}
} }