Refactoring, kleine Verbesserungen, Aufräumarbeiten
This commit is contained in:
1 parent
66244db097
commit
561fe601a9
16 files changed
+255
-142
No files matched your search
@@ -32,13 +32,13 @@ class VerifyParameterException(message: String) : RuntimeException(message)
|
||||
* This function is used to signal that there was an error during argument parsing.
|
||||
*
|
||||
* For use in the application. The exception will be caught by the command-line parser
|
||||
* and displayed to the user via showHelp().
|
||||
* and displayed to the user together with the help information.
|
||||
*
|
||||
* @param msg The error message to include in the exception.
|
||||
* @throws VerifyParameterException with the provided error message.
|
||||
*/
|
||||
fun showParsingError(msg: String): Nothing {
|
||||
throw VerifyParameterException("*** ERROR parsing arguments: $msg")
|
||||
throw VerifyParameterException(msg)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,37 +117,44 @@ class SubCommand private constructor(
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterValue<T> {
|
||||
val result = runCatching {
|
||||
val long = long.handleCase(ignoreCase)
|
||||
val short = short?.toString()?.handleCase(ignoreCase)
|
||||
type.createOption(long, short, description, isMandatory).apply {
|
||||
verifyParameter(this)
|
||||
val processedLong = long.handleCase(ignoreCase)
|
||||
val processedShort = short?.toString()?.handleCase(ignoreCase)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
obj.params[long] = this as ParameterData<Any>
|
||||
short?.let {
|
||||
obj.params[it] = this
|
||||
}
|
||||
return runCatching {
|
||||
type.createOption(processedLong, processedShort, description, isMandatory).apply {
|
||||
verifyParameter(this)
|
||||
registerParameter(this, processedLong, processedShort)
|
||||
}.prop
|
||||
}
|
||||
return result.getOrElse { showDefiningError(it.message ?: "") }
|
||||
}.getOrElse { showDefiningError(it.message ?: "") }
|
||||
}
|
||||
|
||||
private fun registerParameter(parameter: ParameterData<*>, long: String, short: String?) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val data = parameter as ParameterData<Any>
|
||||
obj.params[long] = data
|
||||
short?.let { obj.params[it] = data }
|
||||
}
|
||||
|
||||
private fun verifyParameter(parameter: ParameterData<*>) {
|
||||
if (!NAME_PATTERN.matches(parameter.long)) throw ArgumentParserException.invalidName(
|
||||
parameter.long
|
||||
)
|
||||
if (obj.params.containsKey(parameter.long)) throw ArgumentParserException.optionExists(
|
||||
parameter.long
|
||||
)
|
||||
parameter.short?.let {
|
||||
if (!SHORT_NAME_PATTERN.matches(it)) {
|
||||
throw ArgumentParserException.invalidShortName(it)
|
||||
}
|
||||
if (obj.params.containsKey(it)) throw ArgumentParserException.optionExists(it)
|
||||
val longName = parameter.long
|
||||
if (!NAME_PATTERN.matches(longName)) {
|
||||
throw ArgumentParserException.invalidName(longName)
|
||||
}
|
||||
val n = parameter.long.lowercase()
|
||||
if (n == "help" || n == "version") {
|
||||
if (obj.params.containsKey(longName)) {
|
||||
throw ArgumentParserException.optionExists(longName)
|
||||
}
|
||||
|
||||
parameter.short?.let { shortName ->
|
||||
if (!SHORT_NAME_PATTERN.matches(shortName)) {
|
||||
throw ArgumentParserException.invalidShortName(shortName)
|
||||
}
|
||||
if (obj.params.containsKey(shortName)) {
|
||||
throw ArgumentParserException.optionExists(shortName)
|
||||
}
|
||||
}
|
||||
|
||||
val lowerLong = longName.lowercase()
|
||||
if (lowerLong == "help" || lowerLong == "version") {
|
||||
throw ArgumentParserException.noHelpOrVersion()
|
||||
}
|
||||
|
||||
@@ -166,12 +173,60 @@ class SubCommand private constructor(
|
||||
* @param programVersion The version information of the program, including display name and copyright.
|
||||
* @param ignoreCase Determines whether the parsing should ignore case sensitivity for commands and parameters.
|
||||
* @param remainingDescription The description for remaining command-line arguments, or null if no arguments are allowed.
|
||||
* @param emptyCommandLineAllowed Indicates whether the parser should allow an empty command line to be parsed. Defaults to false.
|
||||
*
|
||||
* ```
|
||||
* // global parameters
|
||||
*
|
||||
* val commandLine = CommandLine(
|
||||
* programVersion = VERSION,
|
||||
* remainingDescription = "EPUB-Dateien",
|
||||
* ignoreCase = true,
|
||||
* )
|
||||
*
|
||||
* val param1 by commandLine.addStringParameter(
|
||||
* long = "param",
|
||||
* short = 'P',
|
||||
* description = "parameter description"
|
||||
* )
|
||||
* // more global parameters
|
||||
*
|
||||
* commandLine.setExecution { additionalParameters: List<String> ->
|
||||
* // called when no subcommand is specified
|
||||
* }
|
||||
*
|
||||
* // subcommand
|
||||
*
|
||||
* val commandSubCommand = commandLine.addSubCommand(
|
||||
* "command",
|
||||
* "My custom command",
|
||||
* listOf("cmd") // aliases
|
||||
* )
|
||||
*
|
||||
* val par1 by commandSubCommand.addStringParameter(
|
||||
* long = "par1",
|
||||
* description = "Parameter Par1 ",
|
||||
* isMandatory = true
|
||||
* )
|
||||
* // more parameters
|
||||
*
|
||||
* commandSubCommand.setExecution { additionalParameters ->
|
||||
* // called when this subcommand is found in the command line
|
||||
* }
|
||||
*
|
||||
* // more subcommands
|
||||
*
|
||||
* // start program
|
||||
* commandLine.parseAndRun(*args)
|
||||
* ```
|
||||
*/
|
||||
class CommandLine(
|
||||
val programVersion: Version,
|
||||
val ignoreCase: Boolean = false,
|
||||
val remainingDescription: String? = null, // null -> no remaining allowed
|
||||
val emptyCommandLineAllowed: Boolean = false,
|
||||
) : AddParameter {
|
||||
private var hasRun = false
|
||||
private val globalSubCommand = SubCommand.create(SubCommandData(), ignoreCase)
|
||||
private var subCommands = mutableMapOf<String, SubCommandData>()
|
||||
|
||||
@@ -210,68 +265,68 @@ class CommandLine(
|
||||
name: String,
|
||||
description: String,
|
||||
aliases: List<String> = emptyList(),
|
||||
): SubCommand {
|
||||
val result = runCatching {
|
||||
val name = verifySubCommand(name, ignoreCase)
|
||||
val subData = SubCommandData(name, description, aliases)
|
||||
subCommands[name] = subData
|
||||
aliases.forEach { alias ->
|
||||
val alias = verifySubCommand(alias, ignoreCase)
|
||||
subCommands[alias] = subData
|
||||
}
|
||||
SubCommand.create(subData, ignoreCase)
|
||||
): SubCommand = runCatching {
|
||||
val normalizedName = normalizeAndVerifySubCommand(name, ignoreCase)
|
||||
val subData = SubCommandData(normalizedName, description, aliases)
|
||||
subCommands[normalizedName] = subData
|
||||
aliases.forEach { alias ->
|
||||
val alias = normalizeAndVerifySubCommand(alias, ignoreCase)
|
||||
subCommands[alias] = subData
|
||||
}
|
||||
return result.getOrElse { showDefiningError(it.message ?: "") }
|
||||
}
|
||||
SubCommand.create(subData, ignoreCase)
|
||||
}.getOrElse { showDefiningError(it.message ?: "") }
|
||||
|
||||
fun parseAndRun(vararg args: String) {
|
||||
val result = runCatching {
|
||||
runCatching {
|
||||
val selectedSubCommand = parse(args.toList())
|
||||
selectedSubCommand.execute(selectedSubCommand.additionalParameters)
|
||||
}.onFailure { exception ->
|
||||
handleErrorAndShowHelp(exception as Exception)
|
||||
}
|
||||
result.exceptionOrNull()?.let { showHelp(it as Exception) }
|
||||
}
|
||||
|
||||
private fun verifySubCommand(cmd: String, ignoreCase: Boolean): String {
|
||||
val cmd = cmd.handleCase(ignoreCase)
|
||||
if (!matchSubCommand(cmd)) throw ArgumentParserException.invalidCommand(cmd)
|
||||
return cmd
|
||||
private fun normalizeAndVerifySubCommand(cmd: String, ignoreCase: Boolean): String {
|
||||
val normalized = cmd.handleCase(ignoreCase)
|
||||
if (!matchSubCommand(normalized)) throw ArgumentParserException.invalidCommand(normalized)
|
||||
return normalized
|
||||
}
|
||||
|
||||
private fun parse(args: List<String>): SubCommandData {
|
||||
if (args.isEmpty()) {
|
||||
throw ArgumentParserException.commandRequired()
|
||||
}
|
||||
if (hasRun) throw ArgumentParserException.noMoreParsing()
|
||||
hasRun = true
|
||||
if (args.isEmpty() && !emptyCommandLineAllowed) throw ArgumentParserException.commandRequired()
|
||||
checkExecutors()
|
||||
|
||||
val remaining = parseParameters(args, globalSubCommand.obj)
|
||||
val remainingAfterGlobal = parseParameters(args, globalSubCommand.obj)
|
||||
validateMandatoryParameters(globalSubCommand.obj)
|
||||
|
||||
val resultSubCommand = parseSubCommand(remaining)
|
||||
val resultSubCommand = parseSubCommand(remainingAfterGlobal)
|
||||
validateMandatoryParameters(resultSubCommand)
|
||||
|
||||
if (remainingDescription != null) {
|
||||
val additionalFromPipe = getRemainingParametersFromPipe()
|
||||
if (additionalFromPipe.isNotEmpty()) {
|
||||
resultSubCommand.additionalParameters += additionalFromPipe
|
||||
}
|
||||
resultSubCommand.additionalParameters += resolveRemainingFromPipe()
|
||||
} else if (resultSubCommand.additionalParameters.isNotEmpty()) {
|
||||
throw ArgumentParserException.noMoreOptions()
|
||||
}
|
||||
|
||||
return resultSubCommand
|
||||
}
|
||||
|
||||
private fun resolveRemainingFromPipe(): List<String> {
|
||||
return if (isStdInputReady()) getRemainingParametersFromPipe() else emptyList()
|
||||
}
|
||||
|
||||
private fun parseSubCommand(args: List<String>): SubCommandData {
|
||||
val sub = args.firstOrNull()?.handleCase(ignoreCase)?.let { subCommands[it] }
|
||||
?: return globalSubCommand.obj.apply { additionalParameters = args }
|
||||
val firstArg = args.firstOrNull()?.handleCase(ignoreCase)
|
||||
val sub = subCommands[firstArg] ?: return globalSubCommand.obj.apply {
|
||||
additionalParameters = args
|
||||
}
|
||||
return sub.apply { additionalParameters = parseParameters(args.drop(1), this) }
|
||||
}
|
||||
|
||||
private fun validateMandatoryParameters(subCommand: SubCommandData) {
|
||||
subCommand.params.forEach { (_, parameter) ->
|
||||
subCommand.params.filter { it.key == it.value.long }.forEach { (_, parameter) ->
|
||||
if (parameter.isMandatory && parameter.prop.value == null) {
|
||||
throw ArgumentParserException.optionRequiresValue(parameter.long)
|
||||
throw ArgumentParserException.optionRequired(parameter.long)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,17 +334,23 @@ class CommandLine(
|
||||
private fun parseParameters(args: List<String>, subCommand: SubCommandData): List<String> {
|
||||
var index = 0
|
||||
while (index < args.size) {
|
||||
val parseResult = matchArgument(args[index]) ?: return args.drop(index)
|
||||
val (key, inlineValue) = matchArgument(args[index]) ?: return args.drop(index)
|
||||
|
||||
var (key, inlineValue) = parseResult
|
||||
checkForHelpOrVersion(key)
|
||||
key = key.handleCase(ignoreCase)
|
||||
val normalizedKey = key.handleCase(ignoreCase)
|
||||
val param =
|
||||
subCommand.params[normalizedKey] ?: throw ArgumentParserException.unknownOption(
|
||||
normalizedKey
|
||||
)
|
||||
|
||||
val param = subCommand.params[key] ?: throw ArgumentParserException.unknownOption(key)
|
||||
|
||||
val (value, consumedExtra) = resolveParameterValue(args, index, key, inlineValue, param)
|
||||
val (value, consumedExtra) = resolveParameterValue(
|
||||
args,
|
||||
index,
|
||||
normalizedKey,
|
||||
inlineValue,
|
||||
param
|
||||
)
|
||||
param.type.convertAndSet(value, param.prop)
|
||||
|
||||
index += 1 + consumedExtra
|
||||
}
|
||||
return emptyList()
|
||||
@@ -312,7 +373,9 @@ class CommandLine(
|
||||
inlineValue.isNotBlank() -> inlineValue to 0
|
||||
|
||||
// --name --irgendwas ist nicht OK
|
||||
currentIndex < args.size - 1 && !ARGUMENT_PATTERN.matches(args[currentIndex + 1]) -> {
|
||||
currentIndex < args.size - 1
|
||||
&& !ARGUMENT_PATTERN.matches(args[currentIndex + 1])
|
||||
&& args[currentIndex + 1].isNotBlank() -> {
|
||||
args[currentIndex + 1] to 1
|
||||
}
|
||||
|
||||
@@ -322,12 +385,11 @@ class CommandLine(
|
||||
|
||||
private fun getRemainingParametersFromPipe(): List<String> {
|
||||
if (isStdInputReady()) {
|
||||
var input: String?
|
||||
val pipe = mutableListOf<String>()
|
||||
do {
|
||||
input = readlnOrNull()
|
||||
input?.let { pipe.add(it) }
|
||||
} while (!input.isNullOrEmpty())
|
||||
while (true) {
|
||||
val input = readlnOrNull() ?: break
|
||||
pipe.add(input)
|
||||
}
|
||||
return pipe
|
||||
}
|
||||
return emptyList()
|
||||
@@ -347,37 +409,28 @@ class CommandLine(
|
||||
}
|
||||
}
|
||||
|
||||
private fun showHelp(
|
||||
private fun handleErrorAndShowHelp(
|
||||
e: Exception?,
|
||||
): Nothing {
|
||||
val sub = !subCommands.isEmpty()
|
||||
val globalParam = globalSubCommand.obj.params.isNotEmpty()
|
||||
val remain = remainingDescription != null
|
||||
val hasSubCommands = subCommands.isNotEmpty()
|
||||
val hasGlobalParams = globalSubCommand.obj.params.isNotEmpty()
|
||||
val allowsRemaining = remainingDescription != null
|
||||
|
||||
println("\n${programVersion.displayName} ${programVersion.version} ${programVersion.copyright}\n")
|
||||
println(programVersion.description)
|
||||
println()
|
||||
println(
|
||||
"usage: ${programVersion.name} ${
|
||||
if (sub)
|
||||
if (globalParam) {
|
||||
"[GLOBAL PARAMETER]"
|
||||
} else {
|
||||
""
|
||||
} + " subcommand [PARAMETER]"
|
||||
else "[PARAMETER]"
|
||||
} ${if (remain) "<$remainingDescription>" else ""}"
|
||||
)
|
||||
println()
|
||||
if (globalParam) {
|
||||
if (sub) {
|
||||
println("Global Parameters:")
|
||||
} else {
|
||||
println("Parameters:")
|
||||
}
|
||||
println("${programVersion.description}\n")
|
||||
|
||||
val usagePrefix = "usage: ${programVersion.name}"
|
||||
val globalPart =
|
||||
if (hasGlobalParams) "[${if (hasSubCommands) "GLOBAL " else ""}PARAMETER]" else ""
|
||||
val subPart = if (hasSubCommands) " subcommand [PARAMETER]" else ""
|
||||
val remainPart = if (allowsRemaining) " <$remainingDescription>" else ""
|
||||
|
||||
println("$usagePrefix $globalPart$subPart$remainPart\n")
|
||||
if (hasGlobalParams) {
|
||||
println("${if (hasSubCommands) "Global " else ""}Parameters:")
|
||||
}
|
||||
showParameters(globalSubCommand.obj.params)
|
||||
if (sub) {
|
||||
if (hasSubCommands) {
|
||||
println()
|
||||
println("Subcommands:")
|
||||
showSubcommands(subCommands)
|
||||
@@ -400,21 +453,20 @@ class CommandLine(
|
||||
println("$INDENT${param.name}$aliases ${param.description}")
|
||||
showParameters(param.params)
|
||||
println()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun showParameters(parameters: MutableMap<String, ParameterData<Any>>) {
|
||||
var len = 0
|
||||
parameters.filter { it.key == it.value.long }.map { (_, param) ->
|
||||
len = maxOf(len, param.long.length + INDENT.length * 2 + 4 + 2)
|
||||
Pair(INDENT + INDENT + (param.short?.let { "-$it, " } ?: " ") + "--${param.long}",
|
||||
"${param.description} ${if (param.isMandatory) "*" else ""}")
|
||||
val parmStr = INDENT + INDENT +
|
||||
(param.short?.let { "-$it, " } ?: " ") +
|
||||
"--${param.long}" + if (param.type != ParameterType.BOOLEAN) " <...>" else ""
|
||||
len = maxOf(len, parmStr.length)
|
||||
Pair(parmStr, "${param.description} ${if (param.isMandatory) "*" else ""}")
|
||||
}.forEach {
|
||||
println(it.first.padEnd(len + 3) + it.second)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun showVersion(): Nothing {
|
||||
|
||||
@@ -157,42 +157,23 @@ internal class ArgumentParserException(message: String?) : RuntimeException(mess
|
||||
fun noHelpOrVersion() =
|
||||
ArgumentParserException("Option 'help' and option 'version' are not allowed.")
|
||||
|
||||
fun noParametersDefined() = ArgumentParserException("No parameters found for command line.")
|
||||
fun noExecutorDefinedGlobal() = ArgumentParserException("No lambda found for command line.")
|
||||
fun noExecutorDefined(name: String?) =
|
||||
ArgumentParserException("No lambda found for '$name'.")
|
||||
|
||||
fun optionExists(name: String) = ArgumentParserException("Option '$name' already exists.")
|
||||
|
||||
fun invalidAnnotationTargetParameter(name: String) =
|
||||
ArgumentParserException("Annotation @CmdLineParameter can only be used on mutable properties, '$name' isn't a mutable property.")
|
||||
|
||||
fun notNullableAnnotationTargetParameter(name: String) =
|
||||
ArgumentParserException("Annotation @CmdLineParameter can only be used on nullable properties, '$name' isn't a nullable property.")
|
||||
|
||||
fun invalidAnnotationTargetOption() =
|
||||
ArgumentParserException("Annotation @CmdLineOption can only be used on CommandLine Objects.")
|
||||
|
||||
fun optionNameRequired() = ArgumentParserException("Option name is required.")
|
||||
fun unknownOption(name: String) = ArgumentParserException("Unknown option '$name'.")
|
||||
fun unknownCommand(name: String) = ArgumentParserException("Unknown command '$name'.")
|
||||
fun optionRequiresValue(name: String) =
|
||||
ArgumentParserException("Option '$name' requires a value.")
|
||||
|
||||
fun optionNoValue(name: String) =
|
||||
ArgumentParserException("Option '$name' does not accept a value.")
|
||||
|
||||
fun booleanNotMandatory(name: String) =
|
||||
ArgumentParserException("Boolean option '$name' can not be mandatory.")
|
||||
|
||||
fun optionRequired(name: String) = ArgumentParserException("Option '$name' is required.")
|
||||
fun commandRequired() = ArgumentParserException("At least one command is required.")
|
||||
fun invalidLongValue(value: String) =
|
||||
ArgumentParserException("Value '$value' is not a long integer.")
|
||||
|
||||
fun invalidType(name: String, type: String) =
|
||||
ArgumentParserException("Unsupported parameter type: ${type} for parameter $name")
|
||||
|
||||
fun invalidShortName(name: String) =
|
||||
ArgumentParserException("Option short name '$name' is invalid.")
|
||||
|
||||
@@ -210,7 +191,7 @@ internal const val TRUE = "true"
|
||||
|
||||
internal const val NAME = """[a-zA-Z][\w-]+"""
|
||||
internal const val SHORT_NAME = """[a-zA-Z]"""
|
||||
internal val ARGUMENT_PATTERN = """^(?:-(${SHORT_NAME})|--(${NAME}))(?:=(.+)|$)$""".toRegex()
|
||||
internal val ARGUMENT_PATTERN = """^(?:-(${SHORT_NAME})|--(${NAME}))(?:[=:](.*)|$)$""".toRegex()
|
||||
internal val NAME_PATTERN = """^${NAME}$""".toRegex()
|
||||
internal val SHORT_NAME_PATTERN = """^${SHORT_NAME}$""".toRegex()
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ package net.keinekekse.util
|
||||
import net.keinekekse.version.Version
|
||||
|
||||
object VERSION: Version {
|
||||
override val version = "0.3.0-beta.8+build.117"
|
||||
override val fullVersion = "0.3.0-beta.8+build.117"
|
||||
override val version = "0.3.0-beta.19+build.129"
|
||||
override val fullVersion = "0.3.0-beta.19+build.129"
|
||||
override val major = 0
|
||||
override val minor = 3
|
||||
override val patch = 0
|
||||
override val preRelease = "beta.8"
|
||||
override val buildMetadata = "build.117"
|
||||
override val buildDate = "2026-01-19T00:08:01.397532Z"
|
||||
override val preRelease = "beta.19"
|
||||
override val buildMetadata = "build.129"
|
||||
override val buildDate = "2026-01-19T20:39:57.564140Z"
|
||||
override val displayName = "Utility Library"
|
||||
override val copyright = "Copyright (c) 2025-2026 Hartmut Bauer"
|
||||
override val name = "util"
|
||||
|
||||
@@ -386,19 +386,16 @@ private fun resolveParameterValue(
|
||||
args: List<String>,
|
||||
currentIndex: Int,
|
||||
key: String,
|
||||
inlineValue: String,
|
||||
parameterValue: String,
|
||||
param: CmdParameter,
|
||||
): Pair<String, Int> {
|
||||
return when {
|
||||
param.type == ParameterType.BOOLEAN -> {
|
||||
if (inlineValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key)
|
||||
if (parameterValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key)
|
||||
TRUE to 0
|
||||
}
|
||||
|
||||
// --name=--irgendwas ist OK
|
||||
inlineValue.isNotBlank() -> inlineValue to 0
|
||||
|
||||
// --name --irgendwas ist nicht OK
|
||||
parameterValue.isNotBlank() -> parameterValue to 0
|
||||
currentIndex < args.size - 1 && !ARGUMENT_PATTERN.matches(args[currentIndex + 1]) -> {
|
||||
args[currentIndex + 1] to 1
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user