Updated kotlix.cli (#4244)
This commit is contained in:
@@ -132,7 +132,7 @@ open class ArgParser(
|
||||
/**
|
||||
* Map of subcommands.
|
||||
*/
|
||||
@UseExperimental(ExperimentalCli::class)
|
||||
@OptIn(ExperimentalCli::class)
|
||||
protected val subcommands = mutableMapOf<String, Subcommand>()
|
||||
|
||||
/**
|
||||
@@ -143,7 +143,7 @@ open class ArgParser(
|
||||
/**
|
||||
* 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.
|
||||
@@ -155,6 +155,11 @@ open class ArgParser(
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -179,6 +184,11 @@ open class ArgParser(
|
||||
LINUX,
|
||||
/* JVM style: both full and short names are prefixed with one hyphen "-". */
|
||||
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.",
|
||||
@@ -216,6 +226,13 @@ open class ArgParser(
|
||||
description: String? = null,
|
||||
deprecatedWarning: String? = null
|
||||
): 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,
|
||||
fullName, shortName, description, deprecatedWarning = deprecatedWarning), CLIEntityWrapper())
|
||||
option.owner.entity = option
|
||||
@@ -334,6 +351,18 @@ open class ArgParser(
|
||||
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.
|
||||
*/
|
||||
@@ -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 argIterator iterator over command line arguments.
|
||||
*/
|
||||
private fun recognizeOptionFullForm(candidate: String) =
|
||||
if (candidate.startsWith(optionFullFormPrefix))
|
||||
options[candidate.substring(optionFullFormPrefix.length)]
|
||||
else null
|
||||
private fun recognizeAndSaveOptionFullForm(candidate: String, argIterator: Iterator<String>): Boolean {
|
||||
if (prefixStyle == OptionPrefixStyle.GNU && candidate == optionFullFormPrefix) {
|
||||
// All other arguments after `--` are treated as non-option arguments.
|
||||
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 argIterator iterator over command line arguments.
|
||||
*/
|
||||
private fun recognizeOptionShortForm(candidate: String) =
|
||||
if (candidate.startsWith(optionShortFromPrefix))
|
||||
shortNames[candidate.substring(optionShortFromPrefix.length)]
|
||||
else null
|
||||
private fun recognizeAndSaveOptionShortForm(candidate: String, argIterator: Iterator<String>): Boolean {
|
||||
if (!candidate.startsWith(optionShortFromPrefix) ||
|
||||
optionFullFormPrefix != optionShortFromPrefix && candidate.startsWith(optionFullFormPrefix)) return false
|
||||
// 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.
|
||||
@@ -442,16 +570,16 @@ open class ArgParser(
|
||||
|
||||
val argumentsQueue = ArgumentsQueue(arguments.map { it.value.descriptor as ArgDescriptor<*, *> })
|
||||
|
||||
var index = 0
|
||||
val argIterator = args.listIterator()
|
||||
try {
|
||||
while (index < args.size) {
|
||||
val arg = args[index]
|
||||
while (argIterator.hasNext()) {
|
||||
val arg = argIterator.next()
|
||||
// Check for subcommands.
|
||||
@UseExperimental(ExperimentalCli::class)
|
||||
@OptIn(ExperimentalCli::class)
|
||||
subcommands.forEach { (name, subcommand) ->
|
||||
if (arg == name) {
|
||||
// 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()
|
||||
parsingState = ArgParserResult(name)
|
||||
|
||||
@@ -459,40 +587,26 @@ open class ArgParser(
|
||||
}
|
||||
}
|
||||
// Parse arguments from command line.
|
||||
if (arg.startsWith('-')) {
|
||||
if (treatAsOption && 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}")
|
||||
}
|
||||
if (!(recognizeAndSaveOptionShortForm(arg, argIterator) ||
|
||||
recognizeAndSaveOptionFullForm(arg, argIterator))) {
|
||||
// State is changed so next options are arguments.
|
||||
if (!treatAsOption) {
|
||||
// Argument is found.
|
||||
treatAsArgument(argIterator.next(), argumentsQueue)
|
||||
} else {
|
||||
// Boolean flags.
|
||||
if (argValue.descriptor.fullName == "help") {
|
||||
println(makeUsage())
|
||||
exitProcess(0)
|
||||
// Try save as argument.
|
||||
if (!saveAsArg(arg, argumentsQueue)) {
|
||||
printError("Unknown option $arg")
|
||||
}
|
||||
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!")
|
||||
}
|
||||
treatAsArgument(arg, argumentsQueue)
|
||||
}
|
||||
index++
|
||||
}
|
||||
// Postprocess results of parsing.
|
||||
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)`,
|
||||
* 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)
|
||||
@Target(
|
||||
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
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:UseExperimental(ExperimentalCli::class)
|
||||
@file:OptIn(ExperimentalCli::class)
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlinx.cli.ArgParser
|
||||
|
||||
@@ -38,6 +38,38 @@ class OptionsTests {
|
||||
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
|
||||
fun testMultipleOptions() {
|
||||
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
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:UseExperimental(ExperimentalCli::class)
|
||||
@file:OptIn(ExperimentalCli::class)
|
||||
package kotlinx.cli
|
||||
|
||||
import kotlinx.cli.ArgParser
|
||||
|
||||
Reference in New Issue
Block a user