CLI library rework (#3215)

This commit is contained in:
LepilkinaElena
2019-09-06 17:49:12 +03:00
committed by GitHub
parent 51d03ece0a
commit 8e0e346492
66 changed files with 1494 additions and 1099 deletions
+9 -3
View File
@@ -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"))
}
}
@@ -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!")
@@ -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)
@@ -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")
}
@@ -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()
}
}
@@ -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
}
@@ -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"))
@@ -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"))
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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() {