Skip to content
This repository has been archived by the owner on Aug 10, 2021. It is now read-only.

Commit

Permalink
Updated kotlix.cli (#4244)
Browse files Browse the repository at this point in the history
  • Loading branch information
LepilkinaElena authored Jun 19, 2020
1 parent e906d29 commit 6008d12
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 45 deletions.
198 changes: 156 additions & 42 deletions endorsedLibraries/kotlinx.cli/src/main/kotlin/kotlinx/cli/ArgParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ open class ArgParser(
/**
* Map of subcommands.
*/
@UseExperimental(ExperimentalCli::class)
@OptIn(ExperimentalCli::class)
protected val subcommands = mutableMapOf<String, Subcommand>()

/**
Expand All @@ -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.
Expand All @@ -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.
*/
Expand All @@ -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.",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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.
Expand Down Expand Up @@ -442,57 +570,43 @@ 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)

return parsingState!!
}
}
// 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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion endorsedLibraries/kotlinx.cli/src/tests/HelpTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions endorsedLibraries/kotlinx.cli/src/tests/OptionsTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6008d12

Please sign in to comment.