Compare commits
4
Commits
907e8c7770
...
3ec788db13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ec788db13 | ||
|
|
22e1e04673 | ||
|
|
d2948e500c | ||
|
|
8aa418c0f1 |
No files matched your search
Generated
+1
-1
@@ -4,5 +4,5 @@
|
|||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="21" project-jdk-type="JavaSDK" />
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
|
||||||
</project>
|
</project>
|
||||||
+4
-1
@@ -14,7 +14,7 @@ group = "net.keinekekse"
|
|||||||
version = masterLibs.versions.utilLibraryVersion.get()
|
version = masterLibs.versions.utilLibraryVersion.get()
|
||||||
description = "Kotlin Utility Library"
|
description = "Kotlin Utility Library"
|
||||||
extra["displayname"] = "Utility Library"
|
extra["displayname"] = "Utility Library"
|
||||||
extra["copyright"] = "Copyright (c) 2025 Hartmut Bauer"
|
extra["copyright"] = "Copyright (c) 2025-2026 Hartmut Bauer"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://nexus.keinekekse.net/repository/maven-public/")
|
maven("https://nexus.keinekekse.net/repository/maven-public/")
|
||||||
@@ -22,6 +22,9 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(masterLibs.kotlinxSerialization)
|
implementation(masterLibs.kotlinxSerialization)
|
||||||
|
// f. cmd line via annotation KMP?
|
||||||
|
implementation(kotlin("reflect"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#auto generated
|
#auto generated
|
||||||
#Sat Dec 20 00:41:12 CET 2025
|
#Sun Jan 18 02:02:24 CET 2026
|
||||||
build.number=103
|
build.number=106
|
||||||
+1
-1
@@ -19,7 +19,7 @@ dependencyResolutionManagement {
|
|||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
create("masterLibs") {
|
create("masterLibs") {
|
||||||
// from(files("gradle/masterlibs.versions.toml"))
|
// from(files("gradle/masterlibs.versions.toml"))
|
||||||
from("net.keinekekse:version-catalog:100")
|
from("net.keinekekse:version-catalog:107")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package net.keinekekse.args.annotation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to define metadata for a command-line argument.
|
||||||
|
*
|
||||||
|
* This annotation can be applied to properties to specify their representation
|
||||||
|
* as command-line arguments. It provides options for defining the argument's
|
||||||
|
* name, shortened single-character alias, description, and whether it is mandatory.
|
||||||
|
*
|
||||||
|
* The property must be declared as mutable and nullable.
|
||||||
|
*
|
||||||
|
* @property name The full name of the command-line argument. Defaults to the name of the property.
|
||||||
|
* @property shortName The single-character alias for the argument. Defaults to [Char.MIN_VALUE],
|
||||||
|
* indicating no short name is defined.
|
||||||
|
* @property description A brief description of the argument's purpose. Defaults to an empty string.
|
||||||
|
* @property isMandatory Specifies if the argument is mandatory. Defaults to `false`.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class CmdLineArgument(
|
||||||
|
val name: String = "",
|
||||||
|
val shortName: Char = Char.MIN_VALUE,
|
||||||
|
val description: String = "",
|
||||||
|
val isMandatory: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to define a subcommand for a command-line interface.
|
||||||
|
*
|
||||||
|
* This annotation can be applied to objects to designate them as subcommands
|
||||||
|
* for a command-line tool. It specifies the subcommand's name, a brief description,
|
||||||
|
* and optional aliases that can be used to invoke the subcommand.
|
||||||
|
*
|
||||||
|
* @property name The primary identifier for the subcommand. Defaults to the simple name of the class.
|
||||||
|
* @property description A short description of the subcommand's purpose. Defaults to an empty string.
|
||||||
|
* @property aliases Alternative names that can be used to invoke the subcommand. Defaults to an
|
||||||
|
* empty array.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class CmdLineSubCommand(
|
||||||
|
val name: String = "",
|
||||||
|
val description: String = "",
|
||||||
|
val aliases: Array<String> = [],
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a set of parameters associated globally with the program.
|
||||||
|
* This class adds additional properties and customization options to manage
|
||||||
|
* the behavior of the command line interpretation, such as allowing additional parameters,
|
||||||
|
* handling case sensitivity, and keeping track of command execution status.
|
||||||
|
*
|
||||||
|
* The parameters have to be annotated with the [CmdLineArgument] annotation.
|
||||||
|
* Subcommands can be defined using the [CmdLineSubCommand] annotation on objects,
|
||||||
|
* derived from the [SubCommand] class.
|
||||||
|
*
|
||||||
|
* @property programDescription A description of the program's purpose or functionality.
|
||||||
|
* @property additionalParametersAllowed Indicates whether the program allows additional,
|
||||||
|
* unspecified parameters during execution.
|
||||||
|
* @property ignoreCase Determines if parameter matching should be case-insensitive.
|
||||||
|
*/
|
||||||
|
abstract class Parameters(
|
||||||
|
val programDescription: String = "",
|
||||||
|
val additionalParametersAllowed: Boolean = true,
|
||||||
|
val ignoreCase: Boolean = false,
|
||||||
|
var didRun: Boolean = false,
|
||||||
|
) : SubCommand()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an abstract subcommand that can be executed with specific behavior and validation logic.
|
||||||
|
*
|
||||||
|
* Subclasses of this class are expected to provide implementation for validating their parameters
|
||||||
|
* and defining the execution logic of the subcommand. Each subcommand can also handle additional
|
||||||
|
* parameters.
|
||||||
|
*
|
||||||
|
* @property additionalParameters A list of additional command line parameters associated with this
|
||||||
|
* subcommand.
|
||||||
|
*/
|
||||||
|
abstract class SubCommand {
|
||||||
|
var additionalParameters: List<String> = emptyList()
|
||||||
|
|
||||||
|
abstract fun verifyParameters(): Boolean
|
||||||
|
abstract fun run(globals: Parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the program logic using the specified command-line arguments.
|
||||||
|
* This method internally invokes the `run` method of the `Parameters` class
|
||||||
|
* to parse, validate, and execute the input arguments.
|
||||||
|
*
|
||||||
|
* @param args A list of command-line arguments to be processed by the program.
|
||||||
|
*/
|
||||||
|
fun Parameters.runProgramWith(args: List<String>) {
|
||||||
|
run(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown to indicate that there is an error related to verifying
|
||||||
|
* the validity of command-line parameters provided to the application.
|
||||||
|
*
|
||||||
|
* This exception is thrown by the [SubCommand.verifyParameters] method to indicate
|
||||||
|
* that the parameters provided to the subcommand are invalid.
|
||||||
|
*
|
||||||
|
* The exception is caught by the [Parameters.runProgramWith] method and the program
|
||||||
|
* execution will be terminated with an error message and the usage information.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* Constructs a new [VerifyParameterException] with the specified detail message.
|
||||||
|
*
|
||||||
|
* @param message The detail message describing the parameter verification error.
|
||||||
|
*/
|
||||||
|
class VerifyParameterException(message: String) : RuntimeException(message)
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
package net.keinekekse.args.annotation
|
||||||
|
|
||||||
|
import kotlin.reflect.KMutableProperty1
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
import kotlin.reflect.full.hasAnnotation
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
import kotlin.reflect.jvm.isAccessible
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
// entry point
|
||||||
|
fun Parameters.run(args: List<String>) {
|
||||||
|
var allParameters: Map<String, CmdSubCommand> = emptyMap()
|
||||||
|
try {
|
||||||
|
if (didRun) throw ArgumentParserException.noMoreParsing() else didRun = true
|
||||||
|
allParameters = fetchAllParameters()
|
||||||
|
|
||||||
|
val subCommand = parseCommandLine(args, allParameters, ignoreCase)
|
||||||
|
if (subCommand.verifyParameters()) {
|
||||||
|
subCommand.run(this)
|
||||||
|
}
|
||||||
|
} catch (e: ArgumentParserException) {
|
||||||
|
showHelp(e, allParameters, programDescription)
|
||||||
|
} catch (e: VerifyParameterException) {
|
||||||
|
showHelp(e, allParameters, programDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fehler beim Check der Parameter-Definitionen und beim Parsen der Kommandozeile
|
||||||
|
private class ArgumentParserException(message: String?) : RuntimeException(message) {
|
||||||
|
companion object {
|
||||||
|
const val ERROR_MSG = "\n*** ERROR parsing 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 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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val GLOBAL_PARAMETERS = "#global#"
|
||||||
|
private const val TRUE = "true"
|
||||||
|
|
||||||
|
private const val NAME = """[a-zA-Z][\w-]+"""
|
||||||
|
private const val SHORT_NAME = """[a-zA-Z]"""
|
||||||
|
private val ARGUMENT_PATTERN = """^(?:-(${SHORT_NAME})|--(${NAME}))(?:=(.+)|$)$""".toRegex()
|
||||||
|
private val NAME_PATTERN = """^${NAME}$""".toRegex()
|
||||||
|
private val SHORT_NAME_PATTERN = """^${SHORT_NAME}$""".toRegex()
|
||||||
|
|
||||||
|
private sealed interface ParameterType {
|
||||||
|
fun convertAndSet(
|
||||||
|
value: String,
|
||||||
|
obj: SubCommand,
|
||||||
|
prop: KMutableProperty1<out SubCommand, *>,
|
||||||
|
)
|
||||||
|
|
||||||
|
object STRING : ParameterType {
|
||||||
|
override fun convertAndSet(
|
||||||
|
value: String,
|
||||||
|
obj: SubCommand,
|
||||||
|
prop: KMutableProperty1<out SubCommand, *>,
|
||||||
|
) {
|
||||||
|
prop.setter.call(obj, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object LONG : ParameterType {
|
||||||
|
override fun convertAndSet(
|
||||||
|
value: String,
|
||||||
|
obj: SubCommand,
|
||||||
|
prop: KMutableProperty1<out SubCommand, *>,
|
||||||
|
) {
|
||||||
|
val valueLong =
|
||||||
|
value.trim().toLongOrNull() ?: throw ArgumentParserException.invalidLongValue(value)
|
||||||
|
prop.setter.call(obj, valueLong)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object BOOLEAN : ParameterType {
|
||||||
|
override fun convertAndSet(
|
||||||
|
value: String,
|
||||||
|
obj: SubCommand,
|
||||||
|
prop: KMutableProperty1<out SubCommand, *>,
|
||||||
|
) {
|
||||||
|
val valueBool = value.toBoolean()
|
||||||
|
prop.setter.call(obj, valueBool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private typealias CmdParameterMap = Map<String, CmdParameter>
|
||||||
|
private typealias CmdSubCommandMap = Map<String, CmdSubCommand>
|
||||||
|
|
||||||
|
private data class CmdSubCommand(
|
||||||
|
val option: String = GLOBAL_PARAMETERS,
|
||||||
|
val description: String = "",
|
||||||
|
val aliases: List<String> = emptyList(),
|
||||||
|
val obj: SubCommand,
|
||||||
|
val params: CmdParameterMap,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class CmdParameter(
|
||||||
|
val long: String,
|
||||||
|
val short: Char? = null,
|
||||||
|
val description: String = "",
|
||||||
|
val isMandatory: Boolean = false,
|
||||||
|
val type: ParameterType,
|
||||||
|
val prop: KMutableProperty1<out SubCommand, *>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// handle annotations
|
||||||
|
|
||||||
|
private fun Parameters.fetchAllParameters(): CmdSubCommandMap {
|
||||||
|
verifyParametersExist()
|
||||||
|
val globalParams = getParameters(ignoreCase)
|
||||||
|
|
||||||
|
return mutableMapOf<String, CmdSubCommand>().apply {
|
||||||
|
put(GLOBAL_PARAMETERS, CmdSubCommand(obj = this@fetchAllParameters, params = globalParams))
|
||||||
|
getSubCommands(this, ignoreCase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SubCommand.getParameters(ignoreCase: Boolean): CmdParameterMap {
|
||||||
|
val params = mutableMapOf<String, CmdParameter>()
|
||||||
|
this::class.memberProperties
|
||||||
|
.filter { it.hasAnnotation<CmdLineArgument>() }
|
||||||
|
.forEach { property ->
|
||||||
|
val mutableProp = validateProperty(property)
|
||||||
|
|
||||||
|
val param = mutableProp.toCmdParameter(this, ignoreCase)
|
||||||
|
if (params.containsKey(param.long)) throw ArgumentParserException.optionExists(param.long)
|
||||||
|
params[param.long] = param
|
||||||
|
|
||||||
|
param.short?.let {
|
||||||
|
val s = it.toString()
|
||||||
|
if (params.containsKey(s)) throw ArgumentParserException.optionExists(s)
|
||||||
|
params[s] = param
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun KMutableProperty1<out SubCommand, *>.toCmdParameter(
|
||||||
|
instance: SubCommand,
|
||||||
|
ignoreCase: Boolean,
|
||||||
|
): CmdParameter {
|
||||||
|
val annotation = findAnnotation<CmdLineArgument>()!!
|
||||||
|
|
||||||
|
val name = (annotation.name.takeIf { it.isNotBlank() } ?: name).handle(ignoreCase)
|
||||||
|
verifyNoHelpOrVersion(name)
|
||||||
|
if (!NAME_PATTERN.matches(name)) throw ArgumentParserException.invalidName(name)
|
||||||
|
|
||||||
|
val short = annotation.shortName.takeIf { it.isLetter() || it.isDigit() }?.let {
|
||||||
|
if (!SHORT_NAME_PATTERN.matches(it.toString())) {
|
||||||
|
throw ArgumentParserException.invalidShortName(it.toString())
|
||||||
|
}
|
||||||
|
if (ignoreCase) it.lowercaseChar() else it
|
||||||
|
}
|
||||||
|
|
||||||
|
return CmdParameter(
|
||||||
|
long = name,
|
||||||
|
short = short,
|
||||||
|
description = annotation.description,
|
||||||
|
isMandatory = annotation.isMandatory,
|
||||||
|
type = this.determineParameterType(name, annotation.isMandatory, instance),
|
||||||
|
prop = this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun KMutableProperty1<out SubCommand, *>.determineParameterType(
|
||||||
|
name: String,
|
||||||
|
isMandatory: Boolean,
|
||||||
|
instance: SubCommand,
|
||||||
|
): ParameterType {
|
||||||
|
return when (returnType.classifier) {
|
||||||
|
String::class -> ParameterType.STRING
|
||||||
|
Long::class -> ParameterType.LONG
|
||||||
|
Boolean::class -> {
|
||||||
|
if (isMandatory) throw ArgumentParserException.booleanNotMandatory(name)
|
||||||
|
ParameterType.BOOLEAN.also { it.convertAndSet("false", instance, this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw ArgumentParserException.invalidType(name, returnType.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateProperty(property: KProperty1<out SubCommand, *>): KMutableProperty1<out SubCommand, *> {
|
||||||
|
// 1. mutable
|
||||||
|
val mutableProp = property as? KMutableProperty1<out SubCommand, *>
|
||||||
|
?: throw ArgumentParserException.invalidAnnotationTargetParameter(property.name)
|
||||||
|
// 2. nullable
|
||||||
|
if (!mutableProp.returnType.isMarkedNullable) throw ArgumentParserException.notNullableAnnotationTargetParameter(
|
||||||
|
property.name
|
||||||
|
)
|
||||||
|
// 3. override 'private'
|
||||||
|
mutableProp.isAccessible = true
|
||||||
|
|
||||||
|
return mutableProp
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SubCommand.getSubCommands(
|
||||||
|
optionList: MutableMap<String, CmdSubCommand>,
|
||||||
|
ignoreCase: Boolean,
|
||||||
|
) {
|
||||||
|
val subCommandClasses =
|
||||||
|
this::class.nestedClasses.filter { it.hasAnnotation<CmdLineSubCommand>() }
|
||||||
|
|
||||||
|
subCommandClasses.forEach { kClass ->
|
||||||
|
val annotation = kClass.findAnnotation<CmdLineSubCommand>()!!
|
||||||
|
val cmdObj = kClass.objectInstance as? SubCommand
|
||||||
|
?: throw ArgumentParserException.invalidAnnotationTargetOption()
|
||||||
|
|
||||||
|
val name = resolveCommandName(kClass, annotation, ignoreCase)
|
||||||
|
val cmdSubCommand = CmdSubCommand(
|
||||||
|
option = name,
|
||||||
|
description = annotation.description,
|
||||||
|
aliases = annotation.aliases.toList().map { it.handle(ignoreCase) },
|
||||||
|
obj = cmdObj,
|
||||||
|
params = cmdObj.getParameters(ignoreCase)
|
||||||
|
)
|
||||||
|
|
||||||
|
optionList[name] = cmdSubCommand
|
||||||
|
annotation.aliases.forEach { alias ->
|
||||||
|
optionList[alias] = cmdSubCommand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveCommandName(
|
||||||
|
kClass: kotlin.reflect.KClass<*>,
|
||||||
|
annotation: CmdLineSubCommand,
|
||||||
|
ignoreCase: Boolean,
|
||||||
|
): String {
|
||||||
|
val rawName = annotation.name.takeIf { it.isNotBlank() }
|
||||||
|
?: kClass.simpleName
|
||||||
|
?: throw ArgumentParserException.optionNameRequired()
|
||||||
|
|
||||||
|
return rawName.handle(ignoreCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Parameters.verifyParametersExist() {
|
||||||
|
val me = this::class
|
||||||
|
val hasParam = me.memberProperties.any { it.hasAnnotation<CmdLineArgument>() }
|
||||||
|
val hasSubcommand = me.nestedClasses.any { it.hasAnnotation<CmdLineSubCommand>() }
|
||||||
|
if (!hasParam && !hasSubcommand) {
|
||||||
|
throw ArgumentParserException.noParametersDefined()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyNoHelpOrVersion(name: String) {
|
||||||
|
val n = name.lowercase()
|
||||||
|
if (n == "help" || n == "version") {
|
||||||
|
throw ArgumentParserException.noHelpOrVersion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse command line arguments
|
||||||
|
|
||||||
|
private fun Parameters.parseCommandLine(
|
||||||
|
args: List<String>,
|
||||||
|
parameters: CmdSubCommandMap,
|
||||||
|
ignoreCase: Boolean,
|
||||||
|
): SubCommand {
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
throw ArgumentParserException.commandRequired()
|
||||||
|
}
|
||||||
|
|
||||||
|
val globals = parameters[GLOBAL_PARAMETERS]!!
|
||||||
|
val remaining = parseParameters(args, globals, ignoreCase)
|
||||||
|
validateMandatoryParameters(globals)
|
||||||
|
|
||||||
|
val resultSubCommand = if (parameters.size > 1) {
|
||||||
|
parseSubCommand(remaining, parameters, ignoreCase)
|
||||||
|
} else {
|
||||||
|
globals.obj.apply { additionalParameters = remaining }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultSubCommand.additionalParameters.isNotEmpty() && !additionalParametersAllowed) {
|
||||||
|
throw ArgumentParserException.noMoreOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultSubCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseSubCommand(
|
||||||
|
args: List<String>,
|
||||||
|
parameters: CmdSubCommandMap,
|
||||||
|
ignoreCase: Boolean,
|
||||||
|
): SubCommand {
|
||||||
|
val subCommandData = getSubCommand(args, parameters, ignoreCase)
|
||||||
|
val remainingAfterSub = parseParameters(args.drop(1), subCommandData, ignoreCase)
|
||||||
|
return subCommandData.obj.apply {
|
||||||
|
additionalParameters = remainingAfterSub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// property has to be nullable for this to work
|
||||||
|
private fun validateMandatoryParameters(arguments: CmdSubCommand) {
|
||||||
|
arguments.params.forEach { (_, param) ->
|
||||||
|
if (param.isMandatory && param.prop.getter.call(arguments.obj) == null) {
|
||||||
|
throw ArgumentParserException.optionRequired(param.long)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSubCommand(
|
||||||
|
args: List<String>,
|
||||||
|
parameters: CmdSubCommandMap,
|
||||||
|
ignoreCase: Boolean,
|
||||||
|
): CmdSubCommand {
|
||||||
|
val commandName =
|
||||||
|
args.firstOrNull()?.handle(ignoreCase) ?: throw ArgumentParserException.commandRequired()
|
||||||
|
|
||||||
|
if (!matchSubCommand(commandName)) {
|
||||||
|
throw ArgumentParserException.invalidCommand(commandName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameters[commandName] ?: throw ArgumentParserException.unknownCommand(commandName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseParameters(
|
||||||
|
args: List<String>,
|
||||||
|
option: CmdSubCommand,
|
||||||
|
ignoreCase: Boolean,
|
||||||
|
): 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.handle(ignoreCase)
|
||||||
|
|
||||||
|
val param = option.params[key] ?: throw ArgumentParserException.unknownOption(key)
|
||||||
|
|
||||||
|
val (value, consumedExtra) = resolveParameterValue(args, index, key, inlineValue, param)
|
||||||
|
param.type.convertAndSet(value, option.obj, param.prop)
|
||||||
|
|
||||||
|
index += 1 + consumedExtra
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveParameterValue(
|
||||||
|
args: List<String>,
|
||||||
|
currentIndex: Int,
|
||||||
|
key: String,
|
||||||
|
inlineValue: String,
|
||||||
|
param: CmdParameter,
|
||||||
|
): 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 String.handle(ignoreCase: Boolean): String {
|
||||||
|
return if (ignoreCase) lowercase() else this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchSubCommand(cmd: String) =
|
||||||
|
NAME_PATTERN.matches(cmd)
|
||||||
|
|
||||||
|
private fun matchArgument(arg: String): Pair<String, String>? =
|
||||||
|
ARGUMENT_PATTERN.matchEntire(arg)?.groupValues?.let { Pair(it[1] + it[2], it[3]) }
|
||||||
|
|
||||||
|
// show help
|
||||||
|
|
||||||
|
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,
|
||||||
|
allParameters: CmdSubCommandMap,
|
||||||
|
programDescription: String,
|
||||||
|
): Nothing {
|
||||||
|
showParameters(allParameters)
|
||||||
|
|
||||||
|
e.message?.let {
|
||||||
|
println()
|
||||||
|
println(ArgumentParserException.ERROR_MSG.format(it))
|
||||||
|
println()
|
||||||
|
}
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showParameters(parameters: CmdSubCommandMap) {
|
||||||
|
parameters.forEach { (name, param) ->
|
||||||
|
if (name == param.option) {
|
||||||
|
println(" - ${name} -> Option = ${param.option}, desc = ${param.description}")
|
||||||
|
param.params.forEach { (pName, pParam) ->
|
||||||
|
if (pName == pParam.long) {
|
||||||
|
println(" * ${pName} -> Param = ${pParam.long}, short = ${pParam.short}, desc = ${pParam.description}, mandatory = ${pParam.isMandatory}, type = ${pParam.type}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showVersion(): Nothing {
|
||||||
|
// TODO woher Version?
|
||||||
|
println("v1.0.0")
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
package net.keinekekse.args
|
package net.keinekekse.args.v1
|
||||||
|
|
||||||
import net.keinekekse.args.AbstractCommandLineParser.Companion.NAME_PATTERN
|
import net.keinekekse.args.v1.AbstractCommandLineParser.Companion.NAME_PATTERN
|
||||||
import net.keinekekse.args.AbstractCommandLineParser.Companion.SHORT_NAME_PATTERN
|
import net.keinekekse.args.v1.AbstractCommandLineParser.Companion.SHORT_NAME_PATTERN
|
||||||
import net.keinekekse.util.ValueDelegate
|
import net.keinekekse.util.ValueDelegate
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.keinekekse.config
|
package net.keinekekse.config
|
||||||
|
|
||||||
|
import net.keinekekse.util.VERSION
|
||||||
import net.keinekekse.version.Version
|
import net.keinekekse.version.Version
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -10,15 +11,16 @@ import java.io.File
|
|||||||
* @property configFileName Name of the configuration file to use
|
* @property configFileName Name of the configuration file to use
|
||||||
*/
|
*/
|
||||||
abstract class AbstractInitConfig(
|
abstract class AbstractInitConfig(
|
||||||
|
val applicationVersion: Version,
|
||||||
val configFileName: String = DEFAULT_CONFIG_FILENAME,
|
val configFileName: String = DEFAULT_CONFIG_FILENAME,
|
||||||
) {
|
) {
|
||||||
// abstract fun start()
|
// abstract fun start()
|
||||||
|
|
||||||
/** Current application version */
|
/** Current application version */
|
||||||
val applicationVersion: Version by lazy { Version(this::class.java) }
|
// val applicationVersion: Version by lazy { Version(this::class.java) }
|
||||||
|
|
||||||
/** Library version information */
|
/** Library version information */
|
||||||
val libraryVersion: Version by lazy { Version(Version::class.java) }
|
val libraryVersion: Version = VERSION //by lazy { Version(Version::class.java) }
|
||||||
|
|
||||||
/** Configuration directory path */
|
/** Configuration directory path */
|
||||||
val configDirectory: String by lazy {
|
val configDirectory: String by lazy {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package net.keinekekse.util
|
||||||
|
|
||||||
|
import net.keinekekse.version.Version
|
||||||
|
|
||||||
|
object VERSION: Version {
|
||||||
|
override val version = "0.2.8"
|
||||||
|
override val fullVersion = "0.2.8+build.106"
|
||||||
|
override val major = 0
|
||||||
|
override val minor = 2
|
||||||
|
override val patch = 8
|
||||||
|
override val preRelease = ""
|
||||||
|
override val buildMetadata = "build.106"
|
||||||
|
override val buildDate = "2026-01-18T01:02:24.139170Z"
|
||||||
|
override val displayName = "Utility Library"
|
||||||
|
override val copyright = "Copyright (c) 2025-2026 Hartmut Bauer"
|
||||||
|
override val name = "util"
|
||||||
|
override val description = "Kotlin Utility Library"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package net.keinekekse.version
|
||||||
|
|
||||||
|
interface Version {
|
||||||
|
val version: String
|
||||||
|
val fullVersion: String
|
||||||
|
val major: Int
|
||||||
|
val minor: Int
|
||||||
|
val patch: Int
|
||||||
|
val preRelease: String
|
||||||
|
val buildMetadata: String
|
||||||
|
val buildDate: String
|
||||||
|
val displayName: String
|
||||||
|
val copyright: String
|
||||||
|
val name: String
|
||||||
|
val description: String
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package net.keinekekse.version
|
package net.keinekekse.version.manifest
|
||||||
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
Reference in new issue
Block a user