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) } }