diff --git a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgParser.kt b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgParser.kt index 6546b649770..321d5ac3f7e 100644 --- a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgParser.kt +++ b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgParser.kt @@ -132,7 +132,7 @@ open class ArgParser( /** * Map of subcommands. */ - @UseExperimental(ExperimentalCli::class) + @OptIn(ExperimentalCli::class) protected val subcommands = mutableMapOf() /** @@ -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 { + 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): 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) { + 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): 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 -> diff --git a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ExperimentalCli.kt b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ExperimentalCli.kt index e073381fa96..7f8f2e9fc14 100644 --- a/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ExperimentalCli.kt +++ b/endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ExperimentalCli.kt @@ -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, diff --git a/endorsedLibraries/kotlinx.cli/src/tests/HelpTests.kt b/endorsedLibraries/kotlinx.cli/src/tests/HelpTests.kt index 31ca3f19790..eafa058a663 100644 --- a/endorsedLibraries/kotlinx.cli/src/tests/HelpTests.kt +++ b/endorsedLibraries/kotlinx.cli/src/tests/HelpTests.kt @@ -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 diff --git a/endorsedLibraries/kotlinx.cli/src/tests/OptionsTests.kt b/endorsedLibraries/kotlinx.cli/src/tests/OptionsTests.kt index f1d31565358..8a883c1fca3 100644 --- a/endorsedLibraries/kotlinx.cli/src/tests/OptionsTests.kt +++ b/endorsedLibraries/kotlinx.cli/src/tests/OptionsTests.kt @@ -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") diff --git a/endorsedLibraries/kotlinx.cli/src/tests/SubcommandsTests.kt b/endorsedLibraries/kotlinx.cli/src/tests/SubcommandsTests.kt index c0d73c415ee..2d13d10d11b 100644 --- a/endorsedLibraries/kotlinx.cli/src/tests/SubcommandsTests.kt +++ b/endorsedLibraries/kotlinx.cli/src/tests/SubcommandsTests.kt @@ -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