New CLI tool (#3085)

This commit is contained in:
LepilkinaElena
2019-07-22 15:55:38 +03:00
committed by GitHub
parent e881ebdf38
commit 3c2a5b0392
42 changed files with 1852 additions and 614 deletions
+29
View File
@@ -0,0 +1,29 @@
def endorsedLibrariesList = ['kliopt']
task clean {
doLast {
delete buildDir
}
}
task jvmJar {
endorsedLibrariesList.each { library ->
dependsOn "$library:jvmJar"
}
}
// Build all default libraries.
targetList.each { target ->
task("${target}EndorsedLibraries", type: Copy) {
endorsedLibrariesList.each { library ->
dependsOn "$library:${target}${library.capitalize()}"
}
destinationDir project.buildDir
endorsedLibrariesList.each { library ->
from(project("$library").file("build/${target}${library.capitalize()}")) {
include('**')
into("$library")
}
}
}
}
+105
View File
@@ -0,0 +1,105 @@
import org.jetbrains.kotlin.konan.target.HostManager
buildscript {
repositories {
maven {
url 'https://cache-redirector.jetbrains.com/jcenter'
}
maven {
url kotlinCompilerRepo
}
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
apply plugin: 'kotlin-multiplatform'
repositories {
maven {
url 'https://cache-redirector.jetbrains.com/jcenter'
}
maven {
url kotlinCompilerRepo
}
maven {
url buildKotlinCompilerRepo
}
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion"
}
kotlin.srcDir 'src/main/kotlin'
}
commonTest {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-test-common:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlinVersion"
}
kotlin.srcDir 'src/tests'
}
jvm().compilations.main.defaultSourceSet {
dependencies {
implementation kotlin('stdlib-jdk8')
}
kotlin.srcDir 'src/main/kotlin-jvm'
}
// JVM-specific tests and their dependencies:
jvm().compilations.test.defaultSourceSet {
dependencies {
implementation kotlin('test-junit')
}
}
jvm().compilations.all {
kotlinOptions {
freeCompilerArgs = ["-Xuse-experimental=org.jetbrains.kliopt.ExperimentalCli"]
suppressWarnings = true
}
}
}
}
def commonSrc = new File("$projectDir/src/main/kotlin")
def nativeSrc = new File("$projectDir/src/main/kotlin-native")
targetList.each { target ->
def konanJvmArgs = [*HostManager.regularJvmArgs,
"-Dkonan.home=${rootProject.projectDir}/dist",
"-Djava.library.path=${project.buildDir}/nativelibs/$hostName",
]
def defaultArgs = ['-nopack', '-nodefaultlibs']
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) {
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',
'-Xuse-experimental=kotlin.ExperimentalMultiplatform',
'-Xallow-result-return-type',
commonSrc.absolutePath,
"-Xcommon-sources=${commonSrc.absolutePath}",
nativeSrc]
inputs.dir(nativeSrc)
inputs.dir(commonSrc)
outputs.dir(project.file("build/${target}Kliopt"))
}
}
@@ -0,0 +1,2 @@
org.jetbrains.kotlin.native.home=../../dist
org.jetbrains.kotlin.native.jvmArgs=-Xmx6G
@@ -0,0 +1,10 @@
/*
* 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
internal actual fun exitProcess(status: Int): Nothing {
error("Not implemented for JS!")
}
@@ -0,0 +1,11 @@
/*
* 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
internal actual fun exitProcess(status: Int): Nothing {
kotlin.system.exitProcess(0)
}
@@ -0,0 +1,10 @@
/*
* 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
internal actual fun exitProcess(status: Int): Nothing {
kotlin.system.exitProcess(0)
}
@@ -0,0 +1,87 @@
/*
* 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
/**
* Possible types of arguments.
*
* Inheritors describe type of argument value. New types can be added by user.
* In case of options type can have parameter or not.
*/
abstract class ArgType<T : Any>(val hasParameter: kotlin.Boolean) {
/**
* Text description of type for helpMessage.
*/
abstract val description: kotlin.String
/**
* Function to convert string argument value to its type.
* In case of error during conversion also provides help message.
*
* @param value value
*/
abstract val conversion: (value: kotlin.String, name: kotlin.String, helpMessage: kotlin.String)->T
/**
* Argument type for flags that can be only set/unset.
*/
object Boolean : ArgType<kotlin.Boolean>(false) {
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 }
}
/**
* Argument type for string values.
*/
object String : ArgType<kotlin.String>(true) {
override val description: kotlin.String
get() = "{ String }"
override val conversion: (value: kotlin.String, name: kotlin.String, _: kotlin.String) -> kotlin.String
get() = { value, _, _ -> value }
}
/**
* Argument type for integer values.
*/
object Int : ArgType<kotlin.Int>(true) {
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") }
}
/**
* Argument type for double values.
*/
object Double : ArgType<kotlin.Double>(true) {
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") }
}
/**
* Type for arguments that have limited set of possible values.
*/
class Choice(val values: List<kotlin.String>) : ArgType<kotlin.String>(true) {
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") }
}
}
@@ -0,0 +1,36 @@
/*
* 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.annotation.AnnotationTarget.*
/**
* This annotation marks the experimental API for working with command line arguments.
*
* > Beware using the annotated API especially if you're developing a library, since your library might become binary incompatible
* with the future versions of the CLI library.
*
* 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`.
*/
@Experimental(level = Experimental.Level.WARNING)
@Retention(AnnotationRetention.BINARY)
@Target(
CLASS,
ANNOTATION_CLASS,
PROPERTY,
FIELD,
LOCAL_VARIABLE,
VALUE_PARAMETER,
CONSTRUCTOR,
FUNCTION,
PROPERTY_GETTER,
PROPERTY_SETTER,
TYPEALIAS
)
@SinceKotlin("1.3")
public annotation class ExperimentalCli
@@ -0,0 +1,863 @@
/*
* 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()
}
}
@@ -0,0 +1,57 @@
/*
* 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.test.*
class ArgumentsTests {
@Test
fun testPositionalArguments() {
val argParser = ArgParser("testParser")
val debugMode by argParser.option(ArgType.Boolean, "debug", "d", "Debug mode")
val input by argParser.argument(ArgType.String, "input", "Input file")
val output by argParser.argument(ArgType.String, "output", "Output file")
argParser.parse(arrayOf("-d", "input.txt", "out.txt"))
assertEquals(true, debugMode)
assertEquals("out.txt", output)
assertEquals("input.txt", input)
}
@Test
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")
argParser.parse(arrayOf("out.txt", "input1.txt", "input2.txt", "input3.txt",
"input4.txt"))
assertEquals("out.txt", output)
assertEquals(4, inputs.size)
}
@Test
fun testArgumetsWithSeveralValues() {
val argParser = ArgParser("testParser")
val addendums by argParser.arguments(ArgType.Int, "addendums", 2, description = "Addendums")
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"))
assertEquals("out.txt", output)
val (first, second) = addendums
assertEquals(2, addendums.size)
assertEquals(2, first)
assertEquals(3, second)
}
@Test
fun testSkippingExtraArguments() {
val argParser = ArgParser("testParser", skipExtraArguments = true)
val addendums by argParser.arguments(ArgType.Int, "addendums", 2, description = "Addendums")
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"))
assertEquals("out.txt", output)
}
}
@@ -0,0 +1,55 @@
/*
* 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.test.*
class ErrorTests {
@Test
fun testExtraArguments() {
val argParser = ArgParser("testParser")
val addendums by argParser.arguments(ArgType.Int, "addendums", 2, description = "Addendums")
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!!)
}
@Test
fun testUnknownOption() {
val argParser = ArgParser("testParser")
val output by argParser.option(ArgType.String, "output", "o", "Output file")
val input by argParser.option(ArgType.String, "input", "i", "Input file")
val exception = assertFailsWith<IllegalStateException> {
argParser.parse(arrayOf("-o", "out.txt", "-d", "-i", "input.txt"))
}
assertTrue("Unknown option -d" in exception.message!!)
}
@Test
fun testWrongFormat() {
val argParser = ArgParser("testParser")
val number by argParser.option(ArgType.Int, "number", description = "Integer number")
val exception = assertFailsWith<IllegalStateException> {
argParser.parse(arrayOf("--number", "out.txt"))
}
assertTrue("Option number is expected to be integer number. out.txt is provided." in exception.message!!)
}
@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 exception = assertFailsWith<IllegalStateException> {
argParser.parse(arrayOf("-r", "xml"))
}
assertTrue("Option renders is expected to be one of [text, html]. xml is provided." in exception.message!!)
}
}
@@ -0,0 +1,95 @@
/*
* 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.
*/
@file:UseExperimental(ExperimentalCli::class)
package org.jetbrains.kliopt
import kotlin.math.exp
import kotlin.test.*
class HelpTests {
@Test
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 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 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)
val user by argParser.option(ArgType.String, shortName = "u", description = "User access information for authorization")
argParser.parse(arrayOf("-h"))
val helpOutput = argParser.makeUsage().trimIndent()
val expectedOutput = """
Usage: test 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] }
--user, -u -> User access information for authorization { String }
--help, -h -> Usage info
""".trimIndent()
assertEquals(helpOutput, expectedOutput)
}
@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 = ",")
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 = ",")
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 = ",")
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")
val mainReport by argument(ArgType.String, description = "Main report for analysis")
override fun execute() {
println("Do some important things!")
}
}
val action = Summary()
// Parse args.
val argParser = ArgParser("test")
argParser.subcommands(action)
argParser.parse(arrayOf("summary", "-h"))
val helpOutput = argParser.makeUsage().trimIndent()
val expectedOutput = """
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] }
--user, -u -> User access information for authorization { String }
--help, -h -> Usage info
""".trimIndent()
assertEquals(helpOutput, expectedOutput)
}
}
@@ -0,0 +1,66 @@
/*
* 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.test.*
class OptionsTests {
@Test
fun testShortForm() {
val argParser = ArgParser("testParser")
val output by argParser.option(ArgType.String, "output", "o", "Output file")
val input by argParser.option(ArgType.String, "input", "i", "Input file")
argParser.parse(arrayOf("-o", "out.txt", "-i", "input.txt"))
assertEquals("out.txt", output)
assertEquals("input.txt", input)
}
@Test
fun testFullForm() {
val argParser = ArgParser("testParser")
val output by argParser.option(ArgType.String, shortName = "o", description = "Output file")
val input by argParser.option(ArgType.String, shortName = "i", description = "Input file")
argParser.parse(arrayOf("--output", "out.txt", "--input", "input.txt"))
assertEquals("out.txt", output)
assertEquals("input.txt", input)
}
@Test
fun testJavaPrefix() {
val argParser = ArgParser("testParser", prefixStyle = ArgParser.OPTION_PREFIX_STYLE.JVM)
val output by argParser.option(ArgType.String, "output", "o", "Output file")
val input by argParser.option(ArgType.String, "input", "i", "Input file")
argParser.parse(arrayOf("-output", "out.txt", "-i", "input.txt"))
assertEquals("out.txt", output)
assertEquals("input.txt", input)
}
@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)
argParser.parse(arrayOf("-s", "-r", "text", "-r", "json"))
assertEquals(true, useShortForm)
assertEquals(2, renders.size)
val (firstRender, secondRender) = renders
assertEquals("text", firstRender)
assertEquals("json", secondRender)
}
@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 output by argParser.option(ArgType.String, "output", "o", "Output file")
argParser.parse(arrayOf("-o", "out.txt"))
assertEquals(false, useShortForm)
assertEquals("text", renders[0])
}
}
@@ -0,0 +1,99 @@
/*
* 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.
*/
@file:UseExperimental(ExperimentalCli::class)
package org.jetbrains.kliopt
import kotlin.test.*
class SubcommandsTests {
@Test
fun testSubcommand() {
val argParser = ArgParser("testParser")
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")
var result: Int = 0
override fun execute() {
result = addendums.sum()
result = if (invert!!) -1 * result else result
}
}
val action = Summary()
argParser.subcommands(action)
argParser.parse(arrayOf("-o", "out.txt", "summary", "-i", "2", "3", "5"))
assertEquals("out.txt", output)
assertEquals(-10, action.result)
}
@Test
fun testCommonOptions() {
abstract class CommonOptions(name: String): Subcommand(name) {
val numbers by arguments(ArgType.Int, "numbers", description = "Numbers")
}
class Summary: CommonOptions("summary") {
val invert by option(ArgType.Boolean, "invert", "i", "Invert results")
var result: Int = 0
override fun execute() {
result = numbers.sum()
result = invert?.let { -1 * result } ?: result
}
}
class Subtraction : CommonOptions("sub") {
var result: Int = 0
override fun execute() {
result = numbers.map { -it }.sum()
}
}
val summaryAction = Summary()
val subtractionAction = Subtraction()
val argParser = ArgParser("testParser")
argParser.subcommands(summaryAction, subtractionAction)
argParser.parse(arrayOf("summary", "2", "3", "5"))
assertEquals(10, summaryAction.result)
val argParserSubtraction = ArgParser("testParser")
argParserSubtraction.subcommands(summaryAction, subtractionAction)
argParserSubtraction.parse(arrayOf("sub", "8", "-2", "3"))
assertEquals(-9, subtractionAction.result)
}
@Test
fun testRecursiveSubcommands() {
val argParser = ArgParser("testParser")
class Summary: Subcommand("summary") {
val addendums by arguments(ArgType.Int, "addendums", description = "Addendums")
var result: Int = 0
override fun execute() {
result = addendums.sum()
}
}
class Calculation: Subcommand("calc") {
init {
subcommands(Summary())
}
val invert by option(ArgType.Boolean, "invert", "i", "Invert results")
var result: Int = 0
override fun execute() {
result = (subcommands["summary"] as Summary).result
result = if (invert!!) -1 * result else result
}
}
val action = Calculation()
argParser.subcommands(action)
argParser.parse(arrayOf("calc", "-i", "summary", "2", "3", "5"))
assertEquals(-10, action.result)
}
}