Updated kotlix.cli (#4244)
This commit is contained in:
@@ -132,7 +132,7 @@ open class ArgParser(
|
|||||||
/**
|
/**
|
||||||
* Map of subcommands.
|
* Map of subcommands.
|
||||||
*/
|
*/
|
||||||
@UseExperimental(ExperimentalCli::class)
|
@OptIn(ExperimentalCli::class)
|
||||||
protected val subcommands = mutableMapOf<String, Subcommand>()
|
protected val subcommands = mutableMapOf<String, Subcommand>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,7 +143,7 @@ open class ArgParser(
|
|||||||
/**
|
/**
|
||||||
* Used prefix form for full option form.
|
* Used prefix form for full option form.
|
||||||
*/
|
*/
|
||||||
private val optionFullFormPrefix = if (prefixStyle == OptionPrefixStyle.LINUX) "--" else "-"
|
private val optionFullFormPrefix = if (prefixStyle == OptionPrefixStyle.JVM) "-" else "--"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used prefix form for short option form.
|
* Used prefix form for short option form.
|
||||||
@@ -155,6 +155,11 @@ open class ArgParser(
|
|||||||
*/
|
*/
|
||||||
protected val fullCommandName = mutableListOf(programName)
|
protected val fullCommandName = mutableListOf(programName)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to recognize if CLI entities can be treated as options.
|
||||||
|
*/
|
||||||
|
protected var treatAsOption = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The way an option/argument has got its value.
|
* The way an option/argument has got its value.
|
||||||
*/
|
*/
|
||||||
@@ -179,6 +184,11 @@ open class ArgParser(
|
|||||||
LINUX,
|
LINUX,
|
||||||
/* JVM style: both full and short names are prefixed with one hyphen "-". */
|
/* JVM style: both full and short names are prefixed with one hyphen "-". */
|
||||||
JVM,
|
JVM,
|
||||||
|
/* GNU style: the full name of an option is prefixed with two hyphens "--" and "=" between options and value
|
||||||
|
and the short name — with one "-".
|
||||||
|
Detailed information https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
|
||||||
|
*/
|
||||||
|
GNU
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("OPTION_PREFIX_STYLE is deprecated. Please, use OptionPrefixStyle.",
|
@Deprecated("OPTION_PREFIX_STYLE is deprecated. Please, use OptionPrefixStyle.",
|
||||||
@@ -216,6 +226,13 @@ open class ArgParser(
|
|||||||
description: String? = null,
|
description: String? = null,
|
||||||
deprecatedWarning: String? = null
|
deprecatedWarning: String? = null
|
||||||
): SingleNullableOption<T> {
|
): SingleNullableOption<T> {
|
||||||
|
if (prefixStyle == OptionPrefixStyle.GNU && shortName != null)
|
||||||
|
require(shortName.length == 1) {
|
||||||
|
"""
|
||||||
|
GNU standart for options allow to use short form whuch consists of one character.
|
||||||
|
For more information, please, see https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
val option = SingleNullableOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type,
|
val option = SingleNullableOption(OptionDescriptor(optionFullFormPrefix, optionShortFromPrefix, type,
|
||||||
fullName, shortName, description, deprecatedWarning = deprecatedWarning), CLIEntityWrapper())
|
fullName, shortName, description, deprecatedWarning = deprecatedWarning), CLIEntityWrapper())
|
||||||
option.owner.entity = option
|
option.owner.entity = option
|
||||||
@@ -334,6 +351,18 @@ open class ArgParser(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Treat value as argument value.
|
||||||
|
*
|
||||||
|
* @param arg string with argument value.
|
||||||
|
* @param argumentsQueue queue with active argument descriptors.
|
||||||
|
*/
|
||||||
|
private fun treatAsArgument(arg: String, argumentsQueue: ArgumentsQueue) {
|
||||||
|
if (!saveAsArg(arg, argumentsQueue)) {
|
||||||
|
printError("Too many arguments! Couldn't process argument $arg!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save value as option value.
|
* Save value as option value.
|
||||||
*/
|
*/
|
||||||
@@ -342,24 +371,123 @@ open class ArgParser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to recognize command line element as full form of option.
|
* Try to recognize and save command line element as full form of option.
|
||||||
*
|
*
|
||||||
* @param candidate string with candidate in options.
|
* @param candidate string with candidate in options.
|
||||||
|
* @param argIterator iterator over command line arguments.
|
||||||
*/
|
*/
|
||||||
private fun recognizeOptionFullForm(candidate: String) =
|
private fun recognizeAndSaveOptionFullForm(candidate: String, argIterator: Iterator<String>): Boolean {
|
||||||
if (candidate.startsWith(optionFullFormPrefix))
|
if (prefixStyle == OptionPrefixStyle.GNU && candidate == optionFullFormPrefix) {
|
||||||
options[candidate.substring(optionFullFormPrefix.length)]
|
// All other arguments after `--` are treated as non-option arguments.
|
||||||
else null
|
treatAsOption = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!candidate.startsWith(optionFullFormPrefix))
|
||||||
|
return false
|
||||||
|
|
||||||
|
val optionString = candidate.substring(optionFullFormPrefix.length)
|
||||||
|
val argValue = if (prefixStyle == OptionPrefixStyle.GNU) null else options[optionString]
|
||||||
|
if (argValue != null) {
|
||||||
|
saveStandardOptionForm(argValue, argIterator)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// Check GNU style of options.
|
||||||
|
if (prefixStyle == OptionPrefixStyle.GNU) {
|
||||||
|
// Option without a parameter.
|
||||||
|
if (options[optionString]?.descriptor?.type?.hasParameter == false) {
|
||||||
|
saveOptionWithoutParameter(options[optionString]!!)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Option with parameters.
|
||||||
|
val optionParts = optionString.split('=', limit = 2)
|
||||||
|
if (optionParts.size != 2)
|
||||||
|
return false
|
||||||
|
if (options[optionParts[0]] != null) {
|
||||||
|
saveAsOption(options[optionParts[0]]!!, optionParts[1])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to recognize command line element as short form of option.
|
* Save option without parameter.
|
||||||
|
*
|
||||||
|
* @param argValue argument value with all information about option.
|
||||||
|
*/
|
||||||
|
private fun saveOptionWithoutParameter(argValue: ParsingValue<*, *>) {
|
||||||
|
// Boolean flags.
|
||||||
|
if (argValue.descriptor.fullName == "help") {
|
||||||
|
println(makeUsage())
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
saveAsOption(argValue, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save option described with standard separated form `--name value`.
|
||||||
|
*
|
||||||
|
* @param argValue argument value with all information about option.
|
||||||
|
* @param argIterator iterator over command line arguments.
|
||||||
|
*/
|
||||||
|
private fun saveStandardOptionForm(argValue: ParsingValue<*, *>, argIterator: Iterator<String>) {
|
||||||
|
if (argValue.descriptor.type.hasParameter) {
|
||||||
|
if (argIterator.hasNext()) {
|
||||||
|
saveAsOption(argValue, argIterator.next())
|
||||||
|
} else {
|
||||||
|
// An error, option with value without value.
|
||||||
|
printError("No value for ${argValue.descriptor.textDescription}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveOptionWithoutParameter(argValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to recognize and save command line element as short form of option.
|
||||||
*
|
*
|
||||||
* @param candidate string with candidate in options.
|
* @param candidate string with candidate in options.
|
||||||
|
* @param argIterator iterator over command line arguments.
|
||||||
*/
|
*/
|
||||||
private fun recognizeOptionShortForm(candidate: String) =
|
private fun recognizeAndSaveOptionShortForm(candidate: String, argIterator: Iterator<String>): Boolean {
|
||||||
if (candidate.startsWith(optionShortFromPrefix))
|
if (!candidate.startsWith(optionShortFromPrefix) ||
|
||||||
shortNames[candidate.substring(optionShortFromPrefix.length)]
|
optionFullFormPrefix != optionShortFromPrefix && candidate.startsWith(optionFullFormPrefix)) return false
|
||||||
else null
|
// Try to find exact match.
|
||||||
|
val option = candidate.substring(optionShortFromPrefix.length)
|
||||||
|
val argValue = shortNames[option]
|
||||||
|
if (argValue != null) {
|
||||||
|
saveStandardOptionForm(argValue, argIterator)
|
||||||
|
} else {
|
||||||
|
if (prefixStyle != OptionPrefixStyle.GNU || option.isEmpty())
|
||||||
|
return false
|
||||||
|
|
||||||
|
// Try to find collapsed form.
|
||||||
|
val firstOption = shortNames["${option[0]}"] ?: return false
|
||||||
|
// Form with value after short form without separator.
|
||||||
|
if (firstOption.descriptor.type.hasParameter) {
|
||||||
|
saveAsOption(firstOption, option.substring(1))
|
||||||
|
} else {
|
||||||
|
// Form with several short forms as one string.
|
||||||
|
val otherBooleanOptions = option.substring(1)
|
||||||
|
saveOptionWithoutParameter(firstOption)
|
||||||
|
for (option in otherBooleanOptions) {
|
||||||
|
shortNames["$option"]?.let {
|
||||||
|
if (it.descriptor.type.hasParameter) {
|
||||||
|
printError(
|
||||||
|
"Option $optionShortFromPrefix$option can't be used in option combination $candidate, " +
|
||||||
|
"because parameter value of type ${it.descriptor.type.description} should be " +
|
||||||
|
"provided for current option."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}?: printError("Unknown option $optionShortFromPrefix$option in option combination $candidate.")
|
||||||
|
|
||||||
|
saveOptionWithoutParameter(shortNames["$option"]!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the provided array of command line arguments.
|
* Parses the provided array of command line arguments.
|
||||||
@@ -442,16 +570,16 @@ open class ArgParser(
|
|||||||
|
|
||||||
val argumentsQueue = ArgumentsQueue(arguments.map { it.value.descriptor as ArgDescriptor<*, *> })
|
val argumentsQueue = ArgumentsQueue(arguments.map { it.value.descriptor as ArgDescriptor<*, *> })
|
||||||
|
|
||||||
var index = 0
|
val argIterator = args.listIterator()
|
||||||
try {
|
try {
|
||||||
while (index < args.size) {
|
while (argIterator.hasNext()) {
|
||||||
val arg = args[index]
|
val arg = argIterator.next()
|
||||||
// Check for subcommands.
|
// Check for subcommands.
|
||||||
@UseExperimental(ExperimentalCli::class)
|
@OptIn(ExperimentalCli::class)
|
||||||
subcommands.forEach { (name, subcommand) ->
|
subcommands.forEach { (name, subcommand) ->
|
||||||
if (arg == name) {
|
if (arg == name) {
|
||||||
// Use parser for this subcommand.
|
// Use parser for this subcommand.
|
||||||
subcommand.parse(args.slice(index + 1..args.size - 1))
|
subcommand.parse(args.slice(argIterator.nextIndex() until args.size))
|
||||||
subcommand.execute()
|
subcommand.execute()
|
||||||
parsingState = ArgParserResult(name)
|
parsingState = ArgParserResult(name)
|
||||||
|
|
||||||
@@ -459,40 +587,26 @@ open class ArgParser(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Parse arguments from command line.
|
// Parse arguments from command line.
|
||||||
if (arg.startsWith('-')) {
|
if (treatAsOption && arg.startsWith('-')) {
|
||||||
// Candidate in being option.
|
// Candidate in being option.
|
||||||
// Option is found.
|
// Option is found.
|
||||||
val argValue = recognizeOptionShortForm(arg) ?: recognizeOptionFullForm(arg)
|
if (!(recognizeAndSaveOptionShortForm(arg, argIterator) ||
|
||||||
argValue?.descriptor?.let {
|
recognizeAndSaveOptionFullForm(arg, argIterator))) {
|
||||||
if (argValue.descriptor.type.hasParameter) {
|
// State is changed so next options are arguments.
|
||||||
if (index < args.size - 1) {
|
if (!treatAsOption) {
|
||||||
saveAsOption(argValue, args[index + 1])
|
// Argument is found.
|
||||||
index++
|
treatAsArgument(argIterator.next(), argumentsQueue)
|
||||||
} else {
|
|
||||||
// An error, option with value without value.
|
|
||||||
printError("No value for ${argValue.descriptor.textDescription}")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Boolean flags.
|
// Try save as argument.
|
||||||
if (argValue.descriptor.fullName == "help") {
|
if (!saveAsArg(arg, argumentsQueue)) {
|
||||||
println(makeUsage())
|
printError("Unknown option $arg")
|
||||||
exitProcess(0)
|
|
||||||
}
|
}
|
||||||
saveAsOption(argValue, "true")
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
// Try save as argument.
|
|
||||||
if (!saveAsArg(arg, argumentsQueue)) {
|
|
||||||
printError("Unknown option $arg")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Argument is found.
|
// Argument is found.
|
||||||
if (!saveAsArg(arg, argumentsQueue)) {
|
treatAsArgument(arg, argumentsQueue)
|
||||||
printError("Too many arguments! Couldn't process argument $arg!")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
index++
|
|
||||||
}
|
}
|
||||||
// Postprocess results of parsing.
|
// Postprocess results of parsing.
|
||||||
options.values.union(arguments.values).forEach { value ->
|
options.values.union(arguments.values).forEach { value ->
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import kotlin.annotation.AnnotationTarget.*
|
|||||||
* annotating that usage with the [UseExperimental] annotation, e.g. `@UseExperimental(ExperimentalCli::class)`,
|
* annotating that usage with the [UseExperimental] annotation, e.g. `@UseExperimental(ExperimentalCli::class)`,
|
||||||
* or by using the compiler argument `-Xuse-experimental=kotlinx.cli.ExperimentalCli`.
|
* or by using the compiler argument `-Xuse-experimental=kotlinx.cli.ExperimentalCli`.
|
||||||
*/
|
*/
|
||||||
@Experimental(level = Experimental.Level.WARNING)
|
@RequiresOptIn("This API is experimental. It may be changed in the future without notice.", RequiresOptIn.Level.WARNING)
|
||||||
@Retention(AnnotationRetention.BINARY)
|
@Retention(AnnotationRetention.BINARY)
|
||||||
@Target(
|
@Target(
|
||||||
CLASS,
|
CLASS,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
* 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.
|
* that can be found in the LICENSE file.
|
||||||
*/
|
*/
|
||||||
@file:UseExperimental(ExperimentalCli::class)
|
@file:OptIn(ExperimentalCli::class)
|
||||||
package kotlinx.cli
|
package kotlinx.cli
|
||||||
|
|
||||||
import kotlinx.cli.ArgParser
|
import kotlinx.cli.ArgParser
|
||||||
|
|||||||
@@ -38,6 +38,38 @@ class OptionsTests {
|
|||||||
assertEquals("input.txt", input)
|
assertEquals("input.txt", input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGNUPrefix() {
|
||||||
|
val argParser = ArgParser("testParser", prefixStyle = ArgParser.OptionPrefixStyle.GNU)
|
||||||
|
val output by argParser.option(ArgType.String, "output", "o", "Output file")
|
||||||
|
val input by argParser.option(ArgType.String, "input", "i", "Input file")
|
||||||
|
val verbose by argParser.option(ArgType.Boolean, "verbose", "v", "Verbose print")
|
||||||
|
val shortForm by argParser.option(ArgType.Boolean, "short", "s", "Short output form")
|
||||||
|
val text by argParser.option(ArgType.Boolean, "text", "t", "Use text format")
|
||||||
|
argParser.parse(arrayOf("-oout.txt", "--input=input.txt", "-vst"))
|
||||||
|
assertEquals("out.txt", output)
|
||||||
|
assertEquals("input.txt", input)
|
||||||
|
assertEquals(verbose, true)
|
||||||
|
assertEquals(shortForm, true)
|
||||||
|
assertEquals(text, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGNUArguments() {
|
||||||
|
val argParser = ArgParser("testParser", prefixStyle = ArgParser.OptionPrefixStyle.GNU)
|
||||||
|
val output by argParser.argument(ArgType.String, "output", "Output file")
|
||||||
|
val input by argParser.argument(ArgType.String, "input", "Input file")
|
||||||
|
val verbose by argParser.option(ArgType.Boolean, "verbose", "v", "Verbose print")
|
||||||
|
val shortForm by argParser.option(ArgType.Boolean, "short", "s", "Short output form").default(false)
|
||||||
|
val text by argParser.option(ArgType.Boolean, "text", "t", "Use text format").default(false)
|
||||||
|
argParser.parse(arrayOf("--verbose", "--", "out.txt", "--input.txt"))
|
||||||
|
assertEquals("out.txt", output)
|
||||||
|
assertEquals("--input.txt", input)
|
||||||
|
assertEquals(verbose, true)
|
||||||
|
assertEquals(shortForm, false)
|
||||||
|
assertEquals(text, false)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMultipleOptions() {
|
fun testMultipleOptions() {
|
||||||
val argParser = ArgParser("testParser")
|
val argParser = ArgParser("testParser")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
* 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.
|
* that can be found in the LICENSE file.
|
||||||
*/
|
*/
|
||||||
@file:UseExperimental(ExperimentalCli::class)
|
@file:OptIn(ExperimentalCli::class)
|
||||||
package kotlinx.cli
|
package kotlinx.cli
|
||||||
|
|
||||||
import kotlinx.cli.ArgParser
|
import kotlinx.cli.ArgParser
|
||||||
|
|||||||
Reference in New Issue
Block a user