diff --git a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgParser.kt b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgParser.kt index ef6a435a84b..a9faa17b713 100644 --- a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgParser.kt +++ b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgParser.kt @@ -41,11 +41,27 @@ internal class ArgumentsQueue(argumentsDescriptors: List>) { } /** - * Interface of argument value. + * A property delegate that provides access to the argument/option value. */ interface ArgumentValueDelegate { + /** + * The value of an option or argument parsed from command line. + * + * Accessing this value before [ArgParser.parse] method is called will result in an exception. + * + * @see CLIEntity.value + */ var value: T + + /** Provides the value for the delegated property getter. Returns the [value] property. + * @throws IllegalStateException in case of accessing the value before [ArgParser.parse] method is called. + */ operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value + + /** Sets the [value] to the [ArgumentValueDelegate.value] property from the delegated property setter. + * This operation is possible only after command line arguments were parsed with [ArgParser.parse] + * @throws IllegalStateException in case of resetting value before command line arguments are parsed. + */ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { this.value = value } @@ -54,7 +70,6 @@ interface ArgumentValueDelegate { /** * Abstract base class for subcommands. */ -@SinceKotlin("1.3") @ExperimentalCli abstract class Subcommand(val name: String): ArgParser(name) { /** @@ -74,14 +89,18 @@ class ArgParserResult(val commandName: String) /** * Arguments parser. * - * @property programName name of current program. - * @property useDefaultHelpShortName add or not -h flag for help message. - * @property prefixStyle style of expected options prefix. - * @property skipExtraArguments just skip extra arguments in command line string without producing error message. + * @property programName the name of the current program. + * @property useDefaultHelpShortName specifies whether to register "-h" option for printing the usage information. + * @property prefixStyle the style of option prefixing. + * @property skipExtraArguments specifies whether the extra unmatched arguments in a command line string + * can be skipped without producing an error message. */ -open class ArgParser(val programName: String, var useDefaultHelpShortName: Boolean = true, - var prefixStyle: OPTION_PREFIX_STYLE = OPTION_PREFIX_STYLE.LINUX, - var skipExtraArguments: Boolean = false) { +open class ArgParser( + val programName: String, + var useDefaultHelpShortName: Boolean = true, + var prefixStyle: OptionPrefixStyle = OptionPrefixStyle.LINUX, + var skipExtraArguments: Boolean = false +) { /** * Map of options: key - full name of option, value - pair of descriptor and parsed values. @@ -102,6 +121,11 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole */ private val declaredArguments = mutableListOf() + /** + * State of parser. Stores last parsing result or null. + */ + private var parsingState: ArgParserResult? = null + /** * Map of subcommands. */ @@ -116,7 +140,7 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole /** * Used prefix form for full option form. */ - private val optionFullFormPrefix = if (prefixStyle == OPTION_PREFIX_STYLE.LINUX) "--" else "-" + private val optionFullFormPrefix = if (prefixStyle == OptionPrefixStyle.LINUX) "--" else "-" /** * Used prefix form for short option form. @@ -129,39 +153,66 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole protected val fullCommandName = mutableListOf(programName) /** - * Origin of option/argument value. - * - * Possible values: - * SET_BY_USER - value of option was provided in command line string; - * SET_DEFAULT_VALUE - value of option wasn't provided in command line, but set using default value; - * UNSET - value of option is unset - * REDEFINED - value of option was redefined in source code after parsing. + * The way an option/argument has got its value. */ - enum class ValueOrigin { SET_BY_USER, SET_DEFAULT_VALUE, UNSET, REDEFINED } + enum class ValueOrigin { + /* The value was parsed from command line arguments. */ + SET_BY_USER, + /* The value was missing in command line, therefore the default value was used. */ + SET_DEFAULT_VALUE, + /* The value is not initialized by command line values or by default values. */ + UNSET, + /* The value was redefined after parsing manually (usually with the property setter). */ + REDEFINED, + /* The value is undefined, because parsing wasn't called. */ + UNDEFINED + } /** - * Options prefix style. - * - * Possible values: - * LINUX - Linux style, for full forms of options "--", for short form - "-" - * JVM - JVM style, both for full and short forms of options "-" + * The style of option prefixing. */ - enum class OPTION_PREFIX_STYLE { LINUX, JVM } + enum class OptionPrefixStyle { + /* Linux style: the full name of an option is prefixed with two hyphens "--" and the short name — with one "-". */ + LINUX, + /* JVM style: both full and short names are prefixed with one hyphen "-". */ + JVM, + } + + @Deprecated("OPTION_PREFIX_STYLE is deprecated. Please, use OptionPrefixStyle.", + ReplaceWith("OptionPrefixStyle", "kotlinx.cli.OptionPrefixStyle")) + @Suppress("TOPLEVEL_TYPEALIASES_ONLY") + typealias OPTION_PREFIX_STYLE = OptionPrefixStyle /** - * Add option with single possible value and get delegator to its value. + * Declares a named option and returns an object which can be used to access the option value + * after all arguments are parsed or to delegate a property for accessing the option value to. * - * @param type argument type, one of [ArgType]. - * @param fullName argument full name. - * @param shortName option short name. - * @param description text description of Argument. - * @param deprecatedWarning text message with information in case if option is deprecated. + * By default, the option supports only a single value, is optional, and has no default value, + * therefore its value's type is `T?`. + * + * You can alter the option properties by chaining extensions for the option type on the returned object: + * - [AbstractSingleOption.default] to provide a default value that is used when the option is not specified; + * - [SingleNullableOption.required] to make the option non-optional; + * - [AbstractSingleOption.delimiter] to allow specifying multiple values in one command line argument with a delimiter; + * - [AbstractSingleOption.multiple] to allow specifying the option several times. + * + * @param type The type describing how to parse an option value from a string, + * an instance of [ArgType], e.g. [ArgType.String] or [ArgType.Choice]. + * @param fullName the full name of the option, can be omitted if the option name is inferred + * from the name of a property delegated to this option. + * @param shortName the short name of the option, `null` if the option cannot be specified in a short form. + * @param description the description of the option used when rendering the usage information. + * @param deprecatedWarning the deprecation message for the option. + * Specifying anything except `null` makes this option deprecated. The message is rendered in a help message and + * issued as a warning when the option is encountered when parsing command line arguments. */ - fun option(type: ArgType, - fullName: String? = null, - shortName: String ? = null, - description: String? = null, - deprecatedWarning: String? = null): SingleNullableOption { + fun option( + type: ArgType, + fullName: String? = null, + shortName: String ? = null, + description: String? = null, + deprecatedWarning: String? = null + ): SingleNullableOption { val option = SingleNullableOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, description, deprecatedWarning = deprecatedWarning), CLIEntityWrapper()) option.owner.entity = option @@ -176,38 +227,55 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole private fun inspectRequiredAndDefaultUsage() { var previousArgument: ParsingValue<*, *>? = null arguments.forEach { (_, currentArgument) -> - previousArgument?.let { + previousArgument?.let { previous -> // Previous argument has default value. - it.descriptor.defaultValue?.let { - if (currentArgument.descriptor.defaultValue == null && currentArgument.descriptor.required) { - printError("Default value of argument ${previousArgument.descriptor.fullName} will be unused, " + + if (previous.descriptor.defaultValueSet) { + if (!currentArgument.descriptor.defaultValueSet && currentArgument.descriptor.required) { + error("Default value of argument ${previous.descriptor.fullName} will be unused, " + "because next argument ${currentArgument.descriptor.fullName} is always required and has no default value.") } } // Previous argument is optional. - if (!it.descriptor.required) { - if (currentArgument.descriptor.defaultValue == null && currentArgument.descriptor.required) { - printError("Argument ${previousArgument.descriptor.fullName} will be always required, " + + if (!previous.descriptor.required) { + if (!currentArgument.descriptor.defaultValueSet && currentArgument.descriptor.required) { + error("Argument ${previous.descriptor.fullName} will be always required, " + "because next argument ${currentArgument.descriptor.fullName} is always required.") } } } + previousArgument = currentArgument } } /** - * Add argument with single nullable value and get delegator to its value. + * Declares an argument and returns an object which can be used to access the argument value + * after all arguments are parsed or to delegate a property for accessing the argument value to. * - * @param type argument type, one of [ArgType]. - * @param fullName argument full name. - * @param description text description of argument. - * @param deprecatedWarning text message with information in case if argument is deprecated. + * By default, the argument supports only a single value, is required, and has no default value, + * therefore its value's type is `T`. + * + * You can alter the argument properties by chaining extensions for the argument type on the returned object: + * - [AbstractSingleArgument.default] to provide a default value that is used when the argument is not specified; + * - [SingleArgument.optional] to allow omitting the argument; + * - [AbstractSingleArgument.multiple] to require the argument to have exactly the number of values specified; + * - [AbstractSingleArgument.vararg] to allow specifying an unlimited number of values for the _last_ argument. + * + * @param type The type describing how to parse an option value from a string, + * an instance of [ArgType], e.g. [ArgType.String] or [ArgType.Choice]. + * @param fullName the full name of the argument, can be omitted if the argument name is inferred + * from the name of a property delegated to this argument. + * @param description the description of the argument used when rendering the usage information. + * @param deprecatedWarning the deprecation message for the argument. + * Specifying anything except `null` makes this argument deprecated. The message is rendered in a help message and + * issued as a warning when the argument is encountered when parsing command line arguments. */ - fun argument(type: ArgType, - fullName: String? = null, - description: String? = null, - deprecatedWarning: String? = null) : SingleArgument { - val argument = SingleArgument(ArgDescriptor(type, fullName, 1, + fun argument( + type: ArgType, + fullName: String? = null, + description: String? = null, + deprecatedWarning: String? = null + ) : SingleArgument { + val argument = SingleArgument(ArgDescriptor(type, fullName, 1, description, deprecatedWarning = deprecatedWarning), CLIEntityWrapper()) argument.owner.entity = argument declaredArguments.add(argument.owner) @@ -215,16 +283,15 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole } /** - * Add subcommands. + * Registers one or more subcommands. * * @param subcommandsList subcommands to add. */ - @SinceKotlin("1.3") @ExperimentalCli fun subcommands(vararg subcommandsList: Subcommand) { subcommandsList.forEach { if (it.name in subcommands) { - printError("Subcommand with name ${it.name} was already defined.") + error("Subcommand with name ${it.name} was already defined.") } // Set same settings as main parser. @@ -238,11 +305,11 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole } /** - * Output error. Also adds help usage information for easy understanding of problem. + * Outputs an error message adding the usage information after it. * * @param message error message. */ - fun printError(message: String): Nothing { + fun printError(message: String): Nothing { error("$message\n${makeUsage()}") } @@ -292,21 +359,30 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole else null /** - * Parse arguments. + * Parses the provided array of command line arguments. + * After a successful parsing, the options and arguments declared in this parser get their values and can be accessed + * with the properties delegated to them. * - * @param args array with command line arguments. + * @param args the array with command line arguments. * - * @return true if all arguments were parsed successfully, otherwise return false and print help message. + * @return an [ArgParserResult] if all arguments were parsed successfully. + * Otherwise, prints the usage information and terminates the program execution. + * @throws IllegalStateException in case of attempt of calling parsing several times. */ - fun parse(args: Array) = parse(args.asList()) + fun parse(args: Array): ArgParserResult = parse(args.asList()) protected fun parse(args: List): ArgParserResult { + check(parsingState == null) { "Parsing of command line options can be called only once." } // Add help option. - val helpDescriptor = if (useDefaultHelpShortName) OptionDescriptor(optionFullFormPrefix, - optionShortFromPrefix, ArgType.Boolean, - "help", "h", "Usage info") - else OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, - ArgType.Boolean, "help", description = "Usage info") + val helpDescriptor = if (useDefaultHelpShortName) OptionDescriptor( + optionFullFormPrefix, + optionShortFromPrefix, ArgType.Boolean, + "help", "h", "Usage info" + ) + else OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, + ArgType.Boolean, "help", description = "Usage info" + ) val helpOption = SingleNullableOption(helpDescriptor, CLIEntityWrapper()) helpOption.owner.entity = helpOption declaredOptions.add(helpOption.owner) @@ -316,6 +392,10 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole argument(ArgType.String, "").vararg() } + // Clean options and arguments maps. + options.clear() + arguments.clear() + // Map declared options and arguments to maps. declaredOptions.forEachIndexed { index, option -> val value = option.entity?.delegate as ParsingValue<*, *> @@ -350,6 +430,12 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole // Make inspections for arguments. inspectRequiredAndDefaultUsage() + listOf(arguments, options).forEach { + it.forEach { (_, value) -> + value.valueOrigin = ValueOrigin.UNSET + } + } + val argumentsQueue = ArgumentsQueue(arguments.map { it.value.descriptor as ArgDescriptor<*, *> }) var index = 0 @@ -363,8 +449,9 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole // Use parser for this subcommand. subcommand.parse(args.slice(index + 1..args.size - 1)) subcommand.execute() + parsingState = ArgParserResult(name) - return ArgParserResult(name) + return parsingState!! } } // Parse arguments from command line. @@ -409,16 +496,19 @@ open class ArgParser(val programName: String, var useDefaultHelpShortName: Boole if (value.isEmpty()) { value.addDefaultValue() } + if (value.valueOrigin != ValueOrigin.SET_BY_USER && value.descriptor.required) { + printError("Value for ${value.descriptor.textDescription} should be always provided in command line.") + } } } catch (exception: ParsingException) { printError(exception.message!!) } - - return ArgParserResult(programName) + parsingState = ArgParserResult(programName) + return parsingState!! } /** - * Create message with usage description. + * Creates a message with the usage information. */ internal fun makeUsage(): String { val result = StringBuilder() diff --git a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgType.kt b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgType.kt index e9b55eec955..d5da8e21da6 100644 --- a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgType.kt +++ b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgType.kt @@ -23,7 +23,7 @@ abstract class ArgType(val hasParameter: kotlin.Boolean) { * * @param value value */ - abstract val conversion: (value: kotlin.String, name: kotlin.String)->T + abstract fun convert(value: kotlin.String, name: kotlin.String): T /** * Argument type for flags that can be only set/unset. @@ -32,8 +32,8 @@ abstract class ArgType(val hasParameter: kotlin.Boolean) { override val description: kotlin.String get() = "" - override val conversion: (value: kotlin.String, name: kotlin.String) -> kotlin.Boolean - get() = { value, _ -> if (value == "false") false else true } + override fun convert(value: kotlin.String, name: kotlin.String): kotlin.Boolean = + value != "false" } /** @@ -43,8 +43,7 @@ abstract class ArgType(val hasParameter: kotlin.Boolean) { override val description: kotlin.String get() = "{ String }" - override val conversion: (value: kotlin.String, name: kotlin.String) -> kotlin.String - get() = { value, _ -> value } + override fun convert(value: kotlin.String, name: kotlin.String): kotlin.String = value } /** @@ -54,9 +53,9 @@ abstract class ArgType(val hasParameter: kotlin.Boolean) { override val description: kotlin.String get() = "{ Int }" - override val conversion: (value: kotlin.String, name: kotlin.String) -> kotlin.Int - get() = { value, name -> value.toIntOrNull() - ?: throw ParsingException("Option $name is expected to be integer number. $value is provided.") } + override fun convert(value: kotlin.String, name: kotlin.String): kotlin.Int = + value.toIntOrNull() + ?: throw ParsingException("Option $name is expected to be integer number. $value is provided.") } /** @@ -66,9 +65,9 @@ abstract class ArgType(val hasParameter: kotlin.Boolean) { override val description: kotlin.String get() = "{ Double }" - override val conversion: (value: kotlin.String, name: kotlin.String) -> kotlin.Double - get() = { value, name -> value.toDoubleOrNull() - ?: throw ParsingException("Option $name is expected to be double number. $value is provided.") } + override fun convert(value: kotlin.String, name: kotlin.String): kotlin.Double = + value.toDoubleOrNull() + ?: throw ParsingException("Option $name is expected to be double number. $value is provided.") } /** @@ -78,9 +77,9 @@ abstract class ArgType(val hasParameter: kotlin.Boolean) { override val description: kotlin.String get() = "{ Value should be one of $values }" - override val conversion: (value: kotlin.String, name: kotlin.String) -> kotlin.String - get() = { value, name -> if (value in values) value - else throw ParsingException("Option $name is expected to be one of $values. $value is provided.") } + override fun convert(value: kotlin.String, name: kotlin.String): kotlin.String = + if (value in values) value + else throw ParsingException("Option $name is expected to be one of $values. $value is provided.") } } diff --git a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgumentValues.kt b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgumentValues.kt index eccb5dee861..81a1ac4f0d1 100644 --- a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgumentValues.kt +++ b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgumentValues.kt @@ -4,8 +4,6 @@ */ package kotlinx.cli -import kotlin.reflect.KProperty - /** * Parsing value of option/argument. */ @@ -18,8 +16,8 @@ internal abstract class ParsingValue(val descriptor: Descr /** * Value origin. */ - var valueOrigin = ArgParser.ValueOrigin.UNSET - protected set + var valueOrigin = ArgParser.ValueOrigin.UNDEFINED + internal set /** * Check if values of argument are empty. @@ -76,9 +74,6 @@ internal abstract class ParsingValue(val descriptor: Descr * Set default value to option. */ fun addDefaultValue() { - if (!descriptor.defaultValueSet && descriptor.required) { - throw ParsingException("Please, provide value for ${descriptor.textDescription}. It should be always set.") - } if (descriptor.defaultValueSet) { parsedValue = descriptor.defaultValue!! valueOrigin = ArgParser.ValueOrigin.SET_DEFAULT_VALUE @@ -105,7 +100,7 @@ internal abstract class AbstractArgumentSingleValue(descriptor: Descript override fun saveValue(stringValue: String) { if (!valueIsInitialized()) { - parsedValue = descriptor.type.conversion(stringValue, descriptor.fullName!!) + parsedValue = descriptor.type.convert(stringValue, descriptor.fullName!!) valueOrigin = ArgParser.ValueOrigin.SET_BY_USER } else { throw ParsingException("Try to provide more than one value $parsedValue and $stringValue for ${descriptor.fullName}.") @@ -165,7 +160,7 @@ internal class ArgumentMultipleValues(descriptor: Descriptor set(value) = setDelegatedValue(value) override fun saveValue(stringValue: String) { - addedValue.add(descriptor.type.conversion(stringValue, descriptor.fullName!!)) + addedValue.add(descriptor.type.convert(stringValue, descriptor.fullName!!)) valueOrigin = ArgParser.ValueOrigin.SET_BY_USER } diff --git a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Arguments.kt b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Arguments.kt index 9e3478e4b1c..7a02502bba6 100644 --- a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Arguments.kt +++ b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Arguments.kt @@ -9,46 +9,98 @@ import kotlin.reflect.KProperty internal data class CLIEntityWrapper(var entity: CLIEntity<*>? = null) /** - * Command line entity. + * Base interface for all possible types of entities with default and required values. + * Provides limitations for API that is accessible for different options/arguments types. + * Allows to save the reason why option/argument can(or can't) be omitted in command line. * - * @property owner parser which owns current entity. + * @see [SingleOption], [MultipleOption], [SingleArgument], [MultipleArgument]. */ -abstract class CLIEntity internal constructor(internal val owner: CLIEntityWrapper) { +interface DefaultRequiredType { /** - * Wrapper for element - read only property. - * Needed to close set of variable [cliElement]. + * Type of an entity with default value. */ - lateinit var delegate: ArgumentValueDelegate - internal set + class Default : DefaultRequiredType /** - * Value of entity. + * Type of an entity which value should always be provided in command line. + */ + class Required : DefaultRequiredType + + /** + * Type of entity which is optional and has no default value. + */ + class None : DefaultRequiredType +} + +/** + * The base class for a command line argument or an option. + */ +abstract class CLIEntity internal constructor(val delegate: ArgumentValueDelegate, + internal val owner: CLIEntityWrapper) { + /** + * The value of the option or argument parsed from command line. + * + * Accessing this property before it gets its value will result in an exception. + * You can use [valueOrigin] property to find out whether the property has been already set. + * + * @see ArgumentValueDelegate.value */ var value: TResult get() = delegate.value - set(value) { delegate.value = value } + set(value) { + check((delegate as ParsingValue<*, *>).valueOrigin != ArgParser.ValueOrigin.UNDEFINED) { + "Resetting value of option/argument is only possible after parsing command line arguments." + + " ArgParser.parse(...) method should be called before" + } + delegate.value = value + } /** - * Origin of argument value. + * The origin of the option/argument value. */ val valueOrigin: ArgParser.ValueOrigin get() = (delegate as ParsingValue<*, *>).valueOrigin + private var delegateProvided = false + + /** + * Returns the delegate object for property delegation and initializes it with the name of the delegated property. + * + * This operator makes it possible to delegate a property to this instance. It returns [delegate] object + * to be used as an actual delegate and uses the name of the delegated property to initialize the full name + * of the option/argument if it wasn't done during construction of that option/argument. + * + * @throws IllegalStateException in case of trying to use same delegate several times. + */ operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgumentValueDelegate { + check(!delegateProvided) { + "There is already used delegate for ${(delegate as ParsingValue<*, *>).descriptor.fullName}." + } (delegate as ParsingValue<*, *>).provideName(prop.name) + delegateProvided = true return delegate } } /** - * Argument instance. + * The base class for command line arguments. + * + * You can use [ArgParser.argument] function to declare an argument. */ -abstract class Argument internal constructor(owner: CLIEntityWrapper): CLIEntity(owner) +abstract class Argument internal constructor(delegate: ArgumentValueDelegate, + owner: CLIEntityWrapper): CLIEntity(delegate, owner) /** - * Common single argument instance. + * The base class of an argument with a single value. + * + * A non-optional argument or an optional argument with a default value is represented with the [SingleArgument] inheritor. + * An optional argument having nullable value is represented with the [SingleNullableArgument] inheritor. */ -abstract class AbstractSingleArgument internal constructor(owner: CLIEntityWrapper): Argument(owner) { +// TODO: investigate if we can collapse two inheritors into the single base class and specialize extensions by TResult upper bound +abstract class AbstractSingleArgument internal constructor( + delegate: ArgumentValueDelegate, + owner: CLIEntityWrapper): + Argument(delegate, owner) { /** * Check descriptor for this kind of argument. */ @@ -60,51 +112,55 @@ abstract class AbstractSingleArgument internal constructor(owne } /** - * Argument with single non-nullable value. + * A non-optional argument or an optional argument with a default value. + * + * The [value] of such argument is non-null. */ -class SingleArgument internal constructor(descriptor: ArgDescriptor, owner: CLIEntityWrapper): - AbstractSingleArgument(owner) { +class SingleArgument internal constructor(descriptor: ArgDescriptor, + owner: CLIEntityWrapper): + AbstractSingleArgument(ArgumentSingleValue(descriptor), owner) { init { checkDescriptor(descriptor) - delegate = ArgumentSingleValue(descriptor) } } /** - * Argument with single nullable value. + * An optional argument with nullable [value]. */ class SingleNullableArgument internal constructor(descriptor: ArgDescriptor, owner: CLIEntityWrapper): - AbstractSingleArgument(owner){ + AbstractSingleArgument(ArgumentSingleNullableValue(descriptor), owner) { init { checkDescriptor(descriptor) - delegate = ArgumentSingleNullableValue(descriptor) } } /** - * Argument with multiple values. + * An argument that allows several values to be provided in command line string. + * + * The [value] property of such argument has type `List`. */ -class MultipleArgument internal constructor(descriptor: ArgDescriptor>, owner: CLIEntityWrapper): - Argument>(owner) { +class MultipleArgument internal constructor( + descriptor: ArgDescriptor>, owner: CLIEntityWrapper): + Argument>(ArgumentMultipleValues(descriptor), owner) { init { if (descriptor.number != null && descriptor.number < 2) { failAssertion("Argument with multiple values can't be initialized with descriptor for single one.") } - delegate = ArgumentMultipleValues(descriptor) } } /** - * Allow argument have several values. + * Allows the argument to have several values specified in command line string. * - * @param value number of arguments are expected. + * @param number the exact number of values expected for this argument, but at least 2. + * + * @throws IllegalArgumentException if number of values expected for this argument less than 2. */ -fun AbstractSingleArgument.multiple(value: Int): MultipleArgument { - if (value < 2) { - error("multiple() modifier with value less than 2 is unavailable. It's already set to 1.") - } +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, value, description, listOfNotNull(defaultValue), + MultipleArgument(ArgDescriptor(type, fullName, number, description, listOfNotNull(defaultValue), required, deprecatedWarning), owner) } owner.entity = newArgument @@ -112,11 +168,12 @@ fun AbstractSingleArgument.multiple(value: Int): } /** - * Allow argument have several values. + * Allows the last argument to take all the trailing values in command line string. */ -fun AbstractSingleArgument.vararg(): MultipleArgument { +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, listOfNotNull(defaultValue), required, deprecatedWarning), owner) } owner.entity = newArgument @@ -124,29 +181,35 @@ fun AbstractSingleArgument.vararg(): MultipleArgu } /** - * Set default value for argument. + * Specifies the default value for the argument, that will be used when no value is provided for the argument + * in command line string. * - * @param value default value. + * Argument becomes optional, because value for it is set even if it isn't provided in command line. + * + * @param value the default value. */ -fun AbstractSingleArgument.default(value: T): SingleArgument { +fun SingleNullableArgument.default(value: T): SingleArgument { val newArgument = with((delegate as ParsingValue).descriptor as ArgDescriptor) { - SingleArgument(ArgDescriptor(type, fullName, number, description, value, required, deprecatedWarning), owner) + SingleArgument(ArgDescriptor(type, fullName, number, description, value, + false, deprecatedWarning), owner) } owner.entity = newArgument return newArgument } /** - * Set default value for argument. + * Specifies the default value for the argument with multiple values, that will be used when no values are provided + * for the argument in command line string. * - * @param value default value. + * Argument becomes optional, because value for it is set even if it isn't provided in command line. + * + * @param value the default value, must be a non-empty collection. */ -fun MultipleArgument.default(value: Collection): MultipleArgument { - if (value.isEmpty()) { - error("Default value for argument can't be empty collection.") - } +fun MultipleArgument.default(value: Collection): + 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(), + MultipleArgument(ArgDescriptor(type, fullName, number, description, value.toList(), required, deprecatedWarning), owner) } owner.entity = newArgument @@ -154,9 +217,13 @@ fun MultipleArgument.default(value: Collection): MultipleArgument } /** - * Allow argument be unprovided in command line. + * Allows the argument to have no value specified in command line string. + * + * The value of the argument is `null` in case if no value was specified in command line string. + * + * Note that only trailing arguments can be optional, i.e. no required arguments can follow optional ones. */ -fun SingleArgument.optional(): SingleNullableArgument { +fun SingleArgument.optional(): SingleNullableArgument { val newArgument = with((delegate as ParsingValue).descriptor as ArgDescriptor) { SingleNullableArgument(ArgDescriptor(type, fullName, number, description, defaultValue, false, deprecatedWarning), owner) @@ -166,15 +233,19 @@ fun SingleArgument.optional(): SingleNullableArgument { } /** - * Allow argument be unprovided in command line. + * Allows the argument with multiple values to have no values specified in command line string. + * + * The value of the argument is an empty list in case if no value was specified in command line string. + * + * Note that only trailing arguments can be optional: no required arguments can follow the optional ones. */ -fun MultipleArgument.optional(): MultipleArgument { +fun MultipleArgument.optional(): MultipleArgument { val newArgument = with((delegate as ParsingValue>).descriptor as ArgDescriptor) { - MultipleArgument(ArgDescriptor(type, fullName, number, description, + MultipleArgument(ArgDescriptor(type, fullName, number, description, defaultValue?.toList() ?: listOf(), false, deprecatedWarning), owner) } owner.entity = newArgument return newArgument } -fun failAssertion(message: String): Nothing = throw AssertionError(message) \ No newline at end of file +internal fun failAssertion(message: String): Nothing = throw AssertionError(message) \ No newline at end of file diff --git a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Descriptors.kt b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Descriptors.kt index 5d3eb05d820..098fc4adfa2 100644 --- a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Descriptors.kt +++ b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Descriptors.kt @@ -11,7 +11,7 @@ package kotlinx.cli * @property fullName option/argument full name. * @property description text description of option/argument. * @property defaultValue default value for option/argument. - * @property required if option/argument is required or not. If it's required and not provided in command line and have no default value, error will be generated. + * @property required if option/argument is required or not. If it's required and not provided in command line, error will be generated. * @property deprecatedWarning text message with information in case if option is deprecated. */ internal abstract class Descriptor(val type: ArgType, @@ -36,7 +36,7 @@ internal abstract class Descriptor(val type: ArgType, */ fun valueDescription(value: TResult?) = value?.let { if (it is List<*> && it.isNotEmpty()) - " [${it.joinToString { it.toString() }}]" + " [${it.joinToString()}]" else if (it !is List<*>) " [$it]" else null @@ -62,7 +62,7 @@ internal abstract class Descriptor(val type: ArgType, * @property shortName option short name. * @property description text description of option. * @property defaultValue default value for option. - * @property required if option is required or not. If it's required and not provided in command line and have no default value, error will be generated. + * @property required if option is required or not. If it's required and not provided in command line, error will be generated. * @property multiple if option can be repeated several times in command line with different values. All values are stored. * @property delimiter delimiter that separate option provided as one string to several values. * @property deprecatedWarning text message with information in case if option is deprecated. @@ -90,9 +90,9 @@ internal class OptionDescriptor( result.append(" $optionFullFormPrefix$fullName") shortName?.let { result.append(", $optionShortFromPrefix$it") } valueDescription(defaultValue)?.let { - result.append("$it") + result.append(it) } - description?.let {result.append(" -> ${it}")} + description?.let {result.append(" -> $it")} if (required) result.append(" (always required)") result.append(" ${type.description}") deprecatedWarning?.let { result.append(" Warning: $it") } @@ -140,9 +140,9 @@ internal class ArgDescriptor( val result = StringBuilder() result.append(" ${fullName}") valueDescription(defaultValue)?.let { - result.append("$it") + result.append(it) } - description?.let { result.append(" -> ${it}") } + description?.let { result.append(" -> $it") } if (!required) result.append(" (optional)") result.append(" ${type.description}") deprecatedWarning?.let { result.append(" Warning: $it") } diff --git a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ExperimentalCli.kt b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ExperimentalCli.kt index 77c979a3333..e073381fa96 100644 --- a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ExperimentalCli.kt +++ b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ExperimentalCli.kt @@ -32,5 +32,5 @@ import kotlin.annotation.AnnotationTarget.* PROPERTY_SETTER, TYPEALIAS ) -@SinceKotlin("1.3") + public annotation class ExperimentalCli \ No newline at end of file diff --git a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Options.kt b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Options.kt index cc1088fe658..2a5508acd30 100644 --- a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Options.kt +++ b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/Options.kt @@ -4,37 +4,49 @@ */ package kotlinx.cli -import kotlin.reflect.KProperty - /** * Base interface for all possible types of options with multiple values. + * Provides limitations for API that is accessible for options with several values. + * Allows to save the way of providing several values in command line. + * + * @see [MultipleOption] */ -interface MultipleOptionType +interface MultipleOptionType { + /** + * Type of an option with multiple values allowed to be provided several times in command line. + */ + class Repeated : MultipleOptionType + + /** + * Type of an option with multiple values allowed to be provided using delimiter in one command line value. + */ + class Delimited : MultipleOptionType + + /** + * Type of an option with multiple values allowed to be provided several times in command line + * both with specifying several values and with delimiter. + */ + class RepeatedDelimited : MultipleOptionType +} /** - * Type of option with multiple values that can be provided several times in command line. + * The base class for command line options. + * + * You can use [ArgParser.option] function to declare an option. */ -class RepeatedOption: MultipleOptionType +abstract class Option internal constructor(delegate: ArgumentValueDelegate, + owner: CLIEntityWrapper) : CLIEntity(delegate, owner) /** - * Type of option with multiple values that are provided using delimiter. + * The base class of an option with a single value. + * + * A required option or an option with a default value is represented with the [SingleOption] inheritor. + * An option having nullable value is represented with the [SingleNullableOption] inheritor. */ -class DelimitedOption: MultipleOptionType - -/** - * Type of option with multiple values that can be both provided several times in command line and using delimiter. - */ -class RepeatedDelimitedOption: MultipleOptionType - -/** - * Option instance. - */ -abstract class Option internal constructor(owner: CLIEntityWrapper): CLIEntity(owner) - -/** - * Common single option instance. - */ -abstract class AbstractSingleOption internal constructor(owner: CLIEntityWrapper): Option(owner) { +abstract class AbstractSingleOption internal constructor( + delegate: ArgumentValueDelegate, + owner: CLIEntityWrapper) : + Option(delegate, owner) { /** * Check descriptor for this kind of option. */ @@ -46,154 +58,208 @@ abstract class AbstractSingleOption internal constructor(owner: } /** - * Option wit single non-nullable value. + * A required option or an option with a default value. + * + * The [value] of such option is non-null. */ -class SingleOption internal constructor(descriptor: OptionDescriptor, owner: CLIEntityWrapper): - AbstractSingleOption(owner) { +class SingleOption internal constructor(descriptor: OptionDescriptor, + owner: CLIEntityWrapper) : + AbstractSingleOption(ArgumentSingleValue(descriptor), owner) { init { checkDescriptor(descriptor) - delegate = ArgumentSingleValue(descriptor) } } /** - * Option with single nullable value. + * An option with nullable [value]. */ -class SingleNullableOption internal constructor(descriptor: OptionDescriptor, owner: CLIEntityWrapper): - AbstractSingleOption(owner) { +class SingleNullableOption internal constructor(descriptor: OptionDescriptor, owner: CLIEntityWrapper) : + AbstractSingleOption(ArgumentSingleNullableValue(descriptor), owner) { init { checkDescriptor(descriptor) - delegate = ArgumentSingleNullableValue(descriptor) } } /** - * Option with multiple values. + * An option that allows several values to be provided in command line string. + * + * The [value] property of such option has type `List`. */ -class MultipleOption internal constructor(descriptor: OptionDescriptor>, owner: CLIEntityWrapper): - Option>(owner) { +class MultipleOption internal constructor( + descriptor: OptionDescriptor>, + owner: CLIEntityWrapper +) : + Option>( ArgumentMultipleValues(descriptor), owner) { init { if (!descriptor.multiple && descriptor.delimiter == null) { failAssertion("Option with multiple values can't be initialized with descriptor for single one.") } - delegate = ArgumentMultipleValues(descriptor) } } /** - * Allow option have several values. + * Allows the option to have several values specified in command line string. + * Number of values is unlimited. */ -fun AbstractSingleOption.multiple(): MultipleOption { +fun AbstractSingleOption.multiple(): + MultipleOption { val newOption = with((delegate as ParsingValue).descriptor as OptionDescriptor) { - MultipleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, + MultipleOption( + OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, description, listOfNotNull(defaultValue), - required, true, delimiter, deprecatedWarning), owner) + required, true, delimiter, deprecatedWarning + ), owner + ) } owner.entity = newOption return newOption } /** - * Allow option have several values. + * Allows the option to have several values specified in command line string. + * Number of values is unlimited. */ -fun MultipleOption.multiple(): MultipleOption { +fun MultipleOption.multiple(): + MultipleOption { val newOption = with((delegate as ParsingValue>).descriptor as OptionDescriptor) { if (multiple) { error("Try to use modifier multiple() twice on option ${fullName ?: ""}") } - MultipleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, + MultipleOption( + OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, description, defaultValue?.toList() ?: listOf(), - required, true, delimiter, deprecatedWarning), owner) + required, true, delimiter, deprecatedWarning + ), owner + ) } owner.entity = newOption return newOption } /** - * Set default option 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 default value. + * @param value the default value. */ -fun AbstractSingleOption.default(value: T): SingleOption { +fun SingleNullableOption.default(value: T): SingleOption { val newOption = with((delegate as ParsingValue).descriptor as OptionDescriptor) { - SingleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, - description, value, required, multiple, delimiter, deprecatedWarning), owner) + SingleOption( + OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, + description, value, required, multiple, delimiter, deprecatedWarning + ), owner + ) } owner.entity = newOption return newOption } /** - * Set default option 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. * - * @param value default value. + * @param value the default value, must be a non-empty collection. + * @throws IllegalArgumentException if provided default value is empty collection. */ -fun - MultipleOption.default(value: Collection): MultipleOption { +fun + MultipleOption.default(value: Collection): + MultipleOption { val newOption = with((delegate as ParsingValue>).descriptor as OptionDescriptor) { - if (value.isEmpty()) { - error("Default value for option can't be empty collection.") - } - MultipleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, + require(value.isNotEmpty()) { "Default value for option can't be empty collection." } + MultipleOption( + OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, description, value.toList(), - required, multiple, delimiter, deprecatedWarning), owner) + required, multiple, delimiter, deprecatedWarning + ), owner + ) } owner.entity = newOption return newOption } /** - * Require option to be always provided in command line. + * Requires the option to be always provided in command line. */ -fun SingleNullableOption.required(): SingleOption { +fun SingleNullableOption.required(): SingleOption { val newOption = with((delegate as ParsingValue).descriptor as OptionDescriptor) { - SingleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, + SingleOption( + OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, description, defaultValue, - true, multiple, delimiter, deprecatedWarning), owner) + true, multiple, delimiter, deprecatedWarning + ), owner + ) } owner.entity = newOption return newOption } /** - * Require option to be always provided in command line. + * Requires the option to be always provided in command line. */ -fun - MultipleOption.required(): MultipleOption { +fun + MultipleOption.required(): + MultipleOption { val newOption = with((delegate as ParsingValue>).descriptor as OptionDescriptor) { - MultipleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, + MultipleOption( + OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, description, defaultValue?.toList() ?: listOf(), - true, multiple, delimiter, deprecatedWarning), owner) + true, multiple, delimiter, deprecatedWarning + ), owner + ) } owner.entity = newOption return newOption } /** - * Allow provide several options using [delimiter]. + * Allows the option to have several values joined with [delimiter] specified in command line string. + * Number of values is unlimited. * - * @param delimiterValue delimiter used to separate string value to option values. + * The value of the argument is an empty list in case if no value was specified in command line string. + * + * @param delimiterValue delimiter used to separate string value to option values list. */ -fun AbstractSingleOption.delimiter(delimiterValue: String): MultipleOption { +fun AbstractSingleOption.delimiter( + delimiterValue: String): + MultipleOption { val newOption = with((delegate as ParsingValue).descriptor as OptionDescriptor) { - MultipleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, + MultipleOption( + OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, description, listOfNotNull(defaultValue), - required, multiple, delimiterValue, deprecatedWarning), owner) + required, multiple, delimiterValue, deprecatedWarning + ), owner + ) } owner.entity = newOption return newOption } /** - * Allow provide several options using [delimiter]. + * Allows the option to have several values joined with [delimiter] specified in command line string. + * Number of values is unlimited. * - * @param delimiterValue delimiter used to separate string value to option values. + * The value of the argument is an empty list in case if no value was specified in command line string. + * + * @param delimiterValue delimiter used to separate string value to option values list. */ -fun MultipleOption.delimiter(delimiterValue: String): MultipleOption { +fun MultipleOption.delimiter( + delimiterValue: String): + MultipleOption { val newOption = with((delegate as ParsingValue>).descriptor as OptionDescriptor) { - MultipleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, + MultipleOption( + OptionDescriptor( + optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName, description, defaultValue?.toList() ?: listOf(), - required, multiple, delimiterValue, deprecatedWarning), owner) + required, multiple, delimiterValue, deprecatedWarning + ), owner + ) } owner.entity = newOption return newOption diff --git a/endorsedLibraries/kotlinx.cli/src/tests/HelpTests.kt b/endorsedLibraries/kotlinx.cli/src/tests/HelpTests.kt index 57a634b7f52..973a0cd4197 100644 --- a/endorsedLibraries/kotlinx.cli/src/tests/HelpTests.kt +++ b/endorsedLibraries/kotlinx.cli/src/tests/HelpTests.kt @@ -28,6 +28,8 @@ class HelpTests { val user by argParser.option(ArgType.String, shortName = "u", description = "User access information for authorization") argParser.parse(arrayOf("main.txt")) val helpOutput = argParser.makeUsage().trimIndent() + @Suppress("CanBeVal") // can't be val in order to build expectedOutput only in run time. + var epsDefault = 1.0 val expectedOutput = """ Usage: test options_list Arguments: @@ -35,7 +37,7 @@ Arguments: compareToReport -> Report to compare to (optional) { String } Options: --output, -o -> Output file { String } - --eps, -e [1.0] -> Meaningful performance changes { Double } + --eps, -e [$epsDefault] -> Meaningful performance changes { Double } --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] } --user, -u -> User access information for authorization { String } diff --git a/endorsedLibraries/kotlinx.cli/src/tests/OptionsTests.kt b/endorsedLibraries/kotlinx.cli/src/tests/OptionsTests.kt index 94814546ce8..f1d31565358 100644 --- a/endorsedLibraries/kotlinx.cli/src/tests/OptionsTests.kt +++ b/endorsedLibraries/kotlinx.cli/src/tests/OptionsTests.kt @@ -5,8 +5,6 @@ package kotlinx.cli -import kotlinx.cli.ArgParser -import kotlinx.cli.ArgType import kotlin.test.* class OptionsTests { @@ -32,7 +30,7 @@ class OptionsTests { @Test fun testJavaPrefix() { - val argParser = ArgParser("testParser", prefixStyle = ArgParser.OPTION_PREFIX_STYLE.JVM) + val argParser = ArgParser("testParser", prefixStyle = ArgParser.OptionPrefixStyle.JVM) val output by argParser.option(ArgType.String, "output", "o", "Output file") val input by argParser.option(ArgType.String, "input", "i", "Input file") argParser.parse(arrayOf("-output", "out.txt", "-i", "input.txt"))