rename source files

This commit is contained in:
Hartmut Bauer committed 2026-01-18 15:06:34 +01:00
1 parent 3ec788db13
commit 1ee8dd356f
3 files changed
+559 -560

No files matched your search

-232
View File
@@ -1,232 +0,0 @@
package net.keinekekse.args
import kotlin.reflect.KProperty
import kotlin.system.exitProcess
interface ValueDelegate<T> {
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<T> : ValueDelegate<T> {
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
this.value = value
}
}
class ParameterValue<T> : ValueDelegate<T> {
override var value: T? = null
}
internal sealed class ParameterType<T> {
abstract fun createOption(
long: String,
short: String?,
description: String,
isMandatory: Boolean,
): ParameterData<T>
abstract fun convertAndSet(value: String, opt: ValueDelegate<T>)
object STRING : ParameterType<String>() {
override fun createOption(
long: String,
short: String?,
description: String,
isMandatory: Boolean,
): ParameterData<String> {
return ParameterData(long, short, description, isMandatory, ParameterValue(), this)
}
override fun convertAndSet(value: String, opt: ValueDelegate<String>) {
opt.updateValue(value)
}
}
object LONG : ParameterType<Long>() {
override fun createOption(
long: String,
short: String?,
description: String,
isMandatory: Boolean,
): ParameterData<Long> {
return ParameterData(long, short, description, isMandatory, ParameterValue(), this)
}
override fun convertAndSet(value: String, opt: ValueDelegate<Long>) {
opt.updateValue(
value.trim().toLongOrNull() ?: throw ArgumentParserException.invalidLongValue(value)
)
}
}
object BOOLEAN : ParameterType<Boolean>() {
override fun createOption(
long: String,
short: String?,
description: String,
isMandatory: Boolean,
): ParameterData<Boolean> {
return ParameterData(
long,
short,
description,
false,
ParameterValue<Boolean>().apply { updateValue(false) },
this
)
}
override fun convertAndSet(value: String, opt: ValueDelegate<Boolean>) {
opt.updateValue(value == TRUE)
}
}
}
internal data class SubCommandData(
val name: String = "",
val description: String = "",
val aliases: List<String> = emptyList(),
// var run: (remaining: List<String>) -> Unit = {},
val params: MutableMap<String, ParameterData<Any>> = mutableMapOf(),
var additionalParameters: List<String> = emptyList<String>(),
) {
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<String>) {
exec.run(remaining)
}
}
internal data class ParameterData<T>(
val long: String,
val short: String? = null,
val description: String = "",
val isMandatory: Boolean = false,
val prop: ParameterValue<T>,
val type: ParameterType<T>,
)
interface AddParameter {
fun addStringParameter(
long: String,
short: Char? = null,
description: String,
isMandatory: Boolean = false,
): ParameterValue<String>
fun addLongParameter(
long: String,
short: Char? = null,
description: String,
isMandatory: Boolean = false,
): ParameterValue<Long>
fun addBooleanParameter(
long: String,
short: Char? = null,
description: String,
isMandatory: Boolean = false,
): ParameterValue<Boolean>
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<String, String>? =
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)
}
+352
View File
@@ -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<String>)
}
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<String> {
return createParameter(ParameterType.STRING, long, short, description, isMandatory)
}
override fun addLongParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<Long> {
return createParameter(ParameterType.LONG, long, short, description, isMandatory)
}
override fun addBooleanParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<Boolean> {
return createParameter(ParameterType.BOOLEAN, long, short, description, isMandatory)
}
override fun setExecution(run: RunProgram) {
obj.setExecution(run)
}
private fun <T> createParameter(
type: ParameterType<T>,
long: String,
short: Char?,
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)
@Suppress("UNCHECKED_CAST")
obj.params[long] = this as ParameterData<Any>
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<String, SubCommandData>()
override fun addStringParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<String> {
return globalSubCommand.addStringParameter(long, short, description, isMandatory)
}
override fun addLongParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<Long> {
return globalSubCommand.addLongParameter(long, short, description, isMandatory)
}
override fun addBooleanParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<Boolean> {
return globalSubCommand.addBooleanParameter(long, short, description, isMandatory)
}
override fun setExecution(run: RunProgram) {
globalSubCommand.setExecution(run)
}
fun addSubCommand(
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)
}
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<String>): 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<String>): 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<String>, subCommand: SubCommandData): List<String> {
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<String>,
currentIndex: Int,
key: String,
inlineValue: String,
param: ParameterData<Any>,
): Pair<String, Int> {
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<String, SubCommandData>) {
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<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 ""}")
}.forEach {
println(it.first.padEnd(len + 3) + it.second)
}
}
private fun showVersion(): Nothing {
println("${programVersion.displayName} ${programVersion.fullVersion}, ${programVersion.copyright}\n")
exitProcess(0)
}
}
+207 -328
View File
@@ -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<T> {
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<String>)
interface MutableValueDelegate<T> : ValueDelegate<T> {
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<T> : ValueDelegate<T> {
override var value: T? = null
}
internal sealed class ParameterType<T> {
abstract fun createOption(
long: String,
short: String?,
description: String,
isMandatory: Boolean,
): ParameterData<T>
abstract fun convertAndSet(value: String, opt: ValueDelegate<T>)
object STRING : ParameterType<String>() {
override fun createOption(
long: String,
short: String?,
description: String,
isMandatory: Boolean,
): ParameterData<String> {
return ParameterData(long, short, description, isMandatory, ParameterValue(), this)
}
override fun convertAndSet(value: String, opt: ValueDelegate<String>) {
opt.updateValue(value)
}
}
object LONG : ParameterType<Long>() {
override fun createOption(
long: String,
short: String?,
description: String,
isMandatory: Boolean,
): ParameterData<Long> {
return ParameterData(long, short, description, isMandatory, ParameterValue(), this)
}
override fun convertAndSet(value: String, opt: ValueDelegate<Long>) {
opt.updateValue(
value.trim().toLongOrNull() ?: throw ArgumentParserException.invalidLongValue(value)
)
}
}
object BOOLEAN : ParameterType<Boolean>() {
override fun createOption(
long: String,
short: String?,
description: String,
isMandatory: Boolean,
): ParameterData<Boolean> {
return ParameterData(
long,
short,
description,
false,
ParameterValue<Boolean>().apply { updateValue(false) },
this
)
}
override fun convertAndSet(value: String, opt: ValueDelegate<Boolean>) {
opt.updateValue(value == TRUE)
}
}
}
internal data class SubCommandData(
val name: String = "",
val description: String = "",
val aliases: List<String> = emptyList(),
val params: MutableMap<String, ParameterData<Any>> = mutableMapOf(),
var additionalParameters: List<String> = emptyList<String>(),
) {
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<String>) {
exec.run(remaining)
}
}
internal data class ParameterData<T>(
val long: String,
val short: String? = null,
val description: String = "",
val isMandatory: Boolean = false,
val prop: ParameterValue<T>,
val type: ParameterType<T>,
)
interface AddParameter {
fun addStringParameter(
long: String,
short: Char? = null,
description: String,
isMandatory: Boolean = false,
): ParameterValue<String>
fun addLongParameter(
long: String,
short: Char? = null,
description: String,
isMandatory: Boolean = false,
): ParameterValue<Long>
fun addBooleanParameter(
long: String,
short: Char? = null,
description: String,
isMandatory: Boolean = false,
): ParameterValue<Boolean>
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.")
override fun addStringParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<String> {
return createParameter(ParameterType.STRING, long, short, description, isMandatory)
}
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'.")
override fun addLongParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<Long> {
return createParameter(ParameterType.LONG, long, short, description, isMandatory)
}
fun optionExists(name: String) = ArgumentParserException("Option '$name' already exists.")
override fun addBooleanParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<Boolean> {
return createParameter(ParameterType.BOOLEAN, long, short, description, isMandatory)
}
fun invalidAnnotationTargetParameter(name: String) =
ArgumentParserException("Annotation @CmdLineParameter can only be used on mutable properties, '$name' isn't a mutable property.")
override fun setExecution(run: RunProgram) {
obj.setExecution(run)
}
fun notNullableAnnotationTargetParameter(name: String) =
ArgumentParserException("Annotation @CmdLineParameter can only be used on nullable properties, '$name' isn't a nullable property.")
private fun <T> createParameter(
type: ParameterType<T>,
long: String,
short: Char?,
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)
fun invalidAnnotationTargetOption() =
ArgumentParserException("Annotation @CmdLineOption can only be used on CommandLine Objects.")
@Suppress("UNCHECKED_CAST")
obj.params[long] = this as ParameterData<Any>
short?.let {
obj.params[it] = this
}
}.prop
}
return result.getOrElse { showDefiningError(it.message ?: "") }
}
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.")
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()
}
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.")
}
}
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<String, SubCommandData>()
internal const val INDENT = " "
internal const val TRUE = "true"
override fun addStringParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<String> {
return globalSubCommand.addStringParameter(long, short, description, isMandatory)
}
override fun addLongParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<Long> {
return globalSubCommand.addLongParameter(long, short, description, isMandatory)
}
override fun addBooleanParameter(
long: String,
short: Char?,
description: String,
isMandatory: Boolean,
): ParameterValue<Boolean> {
return globalSubCommand.addBooleanParameter(long, short, description, isMandatory)
}
override fun setExecution(run: RunProgram) {
globalSubCommand.setExecution(run)
}
fun addSubCommand(
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)
}
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<String>): 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<String>): 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<String>, subCommand: SubCommandData): List<String> {
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<String>,
currentIndex: Int,
key: String,
inlineValue: String,
param: ParameterData<Any>,
): Pair<String, Int> {
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<String, SubCommandData>) {
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<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 ""}")
}.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<String, String>? =
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)
}