CLI library rework (#3215)
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
def endorsedLibrariesList = ['kliopt']
|
||||
def endorsedLibrariesList = ['kotlinx.cli']
|
||||
|
||||
def toTaskName(library) {
|
||||
def name = ""
|
||||
library.split("\\.").each { word -> name += word.capitalize() }
|
||||
return name
|
||||
}
|
||||
|
||||
task clean {
|
||||
doLast {
|
||||
@@ -16,11 +22,11 @@ task jvmJar {
|
||||
targetList.each { target ->
|
||||
task("${target}EndorsedLibraries", type: Copy) {
|
||||
endorsedLibrariesList.each { library ->
|
||||
dependsOn "$library:${target}${library.capitalize()}"
|
||||
dependsOn "$library:${target}${ toTaskName(library) }"
|
||||
}
|
||||
destinationDir project.buildDir
|
||||
endorsedLibrariesList.each { library ->
|
||||
from(project("$library").file("build/${target}${library.capitalize()}")) {
|
||||
from(project("$library").file("build/${target}${ toTaskName(library) }")) {
|
||||
include('**')
|
||||
into("$library")
|
||||
}
|
||||
|
||||
@@ -1,863 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kliopt
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
internal expect fun exitProcess(status: Int): Nothing
|
||||
|
||||
/**
|
||||
* Queue of arguments descriptors.
|
||||
* Arguments can have several values, so one descriptor can be returned several times.
|
||||
*/
|
||||
internal class ArgumentsQueue(argumentsDescriptors: List<ArgParser.ArgDescriptor<*>>) {
|
||||
/**
|
||||
* Map of arguments descriptors and their current usage number.
|
||||
*/
|
||||
private val argumentsUsageNumber = linkedMapOf(*argumentsDescriptors.map { it to 0 }.toTypedArray())
|
||||
|
||||
/**
|
||||
* Get next descriptor from queue.
|
||||
*/
|
||||
fun pop(): String? {
|
||||
if (argumentsUsageNumber.isEmpty())
|
||||
return null
|
||||
|
||||
val (currentDescriptor, usageNumber) = argumentsUsageNumber.iterator().next()
|
||||
currentDescriptor.number?.let {
|
||||
// Parse all arguments for current argument description.
|
||||
if (usageNumber + 1 >= currentDescriptor.number) {
|
||||
// All needed arguments were provided.
|
||||
argumentsUsageNumber.remove(currentDescriptor)
|
||||
} else {
|
||||
argumentsUsageNumber[currentDescriptor] = usageNumber + 1
|
||||
}
|
||||
}
|
||||
return currentDescriptor.fullName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for subcommands.
|
||||
*/
|
||||
@SinceKotlin("1.3")
|
||||
@ExperimentalCli
|
||||
abstract class Subcommand(val name: String): ArgParser(name) {
|
||||
/**
|
||||
* Execute action if subcommand was provided.
|
||||
*/
|
||||
abstract fun execute()
|
||||
}
|
||||
|
||||
/**
|
||||
* Common descriptor both for options and positional arguments.
|
||||
*
|
||||
* @property type option/argument type, one of [ArgType].
|
||||
* @property fullName option/argument full name.
|
||||
* @property description text descrition 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 deprecatedWarning text message with information in case if option is deprecated.
|
||||
*/
|
||||
abstract class Descriptor<T : Any>(val type: ArgType<T>,
|
||||
val fullName: String,
|
||||
val description: String? = null,
|
||||
val defaultValue: List<T> = emptyList(),
|
||||
val required: Boolean = false,
|
||||
val deprecatedWarning: String? = null) {
|
||||
/**
|
||||
* Text description for help message.
|
||||
*/
|
||||
abstract val textDescription: String
|
||||
/**
|
||||
* Help message for descriptor.
|
||||
*/
|
||||
abstract val helpMessage: String
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument parsing result.
|
||||
* Contains name of subcommand which was called.
|
||||
*
|
||||
* @property commandName name of command which was called.
|
||||
*/
|
||||
class ArgParserResult(val commandName: String)
|
||||
|
||||
/**
|
||||
* Arguments parser.
|
||||
*
|
||||
* @property programName name of current program.
|
||||
* @property useDefaultHelpShortName add or not -h flag for hrlp message.
|
||||
* @property prefixStyle style of expected options prefix.
|
||||
* @property skipExtraArguments just skip extra arhuments in command line string without producing 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) {
|
||||
|
||||
/**
|
||||
* Map of options: key - fullname of option, value - pair of descriptor and parsed values.
|
||||
*/
|
||||
protected val options = mutableMapOf<String, ParsingValue<*, *>>()
|
||||
/**
|
||||
* Map of arguments: key - fullname of argument, value - pair of descriptor and parsed values.
|
||||
*/
|
||||
protected val arguments = mutableMapOf<String, ParsingValue<*, *>>()
|
||||
/**
|
||||
* Map of subcommands.
|
||||
*/
|
||||
@UseExperimental(ExperimentalCli::class)
|
||||
protected val subcommands = mutableMapOf<String, Subcommand>()
|
||||
|
||||
/**
|
||||
* Mapping for short options names for quick search.
|
||||
*/
|
||||
private lateinit var shortNames: Map<String, ParsingValue<*, *>>
|
||||
|
||||
/**
|
||||
* Used prefix form for full option form.
|
||||
*/
|
||||
protected val optionFullFormPrefix = if (prefixStyle == OPTION_PREFIX_STYLE.LINUX) "--" else "-"
|
||||
|
||||
/**
|
||||
* Used prefix form for short option form.
|
||||
*/
|
||||
protected val optionShortFromPrefix = "-"
|
||||
|
||||
/**
|
||||
* Name with all commands that should be executed.
|
||||
*/
|
||||
protected val fullCommandName = mutableListOf<String>(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.
|
||||
*/
|
||||
enum class ValueOrigin { SET_BY_USER, SET_DEFAULT_VALUE, UNSET, REDEFINED }
|
||||
/**
|
||||
* 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 "-"
|
||||
*/
|
||||
enum class OPTION_PREFIX_STYLE { LINUX, JVM }
|
||||
|
||||
/**
|
||||
* Option descriptor.
|
||||
*
|
||||
* Command line entity started with some prefix (-/—) and can have value as next entity in command line string.
|
||||
*
|
||||
* @property type option type, one of [ArgType].
|
||||
* @property fullName option full name.
|
||||
* @property shortName option short name.
|
||||
* @property description text descrition 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 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.
|
||||
*/
|
||||
inner class OptionDescriptor<T : Any>(
|
||||
type: ArgType<T>,
|
||||
fullName: String,
|
||||
val shortName: String ? = null,
|
||||
description: String? = null,
|
||||
defaultValue: List<T> = emptyList(),
|
||||
required: Boolean = false,
|
||||
val multiple: Boolean = false,
|
||||
val delimiter: String? = null,
|
||||
deprecatedWarning: String? = null) : Descriptor<T> (type, fullName, description, defaultValue,
|
||||
required, deprecatedWarning) {
|
||||
|
||||
override val textDescription: String
|
||||
get() = "option $optionFullFormPrefix$fullName"
|
||||
|
||||
override val helpMessage: String
|
||||
get() {
|
||||
val result = StringBuilder()
|
||||
result.append(" $optionFullFormPrefix$fullName")
|
||||
shortName?.let { result.append(", $optionShortFromPrefix$it") }
|
||||
(defaultValue.joinToString(",") { it.toString() }).also { if (!it.isEmpty()) result.append(" [$it]") }
|
||||
description?.let {result.append(" -> ${it}")}
|
||||
if (required) result.append(" (always required)")
|
||||
result.append(" ${type.description}")
|
||||
deprecatedWarning?.let { result.append(" Warning: $it") }
|
||||
result.append("\n")
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument descriptor.
|
||||
*
|
||||
* Command line entity which role is connected only with its position.
|
||||
*
|
||||
* @property type argument type, one of [ArgType].
|
||||
* @property fullName argument full name.
|
||||
* @property number expected number of values. Null means any possible number of values.
|
||||
* @property description text descrition of argument.
|
||||
* @property defaultValue default value for argument.
|
||||
* @property required if 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 deprecatedWarning text message with information in case if argument is deprecated.
|
||||
*/
|
||||
inner class ArgDescriptor<T : Any>(
|
||||
type: ArgType<T>,
|
||||
fullName: String,
|
||||
val number: Int? = null,
|
||||
description: String? = null,
|
||||
defaultValue: List<T> = emptyList(),
|
||||
required: Boolean = true,
|
||||
deprecatedWarning: String? = null) : Descriptor<T> (type, fullName, description, defaultValue,
|
||||
required, deprecatedWarning) {
|
||||
|
||||
init {
|
||||
// Check arguments number correctness.
|
||||
number?.let {
|
||||
if (it < 0)
|
||||
printError("Number of arguments for argument description $fullName should be greater than zero.")
|
||||
}
|
||||
}
|
||||
|
||||
override val textDescription: String
|
||||
get() = "argument $fullName"
|
||||
|
||||
override val helpMessage: String
|
||||
get() {
|
||||
val result = StringBuilder()
|
||||
result.append(" ${fullName}")
|
||||
(defaultValue.joinToString(",") { it.toString() }).also { if (!it.isEmpty()) result.append(" [$it]") }
|
||||
description?.let { result.append(" -> ${it}") }
|
||||
if (!required) result.append(" (optional)")
|
||||
result.append(" ${type.description}")
|
||||
deprecatedWarning?.let { result.append(" Warning: $it") }
|
||||
result.append("\n")
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader for option with single possible value which is nullable.
|
||||
*/
|
||||
inner class SingleNullableOptionLoader<T : Any>(val type: ArgType<T>,
|
||||
val fullName: String? = null,
|
||||
val shortName: String ? = null,
|
||||
val description: String? = null,
|
||||
val required: Boolean = false,
|
||||
val deprecatedWarning: String? = null) {
|
||||
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgumentValueInterface<T?> {
|
||||
val name = fullName ?: prop.name
|
||||
val descriptor = OptionDescriptor(type, name, shortName, description, emptyList(),
|
||||
required, deprecatedWarning = deprecatedWarning)
|
||||
val cliElement = ArgumentSingleNullableValue(type.conversion)
|
||||
options[name] = ParsingValue(descriptor, cliElement)
|
||||
return cliElement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader for option with single possible value which has default value.
|
||||
*/
|
||||
inner class SingleOptionWithDefaultLoader<T : Any>(val type: ArgType<T>,
|
||||
val fullName: String? = null,
|
||||
val shortName: String ? = null,
|
||||
val description: String? = null,
|
||||
val defaultValue: T,
|
||||
val required: Boolean = false,
|
||||
val deprecatedWarning: String? = null) {
|
||||
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgumentValueInterface<T> {
|
||||
val name = fullName ?: prop.name
|
||||
val descriptor = OptionDescriptor(type, name, shortName, description, listOf(defaultValue),
|
||||
required, deprecatedWarning = deprecatedWarning)
|
||||
val cliElement = ArgumentSingleValueWithDefault(type.conversion)
|
||||
options[name] = ParsingValue(descriptor, cliElement)
|
||||
return cliElement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader for option with multiple possible values.
|
||||
*/
|
||||
inner class MultipleOptionsLoader<T : Any>(val type: ArgType<T>,
|
||||
val fullName: String? = null,
|
||||
val shortName: String ? = null,
|
||||
val description: String? = null,
|
||||
val defaultValue: List<T> = emptyList(),
|
||||
val required: Boolean = false,
|
||||
val multiple: Boolean = false,
|
||||
val delimiter: String? = null,
|
||||
val deprecatedWarning: String? = null) {
|
||||
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgumentValueInterface<MutableList<T>> {
|
||||
val name = fullName ?: prop.name
|
||||
val descriptor = OptionDescriptor(type, name, shortName, description, defaultValue,
|
||||
required, multiple, delimiter, deprecatedWarning)
|
||||
if (!multiple && delimiter == null)
|
||||
printError("Several values are expected for option $name. " +
|
||||
"Option must be used multiple times or split with delimiter.")
|
||||
val cliElement = ArgumentMultipleValues(type.conversion)
|
||||
options[name] = ParsingValue(descriptor, cliElement)
|
||||
return cliElement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add option with single possible value and get delegator to its value.
|
||||
*
|
||||
* @param type argument type, one of [ArgType].
|
||||
* @param fullName argument full name.
|
||||
* @param shortName option short name.
|
||||
* @param description text descrition of option.
|
||||
* @param 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.
|
||||
* @param deprecatedWarning text message with information in case if option is deprecated.
|
||||
*/
|
||||
fun <T : Any>option(type: ArgType<T>,
|
||||
fullName: String? = null,
|
||||
shortName: String ? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
deprecatedWarning: String? = null) = SingleNullableOptionLoader(type, fullName, shortName,
|
||||
description, required, deprecatedWarning)
|
||||
|
||||
/**
|
||||
* Add option with single possible value with default and get delegator to its value.
|
||||
*
|
||||
* @param type option type, one of [ArgType].
|
||||
* @param fullName option full name.
|
||||
* @param shortName option short name.
|
||||
* @param description text descrition of option.
|
||||
* @param defaultValue default value for option.
|
||||
* @param 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.
|
||||
* @param deprecatedWarning text message with information in case if option is deprecated.
|
||||
*/
|
||||
fun <T : Any>option(type: ArgType<T>,
|
||||
fullName: String? = null,
|
||||
shortName: String ? = null,
|
||||
description: String? = null,
|
||||
defaultValue: T,
|
||||
required: Boolean = false,
|
||||
deprecatedWarning: String? = null) = SingleOptionWithDefaultLoader(type, fullName, shortName,
|
||||
description, defaultValue, required, deprecatedWarning)
|
||||
|
||||
/**
|
||||
* Add option with multiple possible values and get delegator to its values.
|
||||
*
|
||||
* @param type option type, one of [ArgType].
|
||||
* @param fullName option full name.
|
||||
* @param shortName option short name.
|
||||
* @param description text descrition of option.
|
||||
* @param defaultValue default value for option.
|
||||
* @param 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.
|
||||
* @param multiple if option can be repeated several times in command line with different values. All values are stored.
|
||||
* @param delimiter delimiter that separate option provided as one string to several values.
|
||||
* @param deprecatedWarning text message with information in case if option is deprecated.
|
||||
*/
|
||||
fun <T : Any>options(type: ArgType<T>,
|
||||
fullName: String? = null,
|
||||
shortName: String ? = null,
|
||||
description: String? = null,
|
||||
defaultValue: List<T> = emptyList(),
|
||||
required: Boolean = false,
|
||||
multiple: Boolean = false,
|
||||
delimiter: String? = null,
|
||||
deprecatedWarning: String? = null) = MultipleOptionsLoader(type, fullName, shortName,
|
||||
description, defaultValue, required, multiple, delimiter, deprecatedWarning)
|
||||
|
||||
/**
|
||||
* Loader for argument with single possible value which is nullable.
|
||||
*/
|
||||
inner class SingleNullableArgumentLoader<T : Any>(val type: ArgType<T>,
|
||||
val fullName: String? = null,
|
||||
val description: String? = null,
|
||||
val required: Boolean = true,
|
||||
val deprecatedWarning: String? = null) {
|
||||
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgumentValueInterface<T?> {
|
||||
val name = fullName ?: prop.name
|
||||
val descriptor = ArgDescriptor(type, name, 1, description,
|
||||
emptyList(), required, deprecatedWarning)
|
||||
val cliElement = ArgumentSingleNullableValue(type.conversion)
|
||||
arguments[name] = ParsingValue(descriptor, cliElement)
|
||||
return cliElement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader for argument with single possible value which has default one.
|
||||
*/
|
||||
inner class SingleArgumentWithDefaultLoader<T : Any>(val type: ArgType<T>,
|
||||
val fullName: String? = null,
|
||||
val description: String? = null,
|
||||
val defaultValue: T,
|
||||
val required: Boolean = true,
|
||||
val deprecatedWarning: String? = null) {
|
||||
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgumentValueInterface<T> {
|
||||
val name = fullName ?: prop.name
|
||||
val descriptor = ArgDescriptor(type, name, 1, description,
|
||||
listOf(defaultValue), required, deprecatedWarning)
|
||||
val cliElement = ArgumentSingleValueWithDefault(type.conversion)
|
||||
arguments[name] = ParsingValue(descriptor, cliElement)
|
||||
return cliElement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader for option with multiple possible values.
|
||||
*/
|
||||
inner class MultipleArgumentsLoader<T : Any>(val type: ArgType<T>,
|
||||
val fullName: String? = null,
|
||||
val number: Int? = null,
|
||||
val description: String? = null,
|
||||
val defaultValue: List<T> = emptyList(),
|
||||
val required: Boolean = true,
|
||||
val deprecatedWarning: String? = null) {
|
||||
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgumentValueInterface<MutableList<T>> {
|
||||
val name = fullName ?: prop.name
|
||||
val descriptor = ArgDescriptor(type, name, number, description,
|
||||
defaultValue, required, deprecatedWarning)
|
||||
val cliElement = ArgumentMultipleValues(type.conversion)
|
||||
arguments[name] = ParsingValue(descriptor, cliElement)
|
||||
return cliElement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add argument with single nullable value and get delegator to its value.
|
||||
*
|
||||
* @param type argument type, one of [ArgType].
|
||||
* @param fullName argument full name.
|
||||
* @param description text descrition of argument.
|
||||
* @param required if argument is required or not. If it's required and not provided in command line and have no default value, error will be generated.
|
||||
* @param deprecatedWarning text message with information in case if argument is deprecated.
|
||||
*/
|
||||
fun <T : Any>argument(type: ArgType<T>,
|
||||
fullName: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = true,
|
||||
deprecatedWarning: String? = null) = SingleNullableArgumentLoader(type, fullName, description,
|
||||
required, deprecatedWarning)
|
||||
|
||||
/**
|
||||
* Add argument with single value with default and get delegator to its value.
|
||||
*
|
||||
* @param type argument type, one of [ArgType].
|
||||
* @param fullName argument full name.
|
||||
* @param description text descrition of argument.
|
||||
* @param defaultValue default value for argument.
|
||||
* @param required if argument is required or not. If it's required and not provided in command line and have no default value, error will be generated.
|
||||
* @param deprecatedWarning text message with information in case if argument is deprecated.
|
||||
*/
|
||||
fun <T : Any>argument(type: ArgType<T>,
|
||||
fullName: String? = null,
|
||||
description: String? = null,
|
||||
defaultValue: T,
|
||||
required: Boolean = true,
|
||||
deprecatedWarning: String? = null) = SingleArgumentWithDefaultLoader(type, fullName,
|
||||
description, defaultValue, required, deprecatedWarning )
|
||||
|
||||
/**
|
||||
* Add argument with [number] possible values and get delegator to its value.
|
||||
*
|
||||
* @param type argument type, one of [ArgType].
|
||||
* @param fullName argument full name.
|
||||
* @param number expected number of values. Null means any possible number of values.
|
||||
* @param description text descrition of argument.
|
||||
* @param defaultValue default value for argument.
|
||||
* @param required if argument is required or not. If it's required and not provided in command line and have no default value, error will be generated.
|
||||
* @param deprecatedWarning text message with information in case if argument is deprecated.
|
||||
*/
|
||||
fun <T : Any>arguments(type: ArgType<T>,
|
||||
fullName: String? = null,
|
||||
number: Int? = null,
|
||||
description: String? = null,
|
||||
defaultValue: List<T> = emptyList(),
|
||||
required: Boolean = true,
|
||||
deprecatedWarning: String? = null) = MultipleArgumentsLoader(type, fullName, number,
|
||||
description, defaultValue, required, deprecatedWarning)
|
||||
|
||||
/**
|
||||
* Add 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.")
|
||||
}
|
||||
|
||||
// Set same settings as main parser.
|
||||
it.prefixStyle = prefixStyle
|
||||
it.useDefaultHelpShortName = useDefaultHelpShortName
|
||||
fullCommandName.forEachIndexed { index, namePart ->
|
||||
it.fullCommandName.add(index, namePart)
|
||||
}
|
||||
subcommands[it.name] = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all free arguments as unnamed list.
|
||||
*
|
||||
* @param type argument type, one of [ArgType].
|
||||
* @param description text descrition of argument.
|
||||
* @param defaultValue default value for argument.
|
||||
* @param required if argument is required or not. If it's required and not provided in command line and have no default value, error will be generated.
|
||||
* @param deprecatedWarning text message with information in case if argument is deprecated.
|
||||
*/
|
||||
fun <T : Any>arguments(type: ArgType<T>,
|
||||
description: String? = null,
|
||||
defaultValue: List<T> = emptyList(),
|
||||
required: Boolean = true,
|
||||
deprecatedWarning: String? = null): ArgumentValueInterface<MutableList<T>> {
|
||||
val descriptor = ArgDescriptor(type, "", null, description,
|
||||
defaultValue, required, deprecatedWarning)
|
||||
val cliElement = ArgumentMultipleValues(type.conversion)
|
||||
if ("" in arguments) {
|
||||
printError("You can have only one unnamed list with positional arguments.")
|
||||
}
|
||||
arguments[""] = ParsingValue(descriptor, cliElement)
|
||||
return cliElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsing value of option/argument.
|
||||
*/
|
||||
protected inner class ParsingValue<T: Any, U: Any>(val descriptor: Descriptor<T>, val argumentValue: ArgumentValue<U>) {
|
||||
|
||||
/**
|
||||
* Add parsed value from command line.
|
||||
*/
|
||||
fun addValue(stringValue: String,
|
||||
setValue: ArgumentValue<U>.(String, String) -> Unit = ArgumentValue<U>::addValue) {
|
||||
// Check of possibility to set several values to one option/argument.
|
||||
if (descriptor is OptionDescriptor<*> && !descriptor.multiple &&
|
||||
!argumentValue.isEmpty() && descriptor.delimiter == null) {
|
||||
printError("Try to provide more than one value for ${descriptor.fullName}.")
|
||||
}
|
||||
// Show deprecated warning only first time of using option/argument.
|
||||
descriptor.deprecatedWarning?.let {
|
||||
if (argumentValue.isEmpty())
|
||||
println ("Warning: $it")
|
||||
}
|
||||
// Split value if needed.
|
||||
if (descriptor is OptionDescriptor<*> && descriptor.delimiter != null) {
|
||||
stringValue.split(descriptor.delimiter).forEach {
|
||||
argumentValue.setValue(it, descriptor.fullName)
|
||||
}
|
||||
} else {
|
||||
argumentValue.setValue(stringValue, descriptor.fullName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default value to option.
|
||||
*/
|
||||
fun addDefaultValue() {
|
||||
descriptor.defaultValue.forEach {
|
||||
addValue(it.toString(), ArgumentValue<U>::addDefaultValue)
|
||||
}
|
||||
|
||||
if (descriptor.defaultValue.isEmpty() && descriptor.required) {
|
||||
printError("Please, provide value for ${descriptor.textDescription}. It should be always set.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface of argument value.
|
||||
*/
|
||||
interface ArgumentValueInterface<T> {
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument/option value.
|
||||
*/
|
||||
abstract class ArgumentValue<T : Any>(val conversion: (value: String, name: String, helpMessage: String)->T) {
|
||||
/**
|
||||
* Values of arguments.
|
||||
*/
|
||||
protected lateinit var values: T
|
||||
/**
|
||||
* Value origin.
|
||||
*/
|
||||
var valueOrigin = ValueOrigin.UNSET
|
||||
protected set
|
||||
|
||||
/**
|
||||
* Add value from command line.
|
||||
*
|
||||
* @param stringValue value from command line.
|
||||
* @param argumentName name of argument value is added for.
|
||||
*/
|
||||
abstract fun addValue(stringValue: String, argumentName: String)
|
||||
|
||||
/**
|
||||
* Add default value.
|
||||
*
|
||||
* @param stringValue value from command line.
|
||||
* @param argumentName name of argument value is added for.
|
||||
*/
|
||||
fun addDefaultValue(stringValue: String, argumentName: String) {
|
||||
addValue(stringValue, argumentName)
|
||||
valueOrigin = ValueOrigin.SET_DEFAULT_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if values of argument are empty.
|
||||
*/
|
||||
abstract fun isEmpty(): Boolean
|
||||
|
||||
/**
|
||||
* Check if value of argument was initialized.
|
||||
*/
|
||||
protected fun valuesAreInitialized() = ::values.isInitialized
|
||||
|
||||
/**
|
||||
* Set value from delegated property.
|
||||
*/
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
values = value
|
||||
valueOrigin = ValueOrigin.REDEFINED
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single argument value.
|
||||
*
|
||||
* @property conversion conversion function from string value from command line to expected type.
|
||||
*/
|
||||
inner abstract class ArgumentSingleValue<T : Any>(conversion: (value: String, name: String, helpMessage: String)->T):
|
||||
ArgumentValue<T>(conversion) {
|
||||
|
||||
override fun addValue(stringValue: String, argumentName: String) {
|
||||
if (!valuesAreInitialized()) {
|
||||
values = conversion(stringValue, argumentName, makeUsage())
|
||||
valueOrigin = ValueOrigin.SET_BY_USER
|
||||
} else {
|
||||
printError("Try to provide more than one value $values and $stringValue for $argumentName.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean = !valuesAreInitialized()
|
||||
}
|
||||
|
||||
/**
|
||||
* Single nullable argument value.
|
||||
*
|
||||
* @property conversion conversion function from string value from command line to expected type.
|
||||
*/
|
||||
inner class ArgumentSingleNullableValue<T : Any>(conversion: (value: String, name: String, helpMessage: String)->T):
|
||||
ArgumentSingleValue<T>(conversion), ArgumentValueInterface<T?> {
|
||||
override operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = if (!isEmpty()) values else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Single argument value with default.
|
||||
*
|
||||
* @property conversion conversion function from string value from command line to expected type.
|
||||
*/
|
||||
inner class ArgumentSingleValueWithDefault<T : Any>(conversion: (value: String, name: String, helpMessage: String)->T):
|
||||
ArgumentSingleValue<T>(conversion), ArgumentValueInterface<T> {
|
||||
override operator fun getValue(thisRef: Any?, property: KProperty<*>): T = values
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple argument values.
|
||||
*
|
||||
* @property conversion conversion function from string value from command line to expected type.
|
||||
*/
|
||||
inner class ArgumentMultipleValues<T : Any>(conversion: (value: String, name: String, helpMessage: String)->T):
|
||||
ArgumentValue<MutableList<T>> (
|
||||
{ value, name, _ -> mutableListOf(conversion(value, name, makeUsage())) }
|
||||
), ArgumentValueInterface<MutableList<T>> {
|
||||
|
||||
init {
|
||||
values = mutableListOf()
|
||||
}
|
||||
|
||||
override operator fun getValue(thisRef: Any?, property: KProperty<*>): MutableList<T> = values
|
||||
|
||||
override fun addValue(stringValue: String, argumentName: String) {
|
||||
values.addAll(conversion(stringValue, argumentName, makeUsage()))
|
||||
valueOrigin = ValueOrigin.SET_BY_USER
|
||||
}
|
||||
|
||||
override fun isEmpty() = values.isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Output error. Also adds help usage information for easy understanding of problem.
|
||||
*
|
||||
* @param message error message.
|
||||
*/
|
||||
fun printError(message: String): Nothing {
|
||||
error("$message\n${makeUsage()}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get origin of option value.
|
||||
*
|
||||
* @param name name of argument/option.
|
||||
*/
|
||||
fun getOrigin(name: String) = options[name]?.argumentValue?.valueOrigin ?:
|
||||
arguments[name]?.argumentValue?.valueOrigin ?: printError("No option/argument $name in list of avaliable options")
|
||||
|
||||
/**
|
||||
* Save value as argument value.
|
||||
*
|
||||
* @param arg string with argument value.
|
||||
* @param argumentsQueue queue with active argument descriptors.
|
||||
*/
|
||||
private fun saveAsArg(arg: String, argumentsQueue: ArgumentsQueue): Boolean {
|
||||
// Find next uninitialized arguments.
|
||||
val name = argumentsQueue.pop()
|
||||
name?.let {
|
||||
val argumentValue = arguments[name]!!
|
||||
argumentValue.descriptor.deprecatedWarning?.let { println ("Warning: $it") }
|
||||
argumentValue.addValue(arg)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Save value as option value.
|
||||
*/
|
||||
private fun <T : Any, U: Any> saveAsOption(parsingValue: ParsingValue<T, U>, value: String) {
|
||||
parsingValue.addValue(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to recognize command line element as full form of option.
|
||||
*
|
||||
* @param candidate string with candidate in options.
|
||||
*/
|
||||
protected fun recognizeOptionFullForm(candidate: String) =
|
||||
if (candidate.startsWith(optionFullFormPrefix))
|
||||
options[candidate.substring(optionFullFormPrefix.length)]
|
||||
else null
|
||||
|
||||
/**
|
||||
* Try to recognize command line element as short form of option.
|
||||
*
|
||||
* @param candidate string with candidate in options.
|
||||
*/
|
||||
protected fun recognizeOptionShortForm(candidate: String) =
|
||||
if (candidate.startsWith(optionShortFromPrefix))
|
||||
shortNames[candidate.substring(optionShortFromPrefix.length)]
|
||||
else null
|
||||
|
||||
/**
|
||||
* Parse arguments.
|
||||
*
|
||||
* @param args array with command line arguments.
|
||||
*
|
||||
* @return true if all arguments were parsed successfully, otherwise return false and print help message.
|
||||
*/
|
||||
fun parse(args: Array<String>): ArgParserResult {
|
||||
// Add help option.
|
||||
val helpDescriptor = if (useDefaultHelpShortName) OptionDescriptor(ArgType.Boolean,
|
||||
"help", "h", "Usage info")
|
||||
else OptionDescriptor(ArgType.Boolean, "help", description = "Usage info")
|
||||
options["help"] = ParsingValue(helpDescriptor, ArgumentSingleNullableValue(helpDescriptor.type.conversion))
|
||||
|
||||
// Add default list with arguments if there can be extra free arguments.
|
||||
if (skipExtraArguments) {
|
||||
arguments(ArgType.String)
|
||||
}
|
||||
val argumentsQueue = ArgumentsQueue(arguments.map { it.value.descriptor as ArgDescriptor<*> })
|
||||
|
||||
// Fill map with short names of options.
|
||||
shortNames = options.filter { (it.value.descriptor as? OptionDescriptor<*>)?.shortName != null }.
|
||||
map { (it.value.descriptor as OptionDescriptor<*>).shortName!! to it.value }.toMap()
|
||||
|
||||
var index = 0
|
||||
while (index < args.size) {
|
||||
val arg = args[index]
|
||||
// Check for subcommands.
|
||||
@UseExperimental(ExperimentalCli::class)
|
||||
subcommands.forEach { (name, subcommand) ->
|
||||
if (arg == name) {
|
||||
// Use parser for this subcommand.
|
||||
subcommand.parse(args.slice(index + 1..args.size - 1).toTypedArray())
|
||||
subcommand.execute()
|
||||
|
||||
return ArgParserResult(name)
|
||||
}
|
||||
}
|
||||
// Parse argumnets from command line.
|
||||
if (arg.startsWith('-')) {
|
||||
// Candidate in being option.
|
||||
// Option is found.
|
||||
val argValue = recognizeOptionShortForm(arg) ?: recognizeOptionFullForm(arg)
|
||||
argValue?.descriptor?.let {
|
||||
if (argValue.descriptor.type.hasParameter) {
|
||||
if (index < args.size - 1) {
|
||||
saveAsOption(argValue, args[index + 1])
|
||||
index++
|
||||
} else {
|
||||
// An error, option with value without value.
|
||||
printError("No value for ${argValue.descriptor.textDescription}")
|
||||
}
|
||||
} else {
|
||||
// Boolean flags.
|
||||
if (argValue.descriptor.fullName == "help") {
|
||||
println(makeUsage())
|
||||
exitProcess(0)
|
||||
}
|
||||
saveAsOption(argValue, "true")
|
||||
}
|
||||
} ?: run {
|
||||
// Try save as argument.
|
||||
if (!saveAsArg(arg, argumentsQueue)) {
|
||||
printError("Unknown option $arg")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Argument is found.
|
||||
if (!saveAsArg(arg, argumentsQueue)) {
|
||||
printError("Too many arguments! Couldn't proccess argument $arg!")
|
||||
}
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
// Postprocess results of parsing.
|
||||
options.values.union(arguments.values).forEach { value ->
|
||||
// Not inited, append default value if needed.
|
||||
if (value.argumentValue.isEmpty()) {
|
||||
value.addDefaultValue()
|
||||
}
|
||||
}
|
||||
return ArgParserResult(programName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create message with usage description.
|
||||
*/
|
||||
internal fun makeUsage(): String {
|
||||
val result = StringBuilder()
|
||||
result.append("Usage: ${fullCommandName.joinToString(" ")} options_list\n")
|
||||
if (!arguments.isEmpty()) {
|
||||
result.append("Arguments: \n")
|
||||
arguments.forEach {
|
||||
result.append(it.value.descriptor.helpMessage)
|
||||
}
|
||||
}
|
||||
result.append("Options: \n")
|
||||
options.forEach {
|
||||
result.append(it.value.descriptor.helpMessage)
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ kotlin {
|
||||
|
||||
jvm().compilations.all {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = ["-Xuse-experimental=org.jetbrains.kliopt.ExperimentalCli"]
|
||||
freeCompilerArgs = ["-Xuse-experimental=kotlinx.cli.ExperimentalCli"]
|
||||
suppressWarnings = true
|
||||
}
|
||||
}
|
||||
@@ -76,23 +76,23 @@ targetList.each { target ->
|
||||
"-Djava.library.path=${project.buildDir}/nativelibs/$hostName",
|
||||
]
|
||||
|
||||
def defaultArgs = ['-nopack', '-nodefaultlibs']
|
||||
def defaultArgs = ['-nopack', '-no-default-libs', '-no-endorsed-libs']
|
||||
if (target != "wasm32") defaultArgs += '-g'
|
||||
def konanArgs = [*defaultArgs,
|
||||
'-target', target,
|
||||
"-Xruntime=${project(':runtime').file('build/' + target + '/runtime.bc')}",
|
||||
*project.globalBuildArgs]
|
||||
|
||||
task("${target}Kliopt", type: JavaExec) {
|
||||
task("${target}KotlinxCli", type: JavaExec) {
|
||||
dependsOn ":${target}CrossDistRuntime"
|
||||
|
||||
main = 'org.jetbrains.kotlin.cli.bc.K2NativeKt'
|
||||
classpath = project(":backend.native").configurations.cli_bc
|
||||
jvmArgs = konanJvmArgs
|
||||
args = [*konanArgs,
|
||||
'-output', project.file("build/${target}Kliopt"),
|
||||
'-produce', 'library', '-module-name', 'kliopt', '-XXLanguage:+AllowContractsForCustomFunctions',
|
||||
'-Xmulti-platform', '-Xuse-experimental=org.jetbrains.kliopt.ExperimentalCli',
|
||||
'-output', project.file("build/${target}KotlinxCli"),
|
||||
'-produce', 'library', '-module-name', 'kotlinx-cli', '-XXLanguage:+AllowContractsForCustomFunctions',
|
||||
'-Xmulti-platform', '-Xuse-experimental=kotlinx.cli.ExperimentalCli',
|
||||
'-Xuse-experimental=kotlin.ExperimentalMultiplatform',
|
||||
'-Xallow-result-return-type',
|
||||
commonSrc.absolutePath,
|
||||
@@ -100,6 +100,6 @@ targetList.each { target ->
|
||||
nativeSrc]
|
||||
inputs.dir(nativeSrc)
|
||||
inputs.dir(commonSrc)
|
||||
outputs.dir(project.file("build/${target}Kliopt"))
|
||||
outputs.dir(project.file("build/${target}KotlinxCli"))
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kliopt
|
||||
package kotlinx.cli
|
||||
|
||||
internal actual fun exitProcess(status: Int): Nothing {
|
||||
error("Not implemented for JS!")
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kliopt
|
||||
package kotlinx.cli
|
||||
|
||||
internal actual fun exitProcess(status: Int): Nothing {
|
||||
kotlin.system.exitProcess(0)
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kliopt
|
||||
package kotlinx.cli
|
||||
|
||||
internal actual fun exitProcess(status: Int): Nothing {
|
||||
kotlin.system.exitProcess(0)
|
||||
@@ -0,0 +1,449 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
internal expect fun exitProcess(status: Int): Nothing
|
||||
|
||||
/**
|
||||
* Queue of arguments descriptors.
|
||||
* Arguments can have several values, so one descriptor can be returned several times.
|
||||
*/
|
||||
internal class ArgumentsQueue(argumentsDescriptors: List<ArgDescriptor<*, *>>) {
|
||||
/**
|
||||
* Map of arguments descriptors and their current usage number.
|
||||
*/
|
||||
private val argumentsUsageNumber = linkedMapOf(*argumentsDescriptors.map { it to 0 }.toTypedArray())
|
||||
|
||||
/**
|
||||
* Get next descriptor from queue.
|
||||
*/
|
||||
fun pop(): String? {
|
||||
if (argumentsUsageNumber.isEmpty())
|
||||
return null
|
||||
|
||||
val (currentDescriptor, usageNumber) = argumentsUsageNumber.iterator().next()
|
||||
currentDescriptor.number?.let {
|
||||
// Parse all arguments for current argument description.
|
||||
if (usageNumber + 1 >= currentDescriptor.number) {
|
||||
// All needed arguments were provided.
|
||||
argumentsUsageNumber.remove(currentDescriptor)
|
||||
} else {
|
||||
argumentsUsageNumber[currentDescriptor] = usageNumber + 1
|
||||
}
|
||||
}
|
||||
return currentDescriptor.fullName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface of argument value.
|
||||
*/
|
||||
interface ArgumentValueDelegate<T> {
|
||||
var value: T
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for subcommands.
|
||||
*/
|
||||
@SinceKotlin("1.3")
|
||||
@ExperimentalCli
|
||||
abstract class Subcommand(val name: String): ArgParser(name) {
|
||||
/**
|
||||
* Execute action if subcommand was provided.
|
||||
*/
|
||||
abstract fun execute()
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument parsing result.
|
||||
* Contains name of subcommand which was called.
|
||||
*
|
||||
* @property commandName name of command which was called.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
open class ArgParser(val programName: String, var useDefaultHelpShortName: Boolean = true,
|
||||
var prefixStyle: OPTION_PREFIX_STYLE = OPTION_PREFIX_STYLE.LINUX,
|
||||
var skipExtraArguments: Boolean = false) {
|
||||
|
||||
/**
|
||||
* Map of options: key - full name of option, value - pair of descriptor and parsed values.
|
||||
*/
|
||||
private val options = mutableMapOf<String, ParsingValue<*, *>>()
|
||||
/**
|
||||
* Map of arguments: key - full name of argument, value - pair of descriptor and parsed values.
|
||||
*/
|
||||
private val arguments = mutableMapOf<String, ParsingValue<*, *>>()
|
||||
|
||||
/**
|
||||
* Map with declared options.
|
||||
*/
|
||||
private val declaredOptions = mutableListOf<CLIEntityWrapper>()
|
||||
|
||||
/**
|
||||
* Map with declared arguments.
|
||||
*/
|
||||
private val declaredArguments = mutableListOf<CLIEntityWrapper>()
|
||||
|
||||
/**
|
||||
* Map of subcommands.
|
||||
*/
|
||||
@UseExperimental(ExperimentalCli::class)
|
||||
protected val subcommands = mutableMapOf<String, Subcommand>()
|
||||
|
||||
/**
|
||||
* Mapping for short options names for quick search.
|
||||
*/
|
||||
private val shortNames = mutableMapOf<String, ParsingValue<*, *>>()
|
||||
|
||||
/**
|
||||
* Used prefix form for full option form.
|
||||
*/
|
||||
private val optionFullFormPrefix = if (prefixStyle == OPTION_PREFIX_STYLE.LINUX) "--" else "-"
|
||||
|
||||
/**
|
||||
* Used prefix form for short option form.
|
||||
*/
|
||||
private val optionShortFromPrefix = "-"
|
||||
|
||||
/**
|
||||
* Name with all commands that should be executed.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
enum class ValueOrigin { SET_BY_USER, SET_DEFAULT_VALUE, UNSET, REDEFINED }
|
||||
|
||||
/**
|
||||
* 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 "-"
|
||||
*/
|
||||
enum class OPTION_PREFIX_STYLE { LINUX, JVM }
|
||||
|
||||
/**
|
||||
* Add option with single possible value and get delegator to its value.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
fun <T : Any>option(type: ArgType<T>,
|
||||
fullName: String? = null,
|
||||
shortName: String ? = null,
|
||||
description: String? = null,
|
||||
deprecatedWarning: String? = null): SingleNullableOption<T> {
|
||||
val option = SingleNullableOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type,
|
||||
fullName, shortName, description, deprecatedWarning = deprecatedWarning), CLIEntityWrapper())
|
||||
option.owner.entity = option
|
||||
declaredOptions.add(option.owner)
|
||||
return option
|
||||
}
|
||||
|
||||
/**
|
||||
* Check usage of required property for arguments.
|
||||
* Make sense only for several last arguments.
|
||||
*/
|
||||
private fun inspectRequiredAndDefaultUsage() {
|
||||
var previousArgument: ParsingValue<*, *>? = null
|
||||
arguments.forEach { (_, currentArgument) ->
|
||||
previousArgument?.let {
|
||||
// Previous argument has default value.
|
||||
it.descriptor.defaultValue?.let {
|
||||
if (currentArgument.descriptor.defaultValue == null && currentArgument.descriptor.required) {
|
||||
printWarning("Default value of argument ${previousArgument.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) {
|
||||
printWarning("Argument ${previousArgument.descriptor.fullName} will be always required, " +
|
||||
"because next argument ${currentArgument.descriptor.fullName} is always required.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add argument with single nullable value and get delegator to its value.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
fun <T : Any>argument(type: ArgType<T>,
|
||||
fullName: String? = null,
|
||||
description: String? = null,
|
||||
deprecatedWarning: String? = null) : SingleArgument<T> {
|
||||
val argument = SingleArgument(ArgDescriptor(type, fullName, 1,
|
||||
description, deprecatedWarning = deprecatedWarning), CLIEntityWrapper())
|
||||
argument.owner.entity = argument
|
||||
declaredArguments.add(argument.owner)
|
||||
return argument
|
||||
}
|
||||
|
||||
/**
|
||||
* Add 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.")
|
||||
}
|
||||
|
||||
// Set same settings as main parser.
|
||||
it.prefixStyle = prefixStyle
|
||||
it.useDefaultHelpShortName = useDefaultHelpShortName
|
||||
fullCommandName.forEachIndexed { index, namePart ->
|
||||
it.fullCommandName.add(index, namePart)
|
||||
}
|
||||
subcommands[it.name] = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output error. Also adds help usage information for easy understanding of problem.
|
||||
*
|
||||
* @param message error message.
|
||||
*/
|
||||
fun printError(message: String): Nothing {
|
||||
error("$message\n${makeUsage()}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Save value as argument value.
|
||||
*
|
||||
* @param arg string with argument value.
|
||||
* @param argumentsQueue queue with active argument descriptors.
|
||||
*/
|
||||
private fun saveAsArg(arg: String, argumentsQueue: ArgumentsQueue): Boolean {
|
||||
// Find next uninitialized arguments.
|
||||
val name = argumentsQueue.pop()
|
||||
name?.let {
|
||||
val argumentValue = arguments[name]!!
|
||||
argumentValue.descriptor.deprecatedWarning?.let { printWarning(it) }
|
||||
argumentValue.addValue(arg)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Save value as option value.
|
||||
*/
|
||||
private fun <T : Any, U: Any> saveAsOption(parsingValue: ParsingValue<T, U>, value: String) {
|
||||
parsingValue.addValue(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to recognize command line element as full form of option.
|
||||
*
|
||||
* @param candidate string with candidate in options.
|
||||
*/
|
||||
private fun recognizeOptionFullForm(candidate: String) =
|
||||
if (candidate.startsWith(optionFullFormPrefix))
|
||||
options[candidate.substring(optionFullFormPrefix.length)]
|
||||
else null
|
||||
|
||||
/**
|
||||
* Try to recognize command line element as short form of option.
|
||||
*
|
||||
* @param candidate string with candidate in options.
|
||||
*/
|
||||
private fun recognizeOptionShortForm(candidate: String) =
|
||||
if (candidate.startsWith(optionShortFromPrefix))
|
||||
shortNames[candidate.substring(optionShortFromPrefix.length)]
|
||||
else null
|
||||
|
||||
/**
|
||||
* Parse arguments.
|
||||
*
|
||||
* @param args array with command line arguments.
|
||||
*
|
||||
* @return true if all arguments were parsed successfully, otherwise return false and print help message.
|
||||
*/
|
||||
fun parse(args: Array<String>) = parse(args.asList())
|
||||
|
||||
protected fun parse(args: List<String>): ArgParserResult {
|
||||
// Add help option.
|
||||
val helpDescriptor = if (useDefaultHelpShortName) OptionDescriptor<Boolean, Boolean>(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)
|
||||
|
||||
// Add default list with arguments if there can be extra free arguments.
|
||||
if (skipExtraArguments) {
|
||||
argument(ArgType.String, "").number()
|
||||
}
|
||||
|
||||
// Map declared options and arguments to maps.
|
||||
declaredOptions.forEachIndexed { index, option ->
|
||||
val value = option.entity?.delegate as ParsingValue<*, *>
|
||||
value.descriptor.fullName?.let {
|
||||
// Add option.
|
||||
if (options.containsKey(it)) {
|
||||
error("Option with full name $it was already added.")
|
||||
}
|
||||
with(value.descriptor as OptionDescriptor) {
|
||||
if (shortName != null && shortNames.containsKey(shortName)) {
|
||||
error("Option with short name ${shortName} was already added.")
|
||||
}
|
||||
shortName?.let {
|
||||
shortNames[it] = value
|
||||
}
|
||||
}
|
||||
options[it] = value
|
||||
|
||||
} ?: error("Option was added, but unnamed. Added option under №${index + 1}")
|
||||
}
|
||||
|
||||
declaredArguments.forEachIndexed { index, argument ->
|
||||
val value = argument.entity?.delegate as ParsingValue<*, *>
|
||||
value.descriptor.fullName?.let {
|
||||
// Add option.
|
||||
if (arguments.containsKey(it)) {
|
||||
error("Argument with full name $it was already added.")
|
||||
}
|
||||
arguments[it] = value
|
||||
} ?: error("Argument was added, but unnamed. Added argument under №${index + 1}")
|
||||
}
|
||||
// Make inspections for arguments.
|
||||
inspectRequiredAndDefaultUsage()
|
||||
|
||||
val argumentsQueue = ArgumentsQueue(arguments.map { it.value.descriptor as ArgDescriptor<*, *> })
|
||||
|
||||
var index = 0
|
||||
try {
|
||||
while (index < args.size) {
|
||||
val arg = args[index]
|
||||
// Check for subcommands.
|
||||
@UseExperimental(ExperimentalCli::class)
|
||||
subcommands.forEach { (name, subcommand) ->
|
||||
if (arg == name) {
|
||||
// Use parser for this subcommand.
|
||||
subcommand.parse(args.slice(index + 1..args.size - 1))
|
||||
subcommand.execute()
|
||||
|
||||
return ArgParserResult(name)
|
||||
}
|
||||
}
|
||||
// Parse arguments from command line.
|
||||
if (arg.startsWith('-')) {
|
||||
// Candidate in being option.
|
||||
// Option is found.
|
||||
val argValue = recognizeOptionShortForm(arg) ?: recognizeOptionFullForm(arg)
|
||||
argValue?.descriptor?.let {
|
||||
if (argValue.descriptor.type.hasParameter) {
|
||||
if (index < args.size - 1) {
|
||||
saveAsOption(argValue, args[index + 1])
|
||||
index++
|
||||
} else {
|
||||
// An error, option with value without value.
|
||||
printError("No value for ${argValue.descriptor.textDescription}")
|
||||
}
|
||||
} else {
|
||||
// Boolean flags.
|
||||
if (argValue.descriptor.fullName == "help") {
|
||||
println(makeUsage())
|
||||
exitProcess(0)
|
||||
}
|
||||
saveAsOption(argValue, "true")
|
||||
}
|
||||
} ?: run {
|
||||
// Try save as argument.
|
||||
if (!saveAsArg(arg, argumentsQueue)) {
|
||||
printError("Unknown option $arg")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Argument is found.
|
||||
if (!saveAsArg(arg, argumentsQueue)) {
|
||||
printError("Too many arguments! Couldn't process argument $arg!")
|
||||
}
|
||||
}
|
||||
index++
|
||||
}
|
||||
// Postprocess results of parsing.
|
||||
options.values.union(arguments.values).forEach { value ->
|
||||
// Not inited, append default value if needed.
|
||||
if (value.isEmpty()) {
|
||||
value.addDefaultValue()
|
||||
}
|
||||
}
|
||||
} catch (exception: ParsingException) {
|
||||
printError(exception.message!!)
|
||||
}
|
||||
|
||||
return ArgParserResult(programName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create message with usage description.
|
||||
*/
|
||||
internal fun makeUsage(): String {
|
||||
val result = StringBuilder()
|
||||
result.append("Usage: ${fullCommandName.joinToString(" ")} options_list\n")
|
||||
if (arguments.isNotEmpty()) {
|
||||
result.append("Arguments: \n")
|
||||
arguments.forEach {
|
||||
result.append(it.value.descriptor.helpMessage)
|
||||
}
|
||||
}
|
||||
if (options.isNotEmpty()) {
|
||||
result.append("Options: \n")
|
||||
options.forEach {
|
||||
result.append(it.value.descriptor.helpMessage)
|
||||
}
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output warning.
|
||||
*
|
||||
* @param message warning message.
|
||||
*/
|
||||
internal fun printWarning(message: String) {
|
||||
println("WARNING $message")
|
||||
}
|
||||
+18
-18
@@ -3,7 +3,7 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kliopt
|
||||
package kotlinx.cli
|
||||
|
||||
/**
|
||||
* Possible types of arguments.
|
||||
@@ -23,7 +23,7 @@ abstract class ArgType<T : Any>(val hasParameter: kotlin.Boolean) {
|
||||
*
|
||||
* @param value value
|
||||
*/
|
||||
abstract val conversion: (value: kotlin.String, name: kotlin.String, helpMessage: kotlin.String)->T
|
||||
abstract val conversion: (value: kotlin.String, name: kotlin.String)->T
|
||||
|
||||
/**
|
||||
* Argument type for flags that can be only set/unset.
|
||||
@@ -32,8 +32,8 @@ abstract class ArgType<T : Any>(val hasParameter: kotlin.Boolean) {
|
||||
override val description: kotlin.String
|
||||
get() = ""
|
||||
|
||||
override val conversion: (value: kotlin.String, name: kotlin.String, _: kotlin.String) -> kotlin.Boolean
|
||||
get() = { value, _ , _ -> if (value == "false") false else true }
|
||||
override val conversion: (value: kotlin.String, name: kotlin.String) -> kotlin.Boolean
|
||||
get() = { value, _ -> if (value == "false") false else true }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,8 +43,8 @@ abstract class ArgType<T : Any>(val hasParameter: kotlin.Boolean) {
|
||||
override val description: kotlin.String
|
||||
get() = "{ String }"
|
||||
|
||||
override val conversion: (value: kotlin.String, name: kotlin.String, _: kotlin.String) -> kotlin.String
|
||||
get() = { value, _, _ -> value }
|
||||
override val conversion: (value: kotlin.String, name: kotlin.String) -> kotlin.String
|
||||
get() = { value, _ -> value }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,9 +54,9 @@ abstract class ArgType<T : Any>(val hasParameter: kotlin.Boolean) {
|
||||
override val description: kotlin.String
|
||||
get() = "{ Int }"
|
||||
|
||||
override val conversion: (value: kotlin.String, name: kotlin.String, helpMessage: kotlin.String) -> kotlin.Int
|
||||
get() = { value, name, helpMessage -> value.toIntOrNull()
|
||||
?: error("Option $name is expected to be integer number. $value is provided.\n$helpMessage") }
|
||||
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.") }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,10 +66,9 @@ abstract class ArgType<T : Any>(val hasParameter: kotlin.Boolean) {
|
||||
override val description: kotlin.String
|
||||
get() = "{ Double }"
|
||||
|
||||
override val conversion: (value: kotlin.String, name: kotlin.String,
|
||||
helpMessage: kotlin.String) -> kotlin.Double
|
||||
get() = { value, name, helpMessage -> value.toDoubleOrNull()
|
||||
?: error("Option $name is expected to be double number. $value is provided.\n$helpMessage") }
|
||||
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.") }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,9 +78,10 @@ abstract class ArgType<T : Any>(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,
|
||||
helpMessage: kotlin.String) -> kotlin.String
|
||||
get() = { value, name, helpMessage -> if (value in values) value
|
||||
else error("Option $name is expected to be one of $values. $value is provided.\n$helpMessage") }
|
||||
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.") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class ParsingException(message: String) : Exception(message)
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* Parsing value of option/argument.
|
||||
*/
|
||||
internal abstract class ParsingValue<T: Any, TResult: Any>(val descriptor: Descriptor<T, TResult>) {
|
||||
/**
|
||||
* Values of arguments.
|
||||
*/
|
||||
protected lateinit var parsedValue: TResult
|
||||
|
||||
/**
|
||||
* Value origin.
|
||||
*/
|
||||
var valueOrigin = ArgParser.ValueOrigin.UNSET
|
||||
protected set
|
||||
|
||||
/**
|
||||
* Check if values of argument are empty.
|
||||
*/
|
||||
abstract fun isEmpty(): Boolean
|
||||
|
||||
/**
|
||||
* Check if value of argument was initialized.
|
||||
*/
|
||||
protected fun valueIsInitialized() = ::parsedValue.isInitialized
|
||||
|
||||
/**
|
||||
* Sace value from command line.
|
||||
*
|
||||
* @param stringValue value from command line.
|
||||
*/
|
||||
protected abstract fun saveValue(stringValue: String)
|
||||
|
||||
/**
|
||||
* Set value of delegated property.
|
||||
*/
|
||||
fun setDelegatedValue(providedValue: TResult) {
|
||||
parsedValue = providedValue
|
||||
valueOrigin = ArgParser.ValueOrigin.REDEFINED
|
||||
}
|
||||
|
||||
/**
|
||||
* Add parsed value from command line.
|
||||
*
|
||||
* @param stringValue value from command line.
|
||||
*/
|
||||
internal fun addValue(stringValue: String) {
|
||||
// Check of possibility to set several values to one option/argument.
|
||||
if (descriptor is OptionDescriptor<*, *> && !descriptor.multiple &&
|
||||
!isEmpty() && descriptor.delimiter == null) {
|
||||
throw ParsingException("Try to provide more than one value for ${descriptor.fullName}.")
|
||||
}
|
||||
// Show deprecated warning only first time of using option/argument.
|
||||
descriptor.deprecatedWarning?.let {
|
||||
if (isEmpty())
|
||||
println ("Warning: $it")
|
||||
}
|
||||
// Split value if needed.
|
||||
if (descriptor is OptionDescriptor<*, *> && descriptor.delimiter != null) {
|
||||
stringValue.split(descriptor.delimiter).forEach {
|
||||
saveValue(it)
|
||||
}
|
||||
} else {
|
||||
saveValue(stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide name for CLI entity.
|
||||
*
|
||||
* @param name name for CLI entity.
|
||||
*/
|
||||
fun provideName(name: String) {
|
||||
descriptor.fullName ?: run { descriptor.fullName = name }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single argument value.
|
||||
*
|
||||
* @property descriptor descriptor of option/argument.
|
||||
*/
|
||||
internal abstract class AbstractArgumentSingleValue<T: Any>(descriptor: Descriptor<T, T>):
|
||||
ParsingValue<T, T>(descriptor) {
|
||||
|
||||
override fun saveValue(stringValue: String) {
|
||||
if (!valueIsInitialized()) {
|
||||
parsedValue = descriptor.type.conversion(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}.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean = !valueIsInitialized()
|
||||
}
|
||||
|
||||
/**
|
||||
* Single argument value.
|
||||
*
|
||||
* @property descriptor descriptor of option/argument.
|
||||
*/
|
||||
internal class ArgumentSingleValue<T: Any>(descriptor: Descriptor<T, T>): AbstractArgumentSingleValue<T>(descriptor),
|
||||
ArgumentValueDelegate<T> {
|
||||
override var value: T
|
||||
get() = parsedValue
|
||||
set(value) = setDelegatedValue(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Single nullable argument value.
|
||||
*
|
||||
* @property descriptor descriptor of option/argument.
|
||||
*/
|
||||
internal class ArgumentSingleNullableValue<T : Any>(descriptor: Descriptor<T, T>):
|
||||
AbstractArgumentSingleValue<T>(descriptor), ArgumentValueDelegate<T?> {
|
||||
private var setToNull = false
|
||||
override var value: T?
|
||||
get() = if (!isEmpty() && !setToNull) parsedValue else null
|
||||
set(providedValue) = providedValue?.let {
|
||||
setDelegatedValue(it)
|
||||
setToNull = false
|
||||
} ?: run {
|
||||
setToNull = true
|
||||
valueOrigin = ArgParser.ValueOrigin.REDEFINED
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple argument values.
|
||||
*
|
||||
* @property descriptor descriptor of option/argument.
|
||||
*/
|
||||
internal class ArgumentMultipleValues<T : Any>(descriptor: Descriptor<T, List<T>>):
|
||||
ParsingValue<T, List<T>>(descriptor), ArgumentValueDelegate<List<T>> {
|
||||
|
||||
private val addedValue = mutableListOf<T>()
|
||||
init {
|
||||
parsedValue = addedValue
|
||||
}
|
||||
|
||||
override var value: List<T>
|
||||
get() = parsedValue
|
||||
set(value) = setDelegatedValue(value)
|
||||
|
||||
override fun saveValue(stringValue: String) {
|
||||
addedValue.add(descriptor.type.conversion(stringValue, descriptor.fullName!!))
|
||||
valueOrigin = ArgParser.ValueOrigin.SET_BY_USER
|
||||
}
|
||||
|
||||
override fun isEmpty() = parsedValue.isEmpty()
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
internal data class CLIEntityWrapper(var entity: CLIEntity<*>? = null)
|
||||
|
||||
/**
|
||||
* Command line entity.
|
||||
*
|
||||
* @property owner parser which owns current entity.
|
||||
*/
|
||||
abstract class CLIEntity<TResult> internal constructor(internal val owner: CLIEntityWrapper) {
|
||||
/**
|
||||
* Wrapper for element - read only property.
|
||||
* Needed to close set of variable [cliElement].
|
||||
*/
|
||||
lateinit var delegate: ArgumentValueDelegate<TResult>
|
||||
internal set
|
||||
|
||||
/**
|
||||
* Value of entity.
|
||||
*/
|
||||
var value: TResult
|
||||
get() = delegate.value
|
||||
set(value) { delegate.value = value }
|
||||
|
||||
/**
|
||||
* Origin of argument value.
|
||||
*/
|
||||
val valueOrigin: ArgParser.ValueOrigin
|
||||
get() = (delegate as ParsingValue<*, *>).valueOrigin
|
||||
|
||||
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgumentValueDelegate<TResult> {
|
||||
(delegate as ParsingValue<*, *>).provideName(prop.name)
|
||||
return delegate
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument instance.
|
||||
*/
|
||||
abstract class Argument<TResult> internal constructor(owner: CLIEntityWrapper): CLIEntity<TResult>(owner)
|
||||
|
||||
/**
|
||||
* Common single argument instance.
|
||||
*/
|
||||
abstract class AbstractSingleArgument<T: Any, TResult> internal constructor(owner: CLIEntityWrapper): Argument<TResult>(owner) {
|
||||
/**
|
||||
* Check descriptor for this kind of argument.
|
||||
*/
|
||||
internal fun checkDescriptor(descriptor: ArgDescriptor<*, *>) {
|
||||
if (descriptor.number == null || descriptor.number > 1) {
|
||||
failAssertion("Argument with single value can't be initialized with descriptor for multiple values.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument with single non-nullable value.
|
||||
*/
|
||||
class SingleArgument<T : Any> internal constructor(descriptor: ArgDescriptor<T, T>, owner: CLIEntityWrapper):
|
||||
AbstractSingleArgument<T, T>(owner) {
|
||||
init {
|
||||
checkDescriptor(descriptor)
|
||||
delegate = ArgumentSingleValue(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument with single nullable value.
|
||||
*/
|
||||
class SingleNullableArgument<T : Any> internal constructor(descriptor: ArgDescriptor<T, T>, owner: CLIEntityWrapper):
|
||||
AbstractSingleArgument<T, T?>(owner){
|
||||
init {
|
||||
checkDescriptor(descriptor)
|
||||
delegate = ArgumentSingleNullableValue(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument with multiple values.
|
||||
*/
|
||||
class MultipleArgument<T : Any> internal constructor(descriptor: ArgDescriptor<T, List<T>>, owner: CLIEntityWrapper):
|
||||
Argument<List<T>>(owner) {
|
||||
init {
|
||||
if (descriptor.number != null && descriptor.number == 1) {
|
||||
failAssertion("Argument with multiple values can't be initialized with descriptor for single one.")
|
||||
}
|
||||
delegate = ArgumentMultipleValues(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow argument have several values.
|
||||
*
|
||||
* @param number number of arguments are expected. In case of null value any number of arguments can be set.
|
||||
*/
|
||||
fun <T : Any, TResult> AbstractSingleArgument<T, TResult>.number(value: Int? = null): MultipleArgument<T> {
|
||||
if (value != null && value == 1) {
|
||||
error("number() modifier with value 1 is unavailable. It's already set to 1.")
|
||||
}
|
||||
val newArgument = with((delegate as ParsingValue<T, T>).descriptor as ArgDescriptor) {
|
||||
MultipleArgument(ArgDescriptor(type, fullName, value, description, listOfNotNull(defaultValue),
|
||||
required, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newArgument
|
||||
return newArgument
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default value for argument.
|
||||
*
|
||||
* @param value default value.
|
||||
*/
|
||||
fun <T: Any, TResult> AbstractSingleArgument<T, TResult>.default(value: T): SingleArgument<T> {
|
||||
val newArgument = with((delegate as ParsingValue<T, T>).descriptor as ArgDescriptor) {
|
||||
SingleArgument(ArgDescriptor(type, fullName, number, description, value, required, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newArgument
|
||||
return newArgument
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default value for argument.
|
||||
*
|
||||
* @param value default value.
|
||||
*/
|
||||
fun <T: Any> MultipleArgument<T>.default(value: Collection<T>): MultipleArgument<T> {
|
||||
val newArgument = with((delegate as ParsingValue<T, List<T>>).descriptor as ArgDescriptor) {
|
||||
MultipleArgument(ArgDescriptor(type, fullName, number, description, value.toList(),
|
||||
required, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newArgument
|
||||
return newArgument
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow argument be unprovided in command line.
|
||||
*/
|
||||
fun <T: Any, TResult> AbstractSingleArgument<T, TResult>.optional(): SingleNullableArgument<T> {
|
||||
val newArgument = with((delegate as ParsingValue<T, T>).descriptor as ArgDescriptor) {
|
||||
SingleNullableArgument(ArgDescriptor(type, fullName, number, description, defaultValue,
|
||||
false, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newArgument
|
||||
return newArgument
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow argument be unprovided in command line.
|
||||
*/
|
||||
fun <T: Any> MultipleArgument<T>.optional(): MultipleArgument<T> {
|
||||
val newArgument = with((delegate as ParsingValue<T, List<T>>).descriptor as ArgDescriptor) {
|
||||
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)
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
package kotlinx.cli
|
||||
|
||||
/**
|
||||
* Common descriptor both for options and positional arguments.
|
||||
*
|
||||
* @property type option/argument type, one of [ArgType].
|
||||
* @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 deprecatedWarning text message with information in case if option is deprecated.
|
||||
*/
|
||||
internal abstract class Descriptor<T : Any, TResult>(val type: ArgType<T>,
|
||||
var fullName: String? = null,
|
||||
val description: String? = null,
|
||||
val defaultValue: TResult? = null,
|
||||
val required: Boolean = false,
|
||||
val deprecatedWarning: String? = null) {
|
||||
/**
|
||||
* Text description for help message.
|
||||
*/
|
||||
abstract val textDescription: String
|
||||
/**
|
||||
* Help message for descriptor.
|
||||
*/
|
||||
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 { it.toString() }}]"
|
||||
else if (it !is List<*>)
|
||||
" [$it]"
|
||||
else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to check if descriptor has set default value for option/argument.
|
||||
*/
|
||||
val defaultValueSet by lazy {
|
||||
defaultValue != null && (defaultValue is List<*> && defaultValue.isNotEmpty() || defaultValue !is List<*>)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Option descriptor.
|
||||
*
|
||||
* Command line entity started with some prefix (-/--) and can have value as next entity in command line string.
|
||||
*
|
||||
* @property optionFullFormPrefix prefix used before full form of option.
|
||||
* @property optionShortFromPrefix prefix used before short form of option.
|
||||
* @property type option type, one of [ArgType].
|
||||
* @property fullName option full name.
|
||||
* @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 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.
|
||||
*/
|
||||
internal class OptionDescriptor<T : Any, TResult>(
|
||||
val optionFullFormPrefix: String,
|
||||
val optionShortFromPrefix: String,
|
||||
type: ArgType<T>,
|
||||
fullName: String? = null,
|
||||
val shortName: String ? = null,
|
||||
description: String? = null,
|
||||
defaultValue: TResult? = null,
|
||||
required: Boolean = false,
|
||||
val multiple: Boolean = false,
|
||||
val delimiter: String? = null,
|
||||
deprecatedWarning: String? = null) : Descriptor<T, TResult>(type, fullName, description, defaultValue,
|
||||
required, deprecatedWarning) {
|
||||
|
||||
override val textDescription: String
|
||||
get() = "option $optionFullFormPrefix$fullName"
|
||||
|
||||
override val helpMessage: String
|
||||
get() {
|
||||
val result = StringBuilder()
|
||||
result.append(" $optionFullFormPrefix$fullName")
|
||||
shortName?.let { result.append(", $optionShortFromPrefix$it") }
|
||||
valueDescription(defaultValue)?.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") }
|
||||
result.append("\n")
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument descriptor.
|
||||
*
|
||||
* Command line entity which role is connected only with its position.
|
||||
*
|
||||
* @property type argument type, one of [ArgType].
|
||||
* @property fullName argument full name.
|
||||
* @property number expected number of values. Null means any possible number of values.
|
||||
* @property description text description of argument.
|
||||
* @property defaultValue default value for argument.
|
||||
* @property required if 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 deprecatedWarning text message with information in case if argument is deprecated.
|
||||
*/
|
||||
internal class ArgDescriptor<T : Any, TResult>(
|
||||
type: ArgType<T>,
|
||||
fullName: String?,
|
||||
val number: Int? = null,
|
||||
description: String? = null,
|
||||
defaultValue: TResult? = null,
|
||||
required: Boolean = true,
|
||||
deprecatedWarning: String? = null) : Descriptor<T, TResult>(type, fullName, description, defaultValue,
|
||||
required, deprecatedWarning) {
|
||||
|
||||
init {
|
||||
// Check arguments number correctness.
|
||||
number?.let {
|
||||
if (it < 0)
|
||||
error("Number of arguments for argument description $fullName should be greater than zero.")
|
||||
}
|
||||
}
|
||||
|
||||
override val textDescription: String
|
||||
get() = "argument $fullName"
|
||||
|
||||
override val helpMessage: String
|
||||
get() {
|
||||
val result = StringBuilder()
|
||||
result.append(" ${fullName}")
|
||||
valueDescription(defaultValue)?.let {
|
||||
result.append("$it")
|
||||
}
|
||||
description?.let { result.append(" -> ${it}") }
|
||||
if (!required) result.append(" (optional)")
|
||||
result.append(" ${type.description}")
|
||||
deprecatedWarning?.let { result.append(" Warning: $it") }
|
||||
result.append("\n")
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kliopt
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlin.annotation.AnnotationTarget.*
|
||||
|
||||
@@ -15,7 +15,7 @@ import kotlin.annotation.AnnotationTarget.*
|
||||
*
|
||||
* Any usage of a declaration annotated with `@ExperimentalCli` must be accepted either by
|
||||
* annotating that usage with the [UseExperimental] annotation, e.g. `@UseExperimental(ExperimentalCli::class)`,
|
||||
* or by using the compiler argument `-Xuse-experimental=org.jetbrains.kliopt.ExperimentalCli`.
|
||||
* or by using the compiler argument `-Xuse-experimental=kotlinx.cli.ExperimentalCli`.
|
||||
*/
|
||||
@Experimental(level = Experimental.Level.WARNING)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* Base interface for all possible types of options with multiple values.
|
||||
*/
|
||||
interface MultipleOptionType
|
||||
|
||||
/**
|
||||
* Type of option with multiple values that can be provided several times in command line.
|
||||
*/
|
||||
class RepeatedOption: MultipleOptionType
|
||||
|
||||
/**
|
||||
* Type of option with multiple values that are provided using delimiter.
|
||||
*/
|
||||
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<TResult> internal constructor(owner: CLIEntityWrapper): CLIEntity<TResult>(owner)
|
||||
|
||||
/**
|
||||
* Common single option instance.
|
||||
*/
|
||||
abstract class AbstractSingleOption<T: Any, TResult> internal constructor(owner: CLIEntityWrapper): Option<TResult>(owner) {
|
||||
/**
|
||||
* Check descriptor for this kind of option.
|
||||
*/
|
||||
internal fun checkDescriptor(descriptor: OptionDescriptor<*, *>) {
|
||||
if (descriptor.multiple || descriptor.delimiter != null) {
|
||||
failAssertion("Option with single value can't be initialized with descriptor for multiple values.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Option wit single non-nullable value.
|
||||
*/
|
||||
class SingleOption<T : Any> internal constructor(descriptor: OptionDescriptor<T, T>, owner: CLIEntityWrapper):
|
||||
AbstractSingleOption<T, T>(owner) {
|
||||
init {
|
||||
checkDescriptor(descriptor)
|
||||
delegate = ArgumentSingleValue(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Option with single nullable value.
|
||||
*/
|
||||
class SingleNullableOption<T : Any> internal constructor(descriptor: OptionDescriptor<T, T>, owner: CLIEntityWrapper):
|
||||
AbstractSingleOption<T, T?>(owner) {
|
||||
init {
|
||||
checkDescriptor(descriptor)
|
||||
delegate = ArgumentSingleNullableValue(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Option with multiple values.
|
||||
*/
|
||||
class MultipleOption<T : Any, OptionType: MultipleOptionType> internal constructor(descriptor: OptionDescriptor<T, List<T>>, owner: CLIEntityWrapper):
|
||||
Option<List<T>>(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.
|
||||
*/
|
||||
fun <T : Any, TResult> AbstractSingleOption<T, TResult>.multiple(): MultipleOption<T, RepeatedOption> {
|
||||
val newOption = with((delegate as ParsingValue<T, T>).descriptor as OptionDescriptor) {
|
||||
MultipleOption<T, RepeatedOption>(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
|
||||
description, listOfNotNull(defaultValue),
|
||||
required, true, delimiter, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newOption
|
||||
return newOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow option have several values.
|
||||
*/
|
||||
fun <T : Any> MultipleOption<T, DelimitedOption>.multiple(): MultipleOption<T, RepeatedDelimitedOption> {
|
||||
val newOption = with((delegate as ParsingValue<T, List<T>>).descriptor as OptionDescriptor) {
|
||||
if (multiple) {
|
||||
error("Try to use modifier multiple() twice on option ${fullName ?: ""}")
|
||||
}
|
||||
MultipleOption<T, RepeatedDelimitedOption>(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
|
||||
description, defaultValue?.toList() ?: listOf(),
|
||||
required, true, delimiter, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newOption
|
||||
return newOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default option value.
|
||||
*
|
||||
* @param value default value.
|
||||
*/
|
||||
fun <T: Any, TResult> AbstractSingleOption<T, TResult>.default(value: T): SingleOption<T> {
|
||||
val newOption = with((delegate as ParsingValue<T, T>).descriptor as OptionDescriptor) {
|
||||
if (required) {
|
||||
printWarning("required() is unneeded, because option with default value is defined.")
|
||||
}
|
||||
SingleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
|
||||
description, value, required, multiple, delimiter, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newOption
|
||||
return newOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default option value.
|
||||
*
|
||||
* @param value default value.
|
||||
*/
|
||||
fun <T: Any, OptionType: MultipleOptionType>
|
||||
MultipleOption<T, OptionType>.default(value: Collection<T>): MultipleOption<T, OptionType> {
|
||||
val newOption = with((delegate as ParsingValue<T, List<T>>).descriptor as OptionDescriptor) {
|
||||
if (required) {
|
||||
printWarning("required() is unneeded, because option with default value is defined.")
|
||||
}
|
||||
MultipleOption<T, OptionType>(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName,
|
||||
shortName, description, value.toList(),
|
||||
required, multiple, delimiter, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newOption
|
||||
return newOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Require option to be always provided in command line.
|
||||
*/
|
||||
fun <T: Any, TResult> AbstractSingleOption<T, TResult>.required(): SingleOption<T> {
|
||||
val newOption = with((delegate as ParsingValue<T, T>).descriptor as OptionDescriptor) {
|
||||
defaultValue?.let {
|
||||
printWarning("required() is unneeded, because option with default value is defined.")
|
||||
}
|
||||
SingleOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName,
|
||||
shortName, description, defaultValue,
|
||||
true, multiple, delimiter, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newOption
|
||||
return newOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Require option to be always provided in command line.
|
||||
*/
|
||||
fun <T: Any, OptionType: MultipleOptionType>
|
||||
MultipleOption<T, OptionType>.required(): MultipleOption<T, OptionType> {
|
||||
val newOption = with((delegate as ParsingValue<T, List<T>>).descriptor as OptionDescriptor) {
|
||||
if (required) {
|
||||
printWarning("required() is unneeded, because option with default value is defined.")
|
||||
}
|
||||
MultipleOption<T, OptionType>(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
|
||||
description, defaultValue?.toList() ?: listOf(),
|
||||
true, multiple, delimiter, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newOption
|
||||
return newOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow provide several options using [delimiter].
|
||||
*
|
||||
* @param delimiterValue delimiter used to separate string value to option values.
|
||||
*/
|
||||
fun <T : Any, TResult> AbstractSingleOption<T, TResult>.delimiter(delimiterValue: String): MultipleOption<T, DelimitedOption> {
|
||||
val newOption = with((delegate as ParsingValue<T, T>).descriptor as OptionDescriptor) {
|
||||
MultipleOption<T, DelimitedOption>(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
|
||||
description, listOfNotNull(defaultValue),
|
||||
required, multiple, delimiterValue, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newOption
|
||||
return newOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow provide several options using [delimiter].
|
||||
*
|
||||
* @param delimiterValue delimiter used to separate string value to option values.
|
||||
*/
|
||||
fun <T : Any> MultipleOption<T, RepeatedOption>.delimiter(delimiterValue: String): MultipleOption<T, RepeatedDelimitedOption> {
|
||||
val newOption = with((delegate as ParsingValue<T, List<T>>).descriptor as OptionDescriptor) {
|
||||
MultipleOption<T, RepeatedDelimitedOption>(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type, fullName, shortName,
|
||||
description, defaultValue?.toList() ?: listOf(),
|
||||
required, multiple, delimiterValue, deprecatedWarning), owner)
|
||||
}
|
||||
owner.entity = newOption
|
||||
return newOption
|
||||
}
|
||||
+6
-4
@@ -3,8 +3,10 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kliopt
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlinx.cli.ArgParser
|
||||
import kotlinx.cli.ArgType
|
||||
import kotlin.test.*
|
||||
|
||||
class ArgumentsTests {
|
||||
@@ -24,7 +26,7 @@ class ArgumentsTests {
|
||||
fun testArgumetsWithAnyNumberOfValues() {
|
||||
val argParser = ArgParser("testParser")
|
||||
val output by argParser.argument(ArgType.String, "output", "Output file")
|
||||
val inputs by argParser.arguments(ArgType.String, description = "Input files")
|
||||
val inputs by argParser.argument(ArgType.String, description = "Input files").number()
|
||||
argParser.parse(arrayOf("out.txt", "input1.txt", "input2.txt", "input3.txt",
|
||||
"input4.txt"))
|
||||
assertEquals("out.txt", output)
|
||||
@@ -34,7 +36,7 @@ class ArgumentsTests {
|
||||
@Test
|
||||
fun testArgumetsWithSeveralValues() {
|
||||
val argParser = ArgParser("testParser")
|
||||
val addendums by argParser.arguments(ArgType.Int, "addendums", 2, description = "Addendums")
|
||||
val addendums by argParser.argument(ArgType.Int, "addendums", description = "Addendums").number(2)
|
||||
val output by argParser.argument(ArgType.String, "output", "Output file")
|
||||
val debugMode by argParser.option(ArgType.Boolean, "debug", "d", "Debug mode")
|
||||
argParser.parse(arrayOf("2", "-d", "3", "out.txt"))
|
||||
@@ -48,7 +50,7 @@ class ArgumentsTests {
|
||||
@Test
|
||||
fun testSkippingExtraArguments() {
|
||||
val argParser = ArgParser("testParser", skipExtraArguments = true)
|
||||
val addendums by argParser.arguments(ArgType.Int, "addendums", 2, description = "Addendums")
|
||||
val addendums by argParser.argument(ArgType.Int, "addendums", description = "Addendums").number(2)
|
||||
val output by argParser.argument(ArgType.String, "output", "Output file")
|
||||
val debugMode by argParser.option(ArgType.Boolean, "debug", "d", "Debug mode")
|
||||
argParser.parse(arrayOf("2", "-d", "3", "out.txt", "something", "else", "in", "string"))
|
||||
+8
-7
@@ -3,20 +3,22 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kliopt
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlinx.cli.ArgParser
|
||||
import kotlinx.cli.ArgType
|
||||
import kotlin.test.*
|
||||
|
||||
class ErrorTests {
|
||||
@Test
|
||||
fun testExtraArguments() {
|
||||
val argParser = ArgParser("testParser")
|
||||
val addendums by argParser.arguments(ArgType.Int, "addendums", 2, description = "Addendums")
|
||||
val addendums by argParser.argument(ArgType.Int, "addendums", description = "Addendums").number(2)
|
||||
val output by argParser.argument(ArgType.String, "output", "Output file")
|
||||
val debugMode by argParser.option(ArgType.Boolean, "debug", "d", "Debug mode")
|
||||
val exception = assertFailsWith<IllegalStateException> { argParser.parse(
|
||||
arrayOf("2", "-d", "3", "out.txt", "something", "else", "in", "string")) }
|
||||
assertTrue("Too many arguments! Couldn't proccess argument something" in exception.message!!)
|
||||
assertTrue("Too many arguments! Couldn't process argument something" in exception.message!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -43,10 +45,9 @@ class ErrorTests {
|
||||
@Test
|
||||
fun testWrongChoice() {
|
||||
val argParser = ArgParser("testParser")
|
||||
val useShortForm by argParser.option(ArgType.Boolean, "short", "s", "Show short version of report",
|
||||
defaultValue = false)
|
||||
val renders by argParser.options(ArgType.Choice(listOf("text", "html")),
|
||||
"renders", "r", "Renders for showing information", listOf("text"), multiple = true)
|
||||
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")),
|
||||
"renders", "r", "Renders for showing information").multiple().default(listOf("text"))
|
||||
val exception = assertFailsWith<IllegalStateException> {
|
||||
argParser.parse(arrayOf("-r", "xml"))
|
||||
}
|
||||
+36
-31
@@ -3,8 +3,12 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:UseExperimental(ExperimentalCli::class)
|
||||
package org.jetbrains.kliopt
|
||||
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.*
|
||||
|
||||
@@ -13,19 +17,19 @@ class HelpTests {
|
||||
fun testHelpMessage() {
|
||||
val argParser = ArgParser("test")
|
||||
val mainReport by argParser.argument(ArgType.String, description = "Main report for analysis")
|
||||
val compareToReport by argParser.argument(ArgType.String, description = "Report to compare to", required = false)
|
||||
val compareToReport by argParser.argument(ArgType.String, description = "Report to compare to").optional()
|
||||
|
||||
val output by argParser.option(ArgType.String, shortName = "o", description = "Output file")
|
||||
val epsValue by argParser.option(ArgType.Double, "eps", "e", "Meaningful performance changes", 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",
|
||||
"Show short version of report", defaultValue = false)
|
||||
val renders by argParser.options(ArgType.Choice(listOf("text", "html", "teamcity", "statistics", "metrics")),
|
||||
shortName = "r", description = "Renders for showing information", defaultValue = listOf("text"), multiple = true)
|
||||
"Show short version of report").default(false)
|
||||
val renders by argParser.option(ArgType.Choice(listOf("text", "html", "teamcity", "statistics", "metrics")),
|
||||
shortName = "r", description = "Renders for showing information").multiple().default(listOf("text"))
|
||||
val user by argParser.option(ArgType.String, shortName = "u", description = "User access information for authorization")
|
||||
argParser.parse(arrayOf("-h"))
|
||||
argParser.parse(arrayOf("main.txt"))
|
||||
val helpOutput = argParser.makeUsage().trimIndent()
|
||||
val expectedOutput = """
|
||||
Usage: test options_list
|
||||
Usage: test options_list
|
||||
Arguments:
|
||||
mainReport -> Main report for analysis { String }
|
||||
compareToReport -> Report to compare to (optional) { String }
|
||||
@@ -37,31 +41,28 @@ Options:
|
||||
--user, -u -> User access information for authorization { String }
|
||||
--help, -h -> Usage info
|
||||
""".trimIndent()
|
||||
assertEquals(helpOutput, expectedOutput)
|
||||
assertEquals(expectedOutput, helpOutput)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHelpForSubcommands() {
|
||||
class Summary: Subcommand("summary") {
|
||||
val exec by option(ArgType.Choice(listOf("samples", "geomean")),
|
||||
description = "Execution time way of calculation", defaultValue = "geomean")
|
||||
val execSamples by options(ArgType.String, "exec-samples",
|
||||
description = "Samples used for execution time metric (value 'all' allows use all samples)",
|
||||
delimiter = ",")
|
||||
description = "Execution time way of calculation").default("geomean")
|
||||
val execSamples by option(ArgType.String, "exec-samples",
|
||||
description = "Samples used for execution time metric (value 'all' allows use all samples)").delimiter(",")
|
||||
val execNormalize by option(ArgType.String, "exec-normalize",
|
||||
description = "File with golden results which should be used for normalization")
|
||||
val compile by option(ArgType.Choice(listOf("samples", "geomean")),
|
||||
description = "Compile time way of calculation", defaultValue = "geomean")
|
||||
val compileSamples by options(ArgType.String, "compile-samples",
|
||||
description = "Samples used for compile time metric (value 'all' allows use all samples)",
|
||||
delimiter = ",")
|
||||
description = "Compile time way of calculation").default("geomean")
|
||||
val compileSamples by option(ArgType.String, "compile-samples",
|
||||
description = "Samples used for compile time metric (value 'all' allows use all samples)").delimiter(",")
|
||||
val compileNormalize by option(ArgType.String, "compile-normalize",
|
||||
description = "File with golden results which should be used for normalization")
|
||||
val codesize by option(ArgType.Choice(listOf("samples", "geomean")),
|
||||
description = "Code size way of calculation", defaultValue = "geomean")
|
||||
val codesizeSamples by options(ArgType.String, "codesize-samples",
|
||||
description = "Samples used for code size metric (value 'all' allows use all samples)",
|
||||
delimiter = ",")
|
||||
description = "Code size way of calculation").default("geomean")
|
||||
val codesizeSamples by option(ArgType.String, "codesize-samples",
|
||||
description = "Samples used for code size metric (value 'all' allows use all samples)").delimiter(",")
|
||||
val codesizeNormalize by option(ArgType.String, "codesize-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")
|
||||
@@ -75,21 +76,25 @@ Options:
|
||||
// Parse args.
|
||||
val argParser = ArgParser("test")
|
||||
argParser.subcommands(action)
|
||||
argParser.parse(arrayOf("summary", "-h"))
|
||||
val helpOutput = argParser.makeUsage().trimIndent()
|
||||
argParser.parse(arrayOf("summary", "out.txt"))
|
||||
val helpOutput = action.makeUsage().trimIndent()
|
||||
val expectedOutput = """
|
||||
Usage: test summary options_list
|
||||
Usage: test summary options_list
|
||||
Arguments:
|
||||
mainReport -> Main report for analysis { String }
|
||||
compareToReport -> Report to compare to (optional) { String }
|
||||
Options:
|
||||
--output, -o -> Output file { String }
|
||||
--eps, -e [1.0] -> 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] }
|
||||
--exec [geomean] -> Execution time way of calculation { Value should be one of [samples, geomean] }
|
||||
--exec-samples -> Samples used for execution time metric (value 'all' allows use all samples) { String }
|
||||
--exec-normalize -> File with golden results which should be used for normalization { String }
|
||||
--compile [geomean] -> Compile time way of calculation { Value should be one of [samples, geomean] }
|
||||
--compile-samples -> Samples used for compile time metric (value 'all' allows use all samples) { String }
|
||||
--compile-normalize -> File with golden results which should be used for normalization { String }
|
||||
--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-normalize -> File with golden results which should be used for normalization { String }
|
||||
--user, -u -> User access information for authorization { String }
|
||||
--help, -h -> Usage info
|
||||
""".trimIndent()
|
||||
assertEquals(helpOutput, expectedOutput)
|
||||
""".trimIndent()
|
||||
assertEquals(expectedOutput, helpOutput)
|
||||
}
|
||||
}
|
||||
+31
-7
@@ -3,8 +3,10 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kliopt
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlinx.cli.ArgParser
|
||||
import kotlinx.cli.ArgType
|
||||
import kotlin.test.*
|
||||
|
||||
class OptionsTests {
|
||||
@@ -41,9 +43,9 @@ class OptionsTests {
|
||||
@Test
|
||||
fun testMultipleOptions() {
|
||||
val argParser = ArgParser("testParser")
|
||||
val useShortForm by argParser.option(ArgType.Boolean, "short", "s", "Show short version of report", defaultValue = false)
|
||||
val renders by argParser.options(ArgType.Choice(listOf("text", "html", "xml", "json")),
|
||||
"renders", "r", "Renders for showing information", listOf("text"), multiple = true)
|
||||
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"))
|
||||
argParser.parse(arrayOf("-s", "-r", "text", "-r", "json"))
|
||||
assertEquals(true, useShortForm)
|
||||
assertEquals(2, renders.size)
|
||||
@@ -55,12 +57,34 @@ class OptionsTests {
|
||||
@Test
|
||||
fun testDefaultOptions() {
|
||||
val argParser = ArgParser("testParser")
|
||||
val useShortForm by argParser.option(ArgType.Boolean, "short", "s", "Show short version of report", defaultValue = false)
|
||||
val renders by argParser.options(ArgType.Choice(listOf("text", "html", "xml", "json")),
|
||||
"renders", "r", "Renders for showing information", listOf("text"), multiple = true)
|
||||
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 by argParser.option(ArgType.String, "output", "o", "Output file")
|
||||
argParser.parse(arrayOf("-o", "out.txt"))
|
||||
assertEquals(false, useShortForm)
|
||||
assertEquals("text", renders[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResetOptionsValues() {
|
||||
val argParser = ArgParser("testParser")
|
||||
val useShortFormOption = argParser.option(ArgType.Boolean, "short", "s", "Show short version of report").default(false)
|
||||
var useShortForm by useShortFormOption
|
||||
val rendersOption = argParser.option(ArgType.Choice(listOf("text", "html", "xml", "json")),
|
||||
"renders", "r", "Renders for showing information").multiple().default(listOf("text"))
|
||||
var renders by rendersOption
|
||||
val outputOption = argParser.option(ArgType.String, "output", "o", "Output file")
|
||||
var output by outputOption
|
||||
argParser.parse(arrayOf("-o", "out.txt"))
|
||||
output = null
|
||||
useShortForm = true
|
||||
renders = listOf()
|
||||
assertEquals(true, useShortForm)
|
||||
assertEquals(null, output)
|
||||
assertEquals(0, renders.size)
|
||||
assertEquals(ArgParser.ValueOrigin.REDEFINED, outputOption.valueOrigin)
|
||||
assertEquals(ArgParser.ValueOrigin.REDEFINED, useShortFormOption.valueOrigin)
|
||||
assertEquals(ArgParser.ValueOrigin.REDEFINED, rendersOption.valueOrigin)
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -3,8 +3,12 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:UseExperimental(ExperimentalCli::class)
|
||||
package org.jetbrains.kliopt
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlinx.cli.ArgParser
|
||||
import kotlinx.cli.ArgType
|
||||
import kotlinx.cli.ExperimentalCli
|
||||
import kotlinx.cli.Subcommand
|
||||
import kotlin.test.*
|
||||
|
||||
class SubcommandsTests {
|
||||
@@ -14,7 +18,7 @@ class SubcommandsTests {
|
||||
val output by argParser.option(ArgType.String, "output", "o", "Output file")
|
||||
class Summary: Subcommand("summary") {
|
||||
val invert by option(ArgType.Boolean, "invert", "i", "Invert results")
|
||||
val addendums by arguments(ArgType.Int, "addendums", description = "Addendums")
|
||||
val addendums by argument(ArgType.Int, "addendums", description = "Addendums").number()
|
||||
var result: Int = 0
|
||||
|
||||
override fun execute() {
|
||||
@@ -32,7 +36,7 @@ class SubcommandsTests {
|
||||
@Test
|
||||
fun testCommonOptions() {
|
||||
abstract class CommonOptions(name: String): Subcommand(name) {
|
||||
val numbers by arguments(ArgType.Int, "numbers", description = "Numbers")
|
||||
val numbers by argument(ArgType.Int, "numbers", description = "Numbers").number()
|
||||
}
|
||||
class Summary: CommonOptions("summary") {
|
||||
val invert by option(ArgType.Boolean, "invert", "i", "Invert results")
|
||||
@@ -70,7 +74,7 @@ class SubcommandsTests {
|
||||
val argParser = ArgParser("testParser")
|
||||
|
||||
class Summary: Subcommand("summary") {
|
||||
val addendums by arguments(ArgType.Int, "addendums", description = "Addendums")
|
||||
val addendums by argument(ArgType.Int, "addendums", description = "Addendums").number()
|
||||
var result: Int = 0
|
||||
|
||||
override fun execute() {
|
||||
Reference in New Issue
Block a user