diff --git a/core/commonMain/src/ArgumentValues.kt b/core/commonMain/src/ArgumentValues.kt index 81a1ac4..b57442a 100644 --- a/core/commonMain/src/ArgumentValues.kt +++ b/core/commonMain/src/ArgumentValues.kt @@ -75,7 +75,7 @@ internal abstract class ParsingValue(val descriptor: Descr */ fun addDefaultValue() { if (descriptor.defaultValueSet) { - parsedValue = descriptor.defaultValue!! + parsedValue = descriptor.defaultValue!!.value valueOrigin = ArgParser.ValueOrigin.SET_DEFAULT_VALUE } } diff --git a/core/commonMain/src/Arguments.kt b/core/commonMain/src/Arguments.kt index 7a02502..f64ed13 100644 --- a/core/commonMain/src/Arguments.kt +++ b/core/commonMain/src/Arguments.kt @@ -160,7 +160,8 @@ fun AbstractSingleArgument.multiple(number: Int): MultipleArgument { require(number >= 2) { "multiple() modifier with value less than 2 is unavailable. It's already set to 1." } val newArgument = with((delegate as ParsingValue).descriptor as ArgDescriptor) { - MultipleArgument(ArgDescriptor(type, fullName, number, description, listOfNotNull(defaultValue), + MultipleArgument(ArgDescriptor(type, fullName, number, description, + defaultValue?.toMultiple() ?: SimpleDefaultValue(emptyList()), required, deprecatedWarning), owner) } owner.entity = newArgument @@ -173,13 +174,24 @@ fun fun AbstractSingleArgument.vararg(): MultipleArgument { val newArgument = with((delegate as ParsingValue).descriptor as ArgDescriptor) { - MultipleArgument(ArgDescriptor(type, fullName, null, description, listOfNotNull(defaultValue), + MultipleArgument(ArgDescriptor(type, fullName, null, description, + defaultValue?.toMultiple() ?: SimpleDefaultValue(emptyList()), required, deprecatedWarning), owner) } owner.entity = newArgument return newArgument } +internal fun SingleNullableArgument.toArgumentWithDefault(value: DefaultValue): + SingleArgument { + val newArgument = with((delegate as ParsingValue).descriptor as ArgDescriptor) { + SingleArgument(ArgDescriptor(type, fullName, number, description, + value, false, deprecatedWarning), owner) + } + owner.entity = newArgument + return newArgument +} + /** * Specifies the default value for the argument, that will be used when no value is provided for the argument * in command line string. @@ -188,14 +200,20 @@ fun AbstractSingleArgum * * @param value the default value. */ -fun SingleNullableArgument.default(value: T): SingleArgument { - val newArgument = with((delegate as ParsingValue).descriptor as ArgDescriptor) { - SingleArgument(ArgDescriptor(type, fullName, number, description, value, - false, deprecatedWarning), owner) - } - owner.entity = newArgument - return newArgument -} +fun SingleNullableArgument.default(value: T): SingleArgument = + toArgumentWithDefault(SimpleDefaultValue(value)) + + +/** + * Specifies the default value for the argument, that will be used when no value is provided for the argument + * in command line string. + * + * Argument becomes optional, because value for it is set even if it isn't provided in command line. + * + * @param value the default value. + */ +fun SingleNullableArgument.default(value: DefaultValuePattern): + SingleArgument = toArgumentWithDefault(value) /** * Specifies the default value for the argument with multiple values, that will be used when no values are provided @@ -209,8 +227,8 @@ fun MultipleArgument.default(value: Collec MultipleArgument { require (value.isNotEmpty()) { "Default value for argument can't be empty collection." } val newArgument = with((delegate as ParsingValue>).descriptor as ArgDescriptor) { - MultipleArgument(ArgDescriptor(type, fullName, number, description, value.toList(), - required, deprecatedWarning), owner) + MultipleArgument(ArgDescriptor(type, fullName, number, description, + SimpleDefaultValue(value.toList()), required, deprecatedWarning), owner) } owner.entity = newArgument return newArgument @@ -242,7 +260,7 @@ fun SingleArgument.optional(): SingleN fun MultipleArgument.optional(): MultipleArgument { val newArgument = with((delegate as ParsingValue>).descriptor as ArgDescriptor) { MultipleArgument(ArgDescriptor(type, fullName, number, description, - defaultValue?.toList() ?: listOf(), false, deprecatedWarning), owner) + defaultValue ?: SimpleDefaultValue(listOf()), false, deprecatedWarning), owner) } owner.entity = newArgument return newArgument diff --git a/core/commonMain/src/Descriptors.kt b/core/commonMain/src/Descriptors.kt index 098fc4a..545d333 100644 --- a/core/commonMain/src/Descriptors.kt +++ b/core/commonMain/src/Descriptors.kt @@ -4,6 +4,74 @@ */ package kotlinx.cli +/** + * Wrapper for default value. + */ +abstract class DefaultValue { + abstract val value: T + abstract val helpMessage: String + + /** + * Provide text description of value. + * + * @param value value got getting text description for. + */ + fun valueDescription(value: T) = + if (value is List<*> && value.isNotEmpty()) + " [${value.joinToString()}]" + else if (value !is List<*>) + " [$value]" + else "" + + abstract fun toMultiple(): DefaultValue> +} + +/** + * Simple default value which just wrap value of any argument type. + */ +internal class SimpleDefaultValue(private val simpleValue: T) : DefaultValue() { + override val value: T + get() = simpleValue + override val helpMessage: String + get() = valueDescription(value) + + override fun toMultiple(): DefaultValue> = SimpleDefaultValue(listOf(value)) +} + +/** + * Default value pattern which allows to use create default value + * based on values of other command line options/arguments. + */ +class DefaultValuePattern(val cliEntities: List>, + val expression: (values: List) -> T, + val helpMessageGenerator: (values: List) -> String) : + DefaultValue() { + + init { + cliEntities.forEach { + with((it.delegate as ParsingValue<*, *>).descriptor) { + require(defaultValue != null || required) { + "It's possible to use only arguments/options which always have values." + } + } + } + } + + override val value: T by lazy { + expression(cliEntities.map { it.value }) + } + + override val helpMessage: String + get() = " [${helpMessageGenerator(cliEntities. + map { "\${${(it.delegate as ParsingValue<*, *>).descriptor.fullName!!}}" })}]" + + override fun toMultiple(): DefaultValuePattern> { + // For custom generator it's impossible to convert value to another type. + error("Conversion to multiple default values is unknown in case " + + "of provided by user pattern for default value.") + } +} + /** * Common descriptor both for options and positional arguments. * @@ -17,7 +85,7 @@ package kotlinx.cli internal abstract class Descriptor(val type: ArgType, var fullName: String? = null, val description: String? = null, - val defaultValue: TResult? = null, + val defaultValue: DefaultValue? = null, val required: Boolean = false, val deprecatedWarning: String? = null) { /** @@ -29,19 +97,6 @@ internal abstract class Descriptor(val type: ArgType, */ abstract val helpMessage: String - /** - * Provide text description of value. - * - * @param value value got getting text description for. - */ - fun valueDescription(value: TResult?) = value?.let { - if (it is List<*> && it.isNotEmpty()) - " [${it.joinToString()}]" - else if (it !is List<*>) - " [$it]" - else null - } - /** * Flag to check if descriptor has set default value for option/argument. */ @@ -74,7 +129,7 @@ internal class OptionDescriptor( fullName: String? = null, val shortName: String ? = null, description: String? = null, - defaultValue: TResult? = null, + defaultValue: DefaultValue? = null, required: Boolean = false, val multiple: Boolean = false, val delimiter: String? = null, @@ -89,7 +144,7 @@ internal class OptionDescriptor( val result = StringBuilder() result.append(" $optionFullFormPrefix$fullName") shortName?.let { result.append(", $optionShortFromPrefix$it") } - valueDescription(defaultValue)?.let { + defaultValue?.helpMessage?.let { result.append(it) } description?.let {result.append(" -> $it")} @@ -119,7 +174,7 @@ internal class ArgDescriptor( fullName: String?, val number: Int? = null, description: String? = null, - defaultValue: TResult? = null, + defaultValue: DefaultValue? = null, required: Boolean = true, deprecatedWarning: String? = null) : Descriptor(type, fullName, description, defaultValue, required, deprecatedWarning) { @@ -139,7 +194,7 @@ internal class ArgDescriptor( get() { val result = StringBuilder() result.append(" ${fullName}") - valueDescription(defaultValue)?.let { + defaultValue?.helpMessage?.let { result.append(it) } description?.let { result.append(" -> $it") } diff --git a/core/commonMain/src/Options.kt b/core/commonMain/src/Options.kt index 2a5508a..c17e6e9 100644 --- a/core/commonMain/src/Options.kt +++ b/core/commonMain/src/Options.kt @@ -107,7 +107,7 @@ fun AbstractSingleOption( OptionDescriptor( optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, - description, listOfNotNull(defaultValue), + description, defaultValue?.toMultiple() ?: SimpleDefaultValue(emptyList()), required, true, delimiter, deprecatedWarning ), owner ) @@ -129,7 +129,7 @@ fun MultipleOption( OptionDescriptor( optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, - description, defaultValue?.toList() ?: listOf(), + description, defaultValue ?: SimpleDefaultValue(listOf()), required, true, delimiter, deprecatedWarning ), owner ) @@ -138,25 +138,37 @@ fun MultipleOption SingleNullableOption.default(value: T): SingleOption { +internal fun SingleNullableOption.toOptionWithDefault(value: DefaultValue): SingleOption { val newOption = with((delegate as ParsingValue).descriptor as OptionDescriptor) { SingleOption( - OptionDescriptor( - optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, - description, value, required, multiple, delimiter, deprecatedWarning - ), owner + OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, + description, value, required, multiple, delimiter, deprecatedWarning + ), owner ) } owner.entity = newOption return newOption } +/** + * Specifies the default value for the option, that will be used when no value is provided for it + * in command line string. + * + * @param value the default value. + */ +fun SingleNullableOption.default(value: T): SingleOption = + toOptionWithDefault(SimpleDefaultValue(value)) + +/** + * Specifies the default value for the option, that will be used when no value is provided for it + * in command line string. + * + * @param value the default value. + */ +fun SingleNullableOption.default(value: DefaultValuePattern): + SingleOption = toOptionWithDefault(value) + /** * Specifies the default value for the option with multiple values, that will be used when no values are provided * for it in command line string. @@ -172,7 +184,7 @@ fun MultipleOption( OptionDescriptor( optionFullFormPrefix, optionShortFromPrefix, type, fullName, - shortName, description, value.toList(), + shortName, description, SimpleDefaultValue(value.toList()), required, multiple, delimiter, deprecatedWarning ), owner ) @@ -208,7 +220,7 @@ fun MultipleOption( OptionDescriptor( optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, - description, defaultValue?.toList() ?: listOf(), + description, defaultValue ?: SimpleDefaultValue(listOf()), true, multiple, delimiter, deprecatedWarning ), owner ) @@ -232,7 +244,7 @@ fun AbstractSingleOption( OptionDescriptor( optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, - description, listOfNotNull(defaultValue), + description, defaultValue?.toMultiple() ?: SimpleDefaultValue(emptyList()), required, multiple, delimiterValue, deprecatedWarning ), owner ) @@ -256,7 +268,7 @@ fun MultipleOption( OptionDescriptor( optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, - description, defaultValue?.toList() ?: listOf(), + description, defaultValue ?: SimpleDefaultValue(listOf()), required, multiple, delimiterValue, deprecatedWarning ), owner ) diff --git a/core/commonTest/src/OptionsTests.kt b/core/commonTest/src/OptionsTests.kt index f1d3156..f5c16e8 100644 --- a/core/commonTest/src/OptionsTests.kt +++ b/core/commonTest/src/OptionsTests.kt @@ -64,6 +64,23 @@ class OptionsTests { assertEquals("text", renders[0]) } + @Test + fun testDefaultValuePattern() { + val argParser = ArgParser("testParser") + 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")), + "renders", "r", "Renders for showing information").multiple().default(listOf("text")) + val output = argParser.option(ArgType.String, "output", "o", "Output file") + .default("output.txt") + val outputLog by argParser.option(ArgType.String, "log", "l", "Output log file") + .default(DefaultValuePattern(listOf(output), { values -> "${values[0]}.log"}, + { values -> "${values[0]}.log" } )) + argParser.parse(arrayOf("-o", "out.txt")) + assertEquals("out.txt.log", outputLog) + assertEquals("text", renders[0]) + } + @Test fun testResetOptionsValues() { val argParser = ArgParser("testParser")