
commonMain.com.github.ajalt.clikt.parsers.Parser.kt Maven / Gradle / Ivy
package com.github.ajalt.clikt.parsers
import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.mpp.readFileIfExists
import com.github.ajalt.clikt.parameters.arguments.Argument
import com.github.ajalt.clikt.parameters.options.EagerOption
import com.github.ajalt.clikt.parameters.options.Option
import com.github.ajalt.clikt.parameters.options.splitOptionPrefix
import com.github.ajalt.clikt.parsers.OptionParser.Invocation
private data class OptInvocation(val opt: Option, val inv: Invocation)
private data class OptParseResult(val consumed: Int, val unknown: List, val known: List)
internal object Parser {
fun parse(argv: List, context: Context) {
parse(argv, context, 0, true)
}
private fun parse(argv: List, context: Context, startingArgI: Int, canRun: Boolean): Pair, Int> {
var tokens = argv
val command = context.command
val aliases = command.aliases()
val subcommands = command._subcommands.associateBy { it.commandName }
val optionsByName = HashMap()
val arguments = command._arguments
val prefixes = mutableSetOf()
val longNames = mutableSetOf()
val hasMultipleSubAncestor = generateSequence(context.parent) { it.parent }.any { it.command.allowMultipleSubcommands }
for (option in command._options) {
for (name in option.names + option.secondaryNames) {
optionsByName[name] = option
if (name.length > 2) longNames += name
prefixes += splitOptionPrefix(name).first
}
}
prefixes.remove("")
if (startingArgI > tokens.lastIndex && command.printHelpOnEmptyArgs) {
throw PrintHelpMessage(command, error = true)
}
val positionalArgs = ArrayList()
var i = startingArgI
var subcommand: CliktCommand? = null
var canParseOptions = true
var canExpandAtFiles = context.expandArgumentFiles
val invocations = mutableListOf()
var minAliasI = 0
fun isLongOptionWithEquals(prefix: String, token: String): Boolean {
if ("=" !in token) return false
if (prefix.isEmpty()) return false
if (prefix.length > 1) return true
if (context.tokenTransformer(context, token.substringBefore("=")) in longNames) return true
if (context.tokenTransformer(context, token.take(2)) in optionsByName) return false
return true
}
fun consumeParse(result: OptParseResult) {
positionalArgs += result.unknown
invocations += result.known
i += result.consumed
}
loop@ while (i <= tokens.lastIndex) {
val tok = tokens[i]
val normTok = context.tokenTransformer(context, tok)
val prefix = splitOptionPrefix(tok).first
when {
canExpandAtFiles && tok.startsWith("@") && normTok !in optionsByName -> {
if (tok.startsWith("@@")) {
positionalArgs += tok.drop(1)
i += 1
} else {
tokens = loadArgFile(normTok.drop(1)) + tokens.slice(i + 1..tokens.lastIndex)
i = 0
minAliasI = 0
}
}
canParseOptions && tok == "--" -> {
i += 1
canParseOptions = false
canExpandAtFiles = false
}
canParseOptions && (prefix.length > 1 && prefix in prefixes || normTok in longNames || isLongOptionWithEquals(prefix, tok)) -> {
consumeParse(parseLongOpt(command.treatUnknownOptionsAsArgs, context, tokens, tok, i, optionsByName))
}
canParseOptions && tok.length >= 2 && prefix.isNotEmpty() && prefix in prefixes -> {
consumeParse(parseShortOpt(command.treatUnknownOptionsAsArgs, context, tokens, tok, i, optionsByName))
}
i >= minAliasI && tok in aliases -> {
tokens = aliases.getValue(tok) + tokens.slice(i + 1..tokens.lastIndex)
i = 0
minAliasI = aliases.getValue(tok).size
}
normTok in subcommands -> {
subcommand = subcommands.getValue(normTok)
break@loop
}
else -> {
if (!context.allowInterspersedArgs) canParseOptions = false
positionalArgs += tokens[i] // arguments aren't transformed
i += 1
}
}
}
val invocationsByOption = invocations.groupBy({ it.opt }, { it.inv })
val invocationsByGroup = invocations.groupBy { (it.opt as? GroupableOption)?.parameterGroup }
val invocationsByOptionByGroup = invocationsByGroup.mapValues { (_, invs) -> invs.groupBy({ it.opt }, { it.inv }).filterKeys { it !is EagerOption } }
try {
// Finalize and validate everything as long as we aren't resuming a parse for multiple subcommands
if (canRun) {
// Finalize eager options
invocationsByOption.forEach { (o, inv) -> if (o is EagerOption) o.finalize(context, inv) }
// Finalize un-grouped options that occurred on the command line
invocationsByOptionByGroup[null]?.forEach { (o, inv) -> o.finalize(context, inv) }
// Finalize un-grouped options not provided on the command line so that they can apply default values etc.
command._options.forEach { o ->
if (o !is EagerOption && o !in invocationsByOption && (o as? GroupableOption)?.parameterGroup == null) {
o.finalize(context, emptyList())
}
}
// Finalize option groups after other options so that the groups can use their values
invocationsByOptionByGroup.forEach { (group, invocations) ->
group?.finalize(context, invocations)
}
// Finalize groups with no invocations
command._groups.forEach { if (it !in invocationsByGroup) it.finalize(context, emptyMap()) }
val (excess, parsedArgs) = parseArguments(positionalArgs, arguments)
parsedArgs.forEach { (it, v) -> it.finalize(context, v) }
if (excess > 0) {
if (hasMultipleSubAncestor) {
i = tokens.size - excess
} else if (excess == 1 && subcommands.isNotEmpty()) {
val actual = positionalArgs.last()
throw NoSuchSubcommand(actual, context.correctionSuggestor(actual, subcommands.keys.toList()))
} else {
val actual = positionalArgs.takeLast(excess).joinToString(" ", limit = 3, prefix = "(", postfix = ")")
throw UsageError("Got unexpected extra argument${if (excess == 1) "" else "s"} $actual")
}
}
// Now that all parameters have been finalized, we can validate everything
command._options.forEach { o -> if ((o as? GroupableOption)?.parameterGroup == null) o.postValidate(context) }
command._groups.forEach { it.postValidate(context) }
command._arguments.forEach { it.postValidate(context) }
if (subcommand == null && subcommands.isNotEmpty() && !command.invokeWithoutSubcommand) {
throw PrintHelpMessage(command, error = true)
}
command.currentContext.invokedSubcommand = subcommand
if (command.currentContext.printExtraMessages) {
val console = command.currentContext.console
for (warning in command.messages) {
console.print(warning, error = true)
console.print(console.lineSeparator, error = true)
}
}
command.run()
}
} catch (e: UsageError) {
// Augment usage errors with the current context if they don't have one
if (e.context == null) e.context = context
throw e
}
if (subcommand != null) {
val (nextTokens, nextArgI) = parse(tokens, subcommand.currentContext, i + 1, true)
if (command.allowMultipleSubcommands && nextTokens.size - nextArgI > 0) {
parse(nextTokens, context, nextArgI, false)
}
return nextTokens to nextArgI
}
return tokens to i
}
private fun parseLongOpt(
ignoreUnknown: Boolean,
context: Context,
tokens: List,
tok: String,
index: Int,
optionsByName: Map
): OptParseResult {
val equalsIndex = tok.indexOf('=')
var (name, value) = if (equalsIndex >= 0) {
tok.substring(0, equalsIndex) to tok.substring(equalsIndex + 1)
} else {
tok to null
}
name = context.tokenTransformer(context, name)
val option = optionsByName[name] ?: if (ignoreUnknown) {
return OptParseResult(1, listOf(tok), emptyList())
} else {
throw NoSuchOption(
givenName = name,
possibilities = context.correctionSuggestor(name, optionsByName.keys.toList())
)
}
val result = option.parser.parseLongOpt(option, name, tokens, index, value)
return OptParseResult(result.consumedCount, emptyList(), listOf(OptInvocation(option, result.invocation)))
}
private fun parseShortOpt(
ignoreUnknown: Boolean,
context: Context,
tokens: List,
tok: String,
index: Int,
optionsByName: Map
): OptParseResult {
val prefix = tok[0].toString()
val invocations = mutableListOf()
for ((i, opt) in tok.withIndex()) {
if (i == 0) continue // skip the dash
val name = context.tokenTransformer(context, prefix + opt)
val option = optionsByName[name] ?: if (ignoreUnknown && tok.length == 2) {
return OptParseResult(1, listOf(tok), emptyList())
} else {
throw NoSuchOption(name)
}
val result = option.parser.parseShortOpt(option, name, tokens, index, i)
invocations += OptInvocation(option, result.invocation)
if (result.consumedCount > 0) return OptParseResult(result.consumedCount, emptyList(), invocations)
}
throw IllegalStateException(
"Error parsing short option ${tokens[index]}: no parser consumed value.")
}
private fun parseArguments(
positionalArgs: List,
arguments: List
): Pair>> {
val out = linkedMapOf>().withDefault { listOf() }
// The number of fixed size arguments that occur after an unlimited size argument. This
// includes optional single value args, so it might be bigger than the number of provided
// values.
val endSize = arguments.asReversed()
.takeWhile { it.nvalues > 0 }
.sumBy { it.nvalues }
var i = 0
for (argument in arguments) {
val remaining = positionalArgs.size - i
val consumed = when {
argument.nvalues <= 0 -> maxOf(if (argument.required) 1 else 0, remaining - endSize)
argument.nvalues > 0 && !argument.required && remaining == 0 -> 0
else -> argument.nvalues
}
if (consumed > remaining) {
if (remaining == 0) throw MissingParameter(argument)
else throw IncorrectArgumentValueCount(argument)
}
out[argument] = out.getValue(argument) + positionalArgs.subList(i, i + consumed)
i += consumed
}
val excess = positionalArgs.size - i
return excess to out
}
private fun loadArgFile(filename: String): List {
val text = readFileIfExists(filename) ?: throw FileNotFound(filename)
val toks = mutableListOf()
var inQuote: Char? = null
val sb = StringBuilder()
var i = 0
fun err(msg: String): Nothing {
throw InvalidFileFormat(filename, msg, text.take(i).count { it == '\n' })
}
loop@ while (i < text.length) {
val c = text[i]
when {
c == '\r' -> {
i += 1
}
c == '\n' && inQuote != null -> {
err("unclosed quote")
}
c == '\\' -> {
if (i >= text.lastIndex) err("file ends with \\")
if (text[i + 1] in "\r\n") err("unclosed quote")
sb.append(text[i + 1])
i += 2
}
c == inQuote -> {
toks += sb.toString()
sb.clear()
inQuote = null
i += 1
}
inQuote == null && c == '#' -> {
i = text.indexOf('\n', i)
if (i < 0) break@loop
}
inQuote == null && c in "\"'" -> {
inQuote = c
i += 1
}
inQuote == null && c.isWhitespace() -> {
if (sb.isNotEmpty()) {
toks += sb.toString()
sb.clear()
}
i += 1
}
else -> {
sb.append(c)
i += 1
}
}
}
if (inQuote != null) {
throw UsageError("Missing closing quote in @-file")
}
if (sb.isNotEmpty()) {
toks += sb.toString()
}
return toks
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy