diff --git a/src/main/kotlin/net/keinekekse/args/impl.kt b/src/main/kotlin/net/keinekekse/args/impl.kt deleted file mode 100644 index c68c296..0000000 --- a/src/main/kotlin/net/keinekekse/args/impl.kt +++ /dev/null @@ -1,232 +0,0 @@ -package net.keinekekse.args - -import kotlin.reflect.KProperty -import kotlin.system.exitProcess - -interface ValueDelegate { - var value: T? - operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = value - - fun updateValue(value: T?) { - this.value = value - } - - fun isEmpty() = value == null // needed? -} - -interface MutableValueDelegate : ValueDelegate { - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { - this.value = value - } -} - -class ParameterValue : ValueDelegate { - override var value: T? = null -} - -internal sealed class ParameterType { - - abstract fun createOption( - long: String, - short: String?, - description: String, - isMandatory: Boolean, - ): ParameterData - - abstract fun convertAndSet(value: String, opt: ValueDelegate) - - object STRING : ParameterType() { - override fun createOption( - long: String, - short: String?, - description: String, - isMandatory: Boolean, - ): ParameterData { - return ParameterData(long, short, description, isMandatory, ParameterValue(), this) - } - - override fun convertAndSet(value: String, opt: ValueDelegate) { - opt.updateValue(value) - } - } - - object LONG : ParameterType() { - override fun createOption( - long: String, - short: String?, - description: String, - isMandatory: Boolean, - ): ParameterData { - return ParameterData(long, short, description, isMandatory, ParameterValue(), this) - } - - override fun convertAndSet(value: String, opt: ValueDelegate) { - opt.updateValue( - value.trim().toLongOrNull() ?: throw ArgumentParserException.invalidLongValue(value) - ) - } - } - - object BOOLEAN : ParameterType() { - override fun createOption( - long: String, - short: String?, - description: String, - isMandatory: Boolean, - ): ParameterData { - return ParameterData( - long, - short, - description, - false, - ParameterValue().apply { updateValue(false) }, - this - ) - } - - override fun convertAndSet(value: String, opt: ValueDelegate) { - opt.updateValue(value == TRUE) - } - } -} - -internal data class SubCommandData( - val name: String = "", - val description: String = "", - val aliases: List = emptyList(), -// var run: (remaining: List) -> Unit = {}, - val params: MutableMap> = mutableMapOf(), - var additionalParameters: List = emptyList(), -) { - private var exec: RunProgram = {} - private var isSet: Boolean = false - - fun setExecution(run: RunProgram) { - this.exec = run - isSet = true - } - - fun checkExecutors() { - if (!isSet) throw ArgumentParserException.noExecutorDefined(name.ifEmpty { null } - ?: "global parameters") - } - - fun execute(remaining: List) { - exec.run(remaining) - } -} - -internal data class ParameterData( - val long: String, - val short: String? = null, - val description: String = "", - val isMandatory: Boolean = false, - val prop: ParameterValue, - val type: ParameterType, -) - -interface AddParameter { - fun addStringParameter( - long: String, - short: Char? = null, - description: String, - isMandatory: Boolean = false, - ): ParameterValue - - fun addLongParameter( - long: String, - short: Char? = null, - description: String, - isMandatory: Boolean = false, - ): ParameterValue - - fun addBooleanParameter( - long: String, - short: Char? = null, - description: String, - isMandatory: Boolean = false, - ): ParameterValue - - fun setExecution(run: RunProgram) -} - -// Fehler beim Check der Parameter-Definitionen und beim Parsen der Kommandozeile -internal class ArgumentParserException(message: String?) : RuntimeException(message) { - companion object { - const val PARSING_ERROR_MSG = "\n*** ERROR parsing arguments: %s\n" - const val DEFINITION_ERROR_MSG = "\n*** ERROR defining arguments: %s\n" - fun showHelp() = ArgumentParserException(null) - 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.") - - fun invalidName(name: String) = ArgumentParserException("Option name '$name' is invalid.") - fun invalidCommand(name: String) = - ArgumentParserException("Command name '$name' is invalid.") - - fun noMoreOptions() = ArgumentParserException("Too many arguments.") - fun noMoreParsing() = ArgumentParserException("Arguments have already been parsed.") - } -} - -internal const val INDENT = " " -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 NAME_PATTERN = """^${NAME}$""".toRegex() -internal val SHORT_NAME_PATTERN = """^${SHORT_NAME}$""".toRegex() - -internal fun String.handleCase(ignoreCase: Boolean): String { - return if (ignoreCase) lowercase() else this -} - -internal fun matchSubCommand(cmd: String) = - NAME_PATTERN.matches(cmd) - -internal fun matchArgument(arg: String): Pair? = - ARGUMENT_PATTERN.matchEntire(arg)?.groupValues?.let { Pair(it[1] + it[2], it[3]) } - -internal fun showDefiningError(msg: String): Nothing { - println(ArgumentParserException.DEFINITION_ERROR_MSG.format(msg)) - exitProcess(1) -} diff --git a/src/main/kotlin/net/keinekekse/args/priv.kt b/src/main/kotlin/net/keinekekse/args/priv.kt new file mode 100644 index 0000000..15ec705 --- /dev/null +++ b/src/main/kotlin/net/keinekekse/args/priv.kt @@ -0,0 +1,352 @@ +package net.keinekekse.args + +import net.keinekekse.version.Version +import kotlin.system.exitProcess + +class VerifyParameterException(message: String) : RuntimeException(message) + +fun showParsingError(msg: String): Nothing { + println(ArgumentParserException.PARSING_ERROR_MSG.format(msg)) + exitProcess(2) +} + +/** + * Functional interface representing a runnable program execution logic. + * + * This interface allows defining a lambda function or block of code that + * processes a list of remaining arguments. It is intended to be used as + * a mechanism to encapsulate the execution logic for commands or subcommands + * in a command-line parsing framework. + * + * The primary use case for this interface is to define execution behavior + * for subcommands or global commands, commonly used in CLI applications. + * + * @constructor A single abstract method is provided to execute the specified logic. + */ +fun interface RunProgram { + fun run(remaining: List) +} + +class SubCommand private constructor( + internal val obj: SubCommandData, + val ignoreCase: Boolean = false, +) : AddParameter { + companion object { + internal fun create(obj: SubCommandData, ignoreCase: Boolean = false): SubCommand = + SubCommand(obj, ignoreCase) + } + + override fun addStringParameter( + long: String, + short: Char?, + description: String, + isMandatory: Boolean, + ): ParameterValue { + return createParameter(ParameterType.STRING, long, short, description, isMandatory) + } + + override fun addLongParameter( + long: String, + short: Char?, + description: String, + isMandatory: Boolean, + ): ParameterValue { + return createParameter(ParameterType.LONG, long, short, description, isMandatory) + } + + override fun addBooleanParameter( + long: String, + short: Char?, + description: String, + isMandatory: Boolean, + ): ParameterValue { + return createParameter(ParameterType.BOOLEAN, long, short, description, isMandatory) + } + + override fun setExecution(run: RunProgram) { + obj.setExecution(run) + } + + private fun createParameter( + type: ParameterType, + long: String, + short: Char?, + description: String, + isMandatory: Boolean, + ): ParameterValue { + val result = runCatching { + val long = long.handleCase(ignoreCase) + val short = short?.toString()?.handleCase(ignoreCase) + type.createOption(long, short, description, isMandatory).apply { + verifyParameter(this) + + @Suppress("UNCHECKED_CAST") + obj.params[long] = this as ParameterData + short?.let { + obj.params[it] = this + } + }.prop + } + return result.getOrElse { showDefiningError(it.message ?: "") } + } + + 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 n = parameter.long.lowercase() + if (n == "help" || n == "version") { + throw ArgumentParserException.noHelpOrVersion() + } + + } + +} + +class CommandLine( + val programVersion: Version, + val ignoreCase: Boolean = false, + val remainingDescription: String? = null, // null -> no remaining allowed +) : AddParameter { + private val globalSubCommand = SubCommand.create(SubCommandData(), ignoreCase) + private var subCommands = mutableMapOf() + + override fun addStringParameter( + long: String, + short: Char?, + description: String, + isMandatory: Boolean, + ): ParameterValue { + return globalSubCommand.addStringParameter(long, short, description, isMandatory) + } + + override fun addLongParameter( + long: String, + short: Char?, + description: String, + isMandatory: Boolean, + ): ParameterValue { + return globalSubCommand.addLongParameter(long, short, description, isMandatory) + } + + override fun addBooleanParameter( + long: String, + short: Char?, + description: String, + isMandatory: Boolean, + ): ParameterValue { + return globalSubCommand.addBooleanParameter(long, short, description, isMandatory) + } + + override fun setExecution(run: RunProgram) { + globalSubCommand.setExecution(run) + } + + fun addSubCommand( + name: String, + description: String, + aliases: List = 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) + } + return result.getOrElse { showDefiningError(it.message ?: "") } + } + + fun parseAndRun(vararg args: String) { + val result = runCatching { + val selectedSubCommand = parse(args.toList()) + selectedSubCommand.execute(selectedSubCommand.additionalParameters) + } + 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 parse(args: List): SubCommandData { + if (args.isEmpty()) { + throw ArgumentParserException.commandRequired() + } + checkExecutors() + + val remaining = parseParameters(args, globalSubCommand.obj) + validateMandatoryParameters(globalSubCommand.obj) + + val resultSubCommand = parseSubCommand(remaining) + validateMandatoryParameters(resultSubCommand) + + if (resultSubCommand.additionalParameters.isNotEmpty() && remainingDescription != null) { + throw ArgumentParserException.noMoreOptions() + } + + return resultSubCommand + } + + private fun parseSubCommand(args: List): SubCommandData { + val sub = args.firstOrNull()?.handleCase(ignoreCase)?.let { subCommands[it] } + ?: return globalSubCommand.obj.apply { additionalParameters = args } + return sub.apply { additionalParameters = parseParameters(args.drop(1), this) } + } + + private fun validateMandatoryParameters(subCommand: SubCommandData) { + subCommand.params.forEach { (_, parameter) -> + if (parameter.isMandatory && parameter.prop.value == null) { + throw ArgumentParserException.optionRequiresValue(parameter.long) + } + } + } + + private fun parseParameters(args: List, subCommand: SubCommandData): List { + var index = 0 + while (index < args.size) { + val parseResult = matchArgument(args[index]) ?: return args.drop(index) + + var (key, inlineValue) = parseResult + checkForHelpOrVersion(key) + key = key.handleCase(ignoreCase) + + val param = subCommand.params[key] ?: throw ArgumentParserException.unknownOption(key) + + val (value, consumedExtra) = resolveParameterValue(args, index, key, inlineValue, param) + param.type.convertAndSet(value, param.prop) + + index += 1 + consumedExtra + } + return emptyList() + } + + private fun resolveParameterValue( + args: List, + currentIndex: Int, + key: String, + inlineValue: String, + param: ParameterData, + ): Pair { + return when { + param.type == ParameterType.BOOLEAN -> { + if (inlineValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key) + TRUE to 0 + } + + // --name=--irgendwas ist OK + inlineValue.isNotBlank() -> inlineValue to 0 + + // --name --irgendwas ist nicht OK + currentIndex < args.size - 1 && !ARGUMENT_PATTERN.matches(args[currentIndex + 1]) -> { + args[currentIndex + 1] to 1 + } + + else -> throw ArgumentParserException.optionRequiresValue(key) + } + } + + private fun checkExecutors() { + globalSubCommand.obj.checkExecutors() + subCommands.values.forEach { it.checkExecutors() } + } + + private fun checkForHelpOrVersion(key: String) { + val k = key.lowercase() + if (k == "help") { + throw ArgumentParserException.showHelp() + } else if (k == "version") { + showVersion() + } + } + + private fun showHelp( + e: Exception?, + ): Nothing { + val sub = !subCommands.isEmpty() + val globalParam = globalSubCommand.obj.params.isNotEmpty() + val remain = remainingDescription != null + + println("${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:") + } + } + showParameters(globalSubCommand.obj.params) + if (sub) { + println() + println("Subcommands:") + showSubcommands(subCommands) + } + e?.message?.let { + println() + println(ArgumentParserException.PARSING_ERROR_MSG.format(it)) + println() + } + exitProcess(0) + } + + private fun showSubcommands(parameters: MutableMap) { + parameters.filter { it.key == it.value.name }.forEach { (_, param) -> + val aliases = buildString { + param.aliases.forEach { alias -> + append(" | $alias") + } + } + println("$INDENT${param.name}$aliases ${param.description}") + showParameters(param.params) + println() + + } + + } + + private fun showParameters(parameters: MutableMap>) { + 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 ""}") + }.forEach { + println(it.first.padEnd(len + 3) + it.second) + } + + } + + private fun showVersion(): Nothing { + println("${programVersion.displayName} ${programVersion.fullVersion}, ${programVersion.copyright}\n") + exitProcess(0) + } + +} diff --git a/src/main/kotlin/net/keinekekse/args/pub.kt b/src/main/kotlin/net/keinekekse/args/pub.kt index 15ec705..c2d8b70 100644 --- a/src/main/kotlin/net/keinekekse/args/pub.kt +++ b/src/main/kotlin/net/keinekekse/args/pub.kt @@ -1,352 +1,231 @@ package net.keinekekse.args -import net.keinekekse.version.Version +import kotlin.reflect.KProperty import kotlin.system.exitProcess -class VerifyParameterException(message: String) : RuntimeException(message) +interface ValueDelegate { + var value: T? + operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = value -fun showParsingError(msg: String): Nothing { - println(ArgumentParserException.PARSING_ERROR_MSG.format(msg)) - exitProcess(2) + fun updateValue(value: T?) { + this.value = value + } + + fun isEmpty() = value == null // needed? } -/** - * Functional interface representing a runnable program execution logic. - * - * This interface allows defining a lambda function or block of code that - * processes a list of remaining arguments. It is intended to be used as - * a mechanism to encapsulate the execution logic for commands or subcommands - * in a command-line parsing framework. - * - * The primary use case for this interface is to define execution behavior - * for subcommands or global commands, commonly used in CLI applications. - * - * @constructor A single abstract method is provided to execute the specified logic. - */ -fun interface RunProgram { - fun run(remaining: List) +interface MutableValueDelegate : ValueDelegate { + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { + this.value = value + } } -class SubCommand private constructor( - internal val obj: SubCommandData, - val ignoreCase: Boolean = false, -) : AddParameter { +class ParameterValue : ValueDelegate { + override var value: T? = null +} + +internal sealed class ParameterType { + + abstract fun createOption( + long: String, + short: String?, + description: String, + isMandatory: Boolean, + ): ParameterData + + abstract fun convertAndSet(value: String, opt: ValueDelegate) + + object STRING : ParameterType() { + override fun createOption( + long: String, + short: String?, + description: String, + isMandatory: Boolean, + ): ParameterData { + return ParameterData(long, short, description, isMandatory, ParameterValue(), this) + } + + override fun convertAndSet(value: String, opt: ValueDelegate) { + opt.updateValue(value) + } + } + + object LONG : ParameterType() { + override fun createOption( + long: String, + short: String?, + description: String, + isMandatory: Boolean, + ): ParameterData { + return ParameterData(long, short, description, isMandatory, ParameterValue(), this) + } + + override fun convertAndSet(value: String, opt: ValueDelegate) { + opt.updateValue( + value.trim().toLongOrNull() ?: throw ArgumentParserException.invalidLongValue(value) + ) + } + } + + object BOOLEAN : ParameterType() { + override fun createOption( + long: String, + short: String?, + description: String, + isMandatory: Boolean, + ): ParameterData { + return ParameterData( + long, + short, + description, + false, + ParameterValue().apply { updateValue(false) }, + this + ) + } + + override fun convertAndSet(value: String, opt: ValueDelegate) { + opt.updateValue(value == TRUE) + } + } +} + +internal data class SubCommandData( + val name: String = "", + val description: String = "", + val aliases: List = emptyList(), + val params: MutableMap> = mutableMapOf(), + var additionalParameters: List = emptyList(), +) { + private var exec: RunProgram = {} + private var isSet: Boolean = false + + fun setExecution(run: RunProgram) { + this.exec = run + isSet = true + } + + fun checkExecutors() { + if (!isSet) throw ArgumentParserException.noExecutorDefined(name.ifEmpty { null } + ?: "global parameters") + } + + fun execute(remaining: List) { + exec.run(remaining) + } +} + +internal data class ParameterData( + val long: String, + val short: String? = null, + val description: String = "", + val isMandatory: Boolean = false, + val prop: ParameterValue, + val type: ParameterType, +) + +interface AddParameter { + fun addStringParameter( + long: String, + short: Char? = null, + description: String, + isMandatory: Boolean = false, + ): ParameterValue + + fun addLongParameter( + long: String, + short: Char? = null, + description: String, + isMandatory: Boolean = false, + ): ParameterValue + + fun addBooleanParameter( + long: String, + short: Char? = null, + description: String, + isMandatory: Boolean = false, + ): ParameterValue + + fun setExecution(run: RunProgram) +} + +// Fehler beim Check der Parameter-Definitionen und beim Parsen der Kommandozeile +internal class ArgumentParserException(message: String?) : RuntimeException(message) { companion object { - internal fun create(obj: SubCommandData, ignoreCase: Boolean = false): SubCommand = - SubCommand(obj, ignoreCase) + const val PARSING_ERROR_MSG = "\n*** ERROR parsing arguments: %s\n" + const val DEFINITION_ERROR_MSG = "\n*** ERROR defining arguments: %s\n" + fun showHelp() = ArgumentParserException(null) + 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.") + + fun invalidName(name: String) = ArgumentParserException("Option name '$name' is invalid.") + fun invalidCommand(name: String) = + ArgumentParserException("Command name '$name' is invalid.") + + fun noMoreOptions() = ArgumentParserException("Too many arguments.") + fun noMoreParsing() = ArgumentParserException("Arguments have already been parsed.") } - - override fun addStringParameter( - long: String, - short: Char?, - description: String, - isMandatory: Boolean, - ): ParameterValue { - return createParameter(ParameterType.STRING, long, short, description, isMandatory) - } - - override fun addLongParameter( - long: String, - short: Char?, - description: String, - isMandatory: Boolean, - ): ParameterValue { - return createParameter(ParameterType.LONG, long, short, description, isMandatory) - } - - override fun addBooleanParameter( - long: String, - short: Char?, - description: String, - isMandatory: Boolean, - ): ParameterValue { - return createParameter(ParameterType.BOOLEAN, long, short, description, isMandatory) - } - - override fun setExecution(run: RunProgram) { - obj.setExecution(run) - } - - private fun createParameter( - type: ParameterType, - long: String, - short: Char?, - description: String, - isMandatory: Boolean, - ): ParameterValue { - val result = runCatching { - val long = long.handleCase(ignoreCase) - val short = short?.toString()?.handleCase(ignoreCase) - type.createOption(long, short, description, isMandatory).apply { - verifyParameter(this) - - @Suppress("UNCHECKED_CAST") - obj.params[long] = this as ParameterData - short?.let { - obj.params[it] = this - } - }.prop - } - return result.getOrElse { showDefiningError(it.message ?: "") } - } - - 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 n = parameter.long.lowercase() - if (n == "help" || n == "version") { - throw ArgumentParserException.noHelpOrVersion() - } - - } - } -class CommandLine( - val programVersion: Version, - val ignoreCase: Boolean = false, - val remainingDescription: String? = null, // null -> no remaining allowed -) : AddParameter { - private val globalSubCommand = SubCommand.create(SubCommandData(), ignoreCase) - private var subCommands = mutableMapOf() +internal const val INDENT = " " +internal const val TRUE = "true" - override fun addStringParameter( - long: String, - short: Char?, - description: String, - isMandatory: Boolean, - ): ParameterValue { - return globalSubCommand.addStringParameter(long, short, description, isMandatory) - } - - override fun addLongParameter( - long: String, - short: Char?, - description: String, - isMandatory: Boolean, - ): ParameterValue { - return globalSubCommand.addLongParameter(long, short, description, isMandatory) - } - - override fun addBooleanParameter( - long: String, - short: Char?, - description: String, - isMandatory: Boolean, - ): ParameterValue { - return globalSubCommand.addBooleanParameter(long, short, description, isMandatory) - } - - override fun setExecution(run: RunProgram) { - globalSubCommand.setExecution(run) - } - - fun addSubCommand( - name: String, - description: String, - aliases: List = 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) - } - return result.getOrElse { showDefiningError(it.message ?: "") } - } - - fun parseAndRun(vararg args: String) { - val result = runCatching { - val selectedSubCommand = parse(args.toList()) - selectedSubCommand.execute(selectedSubCommand.additionalParameters) - } - 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 parse(args: List): SubCommandData { - if (args.isEmpty()) { - throw ArgumentParserException.commandRequired() - } - checkExecutors() - - val remaining = parseParameters(args, globalSubCommand.obj) - validateMandatoryParameters(globalSubCommand.obj) - - val resultSubCommand = parseSubCommand(remaining) - validateMandatoryParameters(resultSubCommand) - - if (resultSubCommand.additionalParameters.isNotEmpty() && remainingDescription != null) { - throw ArgumentParserException.noMoreOptions() - } - - return resultSubCommand - } - - private fun parseSubCommand(args: List): SubCommandData { - val sub = args.firstOrNull()?.handleCase(ignoreCase)?.let { subCommands[it] } - ?: return globalSubCommand.obj.apply { additionalParameters = args } - return sub.apply { additionalParameters = parseParameters(args.drop(1), this) } - } - - private fun validateMandatoryParameters(subCommand: SubCommandData) { - subCommand.params.forEach { (_, parameter) -> - if (parameter.isMandatory && parameter.prop.value == null) { - throw ArgumentParserException.optionRequiresValue(parameter.long) - } - } - } - - private fun parseParameters(args: List, subCommand: SubCommandData): List { - var index = 0 - while (index < args.size) { - val parseResult = matchArgument(args[index]) ?: return args.drop(index) - - var (key, inlineValue) = parseResult - checkForHelpOrVersion(key) - key = key.handleCase(ignoreCase) - - val param = subCommand.params[key] ?: throw ArgumentParserException.unknownOption(key) - - val (value, consumedExtra) = resolveParameterValue(args, index, key, inlineValue, param) - param.type.convertAndSet(value, param.prop) - - index += 1 + consumedExtra - } - return emptyList() - } - - private fun resolveParameterValue( - args: List, - currentIndex: Int, - key: String, - inlineValue: String, - param: ParameterData, - ): Pair { - return when { - param.type == ParameterType.BOOLEAN -> { - if (inlineValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key) - TRUE to 0 - } - - // --name=--irgendwas ist OK - inlineValue.isNotBlank() -> inlineValue to 0 - - // --name --irgendwas ist nicht OK - currentIndex < args.size - 1 && !ARGUMENT_PATTERN.matches(args[currentIndex + 1]) -> { - args[currentIndex + 1] to 1 - } - - else -> throw ArgumentParserException.optionRequiresValue(key) - } - } - - private fun checkExecutors() { - globalSubCommand.obj.checkExecutors() - subCommands.values.forEach { it.checkExecutors() } - } - - private fun checkForHelpOrVersion(key: String) { - val k = key.lowercase() - if (k == "help") { - throw ArgumentParserException.showHelp() - } else if (k == "version") { - showVersion() - } - } - - private fun showHelp( - e: Exception?, - ): Nothing { - val sub = !subCommands.isEmpty() - val globalParam = globalSubCommand.obj.params.isNotEmpty() - val remain = remainingDescription != null - - println("${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:") - } - } - showParameters(globalSubCommand.obj.params) - if (sub) { - println() - println("Subcommands:") - showSubcommands(subCommands) - } - e?.message?.let { - println() - println(ArgumentParserException.PARSING_ERROR_MSG.format(it)) - println() - } - exitProcess(0) - } - - private fun showSubcommands(parameters: MutableMap) { - parameters.filter { it.key == it.value.name }.forEach { (_, param) -> - val aliases = buildString { - param.aliases.forEach { alias -> - append(" | $alias") - } - } - println("$INDENT${param.name}$aliases ${param.description}") - showParameters(param.params) - println() - - } - - } - - private fun showParameters(parameters: MutableMap>) { - 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 ""}") - }.forEach { - println(it.first.padEnd(len + 3) + it.second) - } - - } - - private fun showVersion(): Nothing { - println("${programVersion.displayName} ${programVersion.fullVersion}, ${programVersion.copyright}\n") - exitProcess(0) - } +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 NAME_PATTERN = """^${NAME}$""".toRegex() +internal val SHORT_NAME_PATTERN = """^${SHORT_NAME}$""".toRegex() +internal fun String.handleCase(ignoreCase: Boolean): String { + return if (ignoreCase) lowercase() else this +} + +internal fun matchSubCommand(cmd: String) = + NAME_PATTERN.matches(cmd) + +internal fun matchArgument(arg: String): Pair? = + ARGUMENT_PATTERN.matchEntire(arg)?.groupValues?.let { Pair(it[1] + it[2], it[3]) } + +internal fun showDefiningError(msg: String): Nothing { + println(ArgumentParserException.DEFINITION_ERROR_MSG.format(msg)) + exitProcess(1) }