Compare commits
5
Commits
0.0.29
...
3ec788db13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ec788db13 | ||
|
|
22e1e04673 | ||
|
|
d2948e500c | ||
|
|
8aa418c0f1 | ||
|
|
907e8c7770 |
No files matched your search
Generated
+1
@@ -5,6 +5,7 @@
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
</profile>
|
||||
</component>
|
||||
Generated
+4
-3
@@ -7,10 +7,11 @@
|
||||
<option name="jvmTarget" value="21" />
|
||||
</component>
|
||||
<component name="KotlinCommonCompilerArguments">
|
||||
<option name="apiVersion" value="2.1" />
|
||||
<option name="languageVersion" value="2.1" />
|
||||
<option name="apiVersion" value="2.3" />
|
||||
<option name="languageVersion" value="2.3" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.1.10" />
|
||||
<option name="externalSystemId" value="Gradle" />
|
||||
<option name="version" value="2.3.0" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+1
-19
@@ -4,23 +4,5 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="21" project-jdk-type="JavaSDK" />
|
||||
<component name="ScriptDefinitionSettings">
|
||||
<option name="settings">
|
||||
<list>
|
||||
<ScriptDefinitionSetting>
|
||||
<option name="definitionId" value="org.jetbrains.kotlin.mainKts.MainKtsScript" />
|
||||
</ScriptDefinitionSetting>
|
||||
<ScriptDefinitionSetting>
|
||||
<option name="definitionId" value="kotlin.Any" />
|
||||
</ScriptDefinitionSetting>
|
||||
<ScriptDefinitionSetting>
|
||||
<option name="definitionId" value="kotlin.Any" />
|
||||
</ScriptDefinitionSetting>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="TaskProjectConfiguration">
|
||||
<server type="Redmine" url="https://redmine.keinekekse.net" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
+18
-2
@@ -1,3 +1,7 @@
|
||||
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version masterLibs.versions.kotlinVersion
|
||||
`java-library`
|
||||
@@ -10,13 +14,18 @@ group = "net.keinekekse"
|
||||
version = masterLibs.versions.utilLibraryVersion.get()
|
||||
description = "Kotlin Utility Library"
|
||||
extra["displayname"] = "Utility Library"
|
||||
extra["copyright"] = "Copyright (c) 2025 Hartmut Bauer"
|
||||
extra["copyright"] = "Copyright (c) 2025-2026 Hartmut Bauer"
|
||||
|
||||
repositories {
|
||||
maven("https://nexus.keinekekse.net/repository/maven-public/")
|
||||
}
|
||||
|
||||
dependencies {}
|
||||
dependencies {
|
||||
implementation(masterLibs.kotlinxSerialization)
|
||||
// f. cmd line via annotation KMP?
|
||||
implementation(kotlin("reflect"))
|
||||
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
@@ -25,6 +34,13 @@ java {
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(masterLibs.versions.javaVersion.get().toInt())
|
||||
// compilerOptions {
|
||||
// languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3)
|
||||
// }
|
||||
}
|
||||
|
||||
/* Generierte Dokka-Doku als javadoc.jar ausgeben (s.a. "publishing")
|
||||
tasks.register<Jar>("dokkaJar") {
|
||||
group = "build"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#auto generated
|
||||
#Sun Mar 09 18:10:28 CET 2025
|
||||
build.number=66
|
||||
#Sun Jan 18 02:02:24 CET 2026
|
||||
build.number=106
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../gradle/version-catalog/gradle/libs.versions.toml
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -172,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -205,15 +203,14 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+1
-2
@@ -70,11 +70,10 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
+3
-2
@@ -7,7 +7,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
// Apply the foojay-resolver plugin to allow automatic download of JDKs
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
}
|
||||
|
||||
rootProject.name = "util"
|
||||
@@ -18,7 +18,8 @@ dependencyResolutionManagement {
|
||||
}
|
||||
versionCatalogs {
|
||||
create("masterLibs") {
|
||||
from("net.keinekekse:version-catalog:55")
|
||||
// from(files("gradle/masterlibs.versions.toml"))
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
package net.keinekekse.args.v1
|
||||
|
||||
import net.keinekekse.args.v1.AbstractCommandLineParser.Companion.NAME_PATTERN
|
||||
import net.keinekekse.args.v1.AbstractCommandLineParser.Companion.SHORT_NAME_PATTERN
|
||||
import net.keinekekse.util.ValueDelegate
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class ArgumentParserException(message: String) : RuntimeException(message) {
|
||||
companion object {
|
||||
const val ERROR_MSG = "\n*** ERROR parsing arguments: %s\n"
|
||||
fun optionExists(name: String) = ArgumentParserException("Option '$name' already exists.")
|
||||
fun commandExists(name: String) =
|
||||
ArgumentParserException("Command or alias '$name' already exists.")
|
||||
|
||||
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 optionRequired(name: String) = ArgumentParserException("Option '$name' is required.")
|
||||
fun commandRequired() = ArgumentParserException("At least on command is required.")
|
||||
fun invalidLongValue(value: String) =
|
||||
ArgumentParserException("Value '$value' is not a long integer.")
|
||||
|
||||
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("Options can't be added after parsing.")
|
||||
fun noMoreParsing() = ArgumentParserException("Arguments have already been parsed.")
|
||||
}
|
||||
}
|
||||
|
||||
class ParameterValue : ValueDelegate<List<String>> {
|
||||
override var value: List<String>? = null
|
||||
}
|
||||
|
||||
class OptionValue<T> : ValueDelegate<T> {
|
||||
override var value: T? = null
|
||||
fun isEmpty() = value == null
|
||||
}
|
||||
|
||||
data class OptionConfig(
|
||||
val name: String,
|
||||
val shortName: Char? = null,
|
||||
val description: String,
|
||||
val required: Boolean = false,
|
||||
)
|
||||
|
||||
interface OptionConverter<T> {
|
||||
fun convert(value: String): T
|
||||
}
|
||||
|
||||
sealed class OptionType<T>(
|
||||
val converter: OptionConverter<T>,
|
||||
val hasParameter: Boolean = true,
|
||||
) {
|
||||
companion object {
|
||||
private class StringConverter : OptionConverter<String> {
|
||||
override fun convert(value: String) = value
|
||||
}
|
||||
|
||||
private class LongConverter : OptionConverter<Long> {
|
||||
override fun convert(value: String) = value.toLongOrNull()
|
||||
?: throw ArgumentParserException.invalidLongValue(value)
|
||||
}
|
||||
|
||||
private class BooleanConverter : OptionConverter<Boolean> {
|
||||
override fun convert(value: String) = value == "true"
|
||||
}
|
||||
}
|
||||
|
||||
open fun createOption(config: OptionConfig): Option<T> = Option(config, this)
|
||||
fun convertAndSet(value: String, opt: Option<T>) {
|
||||
opt.setValue(converter.convert(value))
|
||||
}
|
||||
|
||||
object STRING : OptionType<String>(StringConverter())
|
||||
object LONG : OptionType<Long>(LongConverter())
|
||||
object BOOLEAN : OptionType<Boolean>(BooleanConverter(), hasParameter = false) {
|
||||
override fun createOption(config: OptionConfig): Option<Boolean> {
|
||||
return super.createOption(config).apply { setValue(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Option<T>(
|
||||
val config: OptionConfig,
|
||||
val type: OptionType<T>,
|
||||
) {
|
||||
val optionValue = OptionValue<T>()
|
||||
|
||||
fun setValue(value: T) {
|
||||
optionValue.value = value
|
||||
}
|
||||
|
||||
fun isEmpty() = optionValue.isEmpty()
|
||||
}
|
||||
|
||||
interface AddOptions {
|
||||
val options: List<Option<Any>>
|
||||
fun stringOption(config: OptionConfig): OptionValue<String>
|
||||
fun longOption(config: OptionConfig): OptionValue<Long>
|
||||
fun boolOption(config: OptionConfig): OptionValue<Boolean>
|
||||
fun <T> createOption(config: OptionConfig, type: OptionType<T>): OptionValue<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a command in a command-line interface that can have options and aliases.
|
||||
*
|
||||
* A `Command` is identified by its name and can optionally have a description and alternative names (aliases).
|
||||
* It also supports adding and managing options that customize its behavior.
|
||||
*
|
||||
* @property name The unique name of the command.
|
||||
* @property description A brief description explaining the purpose of the command.
|
||||
* @property aliases A list of alternative names (aliases) that can also invoke the command.
|
||||
*
|
||||
* @constructor Creates a new `Command` instance with the specified name, description, and optional aliases.
|
||||
*/
|
||||
class Command(
|
||||
val name: String,
|
||||
val description: String,
|
||||
val aliases: List<String> = emptyList<String>(),
|
||||
) : AddOptions {
|
||||
override val options = mutableListOf<Option<Any>>()
|
||||
var isSet = false
|
||||
internal set
|
||||
|
||||
/**
|
||||
* Adds a string-based option to the command configuration.
|
||||
*
|
||||
* This method creates a command-line option that accepts a string value, based on the provided
|
||||
* configuration. The configuration includes details such as the option's name, description,
|
||||
* and whether it is required.
|
||||
*
|
||||
* @param config the configuration object containing details about the string option to be created
|
||||
* @return an `OptionValue` instance representing the value of the string option
|
||||
*/
|
||||
override fun stringOption(config: OptionConfig): OptionValue<String> =
|
||||
createOption(config, OptionType.STRING)
|
||||
|
||||
/**
|
||||
* Creates a command-line option that accepts a `Long` value based on the provided configuration.
|
||||
*
|
||||
* This method utilizes the specified configuration to define details such as the option's name,
|
||||
* description, and whether it is required. It then generates an option that expects a `Long` value.
|
||||
*
|
||||
* @param config the configuration object containing details about the long option to be created
|
||||
* @return an `OptionValue<Long>` instance representing the value of the long option
|
||||
*/
|
||||
override fun longOption(config: OptionConfig): OptionValue<Long> =
|
||||
createOption(config, OptionType.LONG)
|
||||
|
||||
/**
|
||||
* Creates a command-line option that accepts a Boolean value based on the provided configuration.
|
||||
*
|
||||
* This method uses the specified configuration to define details such as the option's name,
|
||||
* description, and whether it is required. The resulting option accepts a Boolean value, with
|
||||
* `false` being the default if the option is not explicitly provided in the command-line arguments.
|
||||
*
|
||||
* @param config the configuration object containing details about the Boolean option to be created
|
||||
* @return an `OptionValue<Boolean>` instance representing the value of the Boolean option
|
||||
*/
|
||||
override fun boolOption(config: OptionConfig): OptionValue<Boolean> =
|
||||
createOption(
|
||||
config.copy(required = false),
|
||||
OptionType.BOOLEAN
|
||||
)
|
||||
|
||||
override fun <T> createOption(
|
||||
config: OptionConfig,
|
||||
type: OptionType<T>,
|
||||
): OptionValue<T> {
|
||||
DefaultOptionValidator.validateOption(config, options)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return type.createOption(config).also { options.add(it as Option<Any>) }.optionValue
|
||||
}
|
||||
|
||||
override operator fun equals(other: Any?): Boolean {
|
||||
if (other is String) {
|
||||
return name == other || aliases.contains(other)
|
||||
}
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = isSet.hashCode()
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + description.hashCode()
|
||||
result = 31 * result + aliases.hashCode()
|
||||
result = 31 * result + options.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CommandList(commands: MutableList<Command> = mutableListOf<Command>()) :
|
||||
MutableList<Command> by commands {
|
||||
operator fun contains(name: String) = this.any { it.equals(name) }
|
||||
operator fun get(name: String) =
|
||||
this.find { it.equals(name) } ?: throw ArgumentParserException.unknownCommand(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and handles command-line arguments for commands and their associated options.
|
||||
*
|
||||
* The `CommandCommandLineParser` is a specialized parser designed to handle top-level commands,
|
||||
* each potentially with its own set of options and arguments. It extends `AbstractCommandLineParser`
|
||||
* to incorporate functionality for parsing commands alongside their arguments and options.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* val arguments = CommandCommandLineParser("some info text")
|
||||
*
|
||||
* val cmdImport = arguments.addCommand("import", "description", listOf("imp", "in"))
|
||||
* val auto by cmdImport.boolOption(OptionConfig("auto",'a',"description",false))
|
||||
*
|
||||
* // more option & commands ...
|
||||
*
|
||||
* val files by arguments.parametersList()
|
||||
* arguments.parse(args)
|
||||
*
|
||||
* when {
|
||||
* cmdImport.isSet -> {
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* @constructor accepts a help string that is used to display usage instructions, adding context
|
||||
* for the users of the command-line tool.
|
||||
* @param helpString A message string to display as guidance for the tool usage.
|
||||
*/
|
||||
class CommandCommandLineParser(private val helpString: String) :
|
||||
AbstractCommandLineParser(helpString) {
|
||||
private val commands = CommandList()
|
||||
|
||||
/**
|
||||
* Holds the currently active command after the command-line arguments are parsed.
|
||||
*
|
||||
* This variable is updated when a valid command is identified and processed by the parser.
|
||||
* If no valid command is found during parsing, it remains null.
|
||||
* This property is read-only externally and can only be set internally by the parser.
|
||||
*/
|
||||
var command: Command? = null
|
||||
private set
|
||||
|
||||
override val helpFormatter = CommandHelpFormatter(commands, helpString)
|
||||
|
||||
init {
|
||||
addCommand("help", "Display this help message.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new command with the specified name, description, and optional aliases.
|
||||
*
|
||||
* This method registers a command to the parser, allowing it to be identified by its name or aliases.
|
||||
* Throws an exception if commands cannot be added or if the name or aliases conflict with existing commands.
|
||||
*
|
||||
* @param name The unique name of the command to be added.
|
||||
* @param description A brief description of the command.
|
||||
* @param aliases An optional list of alternative names (aliases) for the command. Defaults to an empty list.
|
||||
* @return A `Command` instance that can be used to add option for this command.
|
||||
*/
|
||||
fun addCommand(name: String, description: String, aliases: List<String> = emptyList()) =
|
||||
try {
|
||||
if (isParsed) throw ArgumentParserException.noMoreOptions()
|
||||
if (commands.contains(name) || aliases.any { commands.contains(it) })
|
||||
throw ArgumentParserException.commandExists(name)
|
||||
Command(name, description, aliases).also { commands.add(it) }
|
||||
} catch (e: ArgumentParserException) {
|
||||
handleParsingError(e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an array of command-line arguments and validates the command and its options.
|
||||
*
|
||||
* The method processes the first argument as the command and checks whether it is valid.
|
||||
* If the command is recognized and exists within the predefined set of allowed commands,
|
||||
* its associated options are then parsed. If an error occurs during parsing, appropriate
|
||||
* error handling is invoked.
|
||||
*
|
||||
* @param args The array of command-line arguments to be parsed. The first element is treated
|
||||
* as the primary command, and the rest are interpreted as its options.
|
||||
*/
|
||||
override fun parse(args: Array<String>) {
|
||||
if (args.isEmpty()) {
|
||||
handleParsingError(ArgumentParserException.commandRequired())
|
||||
}
|
||||
val cmd = args[0]
|
||||
if (!argumentParser.parseCommand(cmd)) {
|
||||
handleParsingError(ArgumentParserException.invalidCommand(cmd))
|
||||
}
|
||||
if (cmd !in commands) {
|
||||
handleParsingError(ArgumentParserException.unknownCommand(cmd))
|
||||
} else {
|
||||
parseArguments(args.drop(1).toTypedArray(), commands[cmd].options)
|
||||
commands[cmd].isSet = true
|
||||
command = commands[cmd]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A command-line parser specifically designed to parse and manage options provided
|
||||
* via command-line arguments. It extends the `AbstractCommandLineParser` and implements
|
||||
* the `AddOptions` interface.
|
||||
*
|
||||
* This parser supports different types of options including string, long, and boolean,
|
||||
* and ensures validation during the parsing process. It also auto-registers a help option
|
||||
* to display the usage of the parser.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* val arguments = OptionCommandLineParser("some info text")
|
||||
* val author by arguments.stringOption(OptionConfig("author", 'a', "some author", true))
|
||||
*
|
||||
* // more options ...
|
||||
*
|
||||
* val files by arguments.parametersList()
|
||||
* arguments.parse(args)
|
||||
*
|
||||
* if (author != null) {
|
||||
*```
|
||||
*
|
||||
* @constructor Creates an `OptionCommandLineParser` with a custom help string.
|
||||
* @param helpString Descriptive text that is displayed when the help option is invoked.
|
||||
*/
|
||||
class OptionCommandLineParser(private val helpString: String) :
|
||||
AbstractCommandLineParser(helpString), AddOptions {
|
||||
override val options = mutableListOf<Option<Any>>()
|
||||
|
||||
override val helpFormatter = OptionHelpFormatter(options, helpString)
|
||||
|
||||
init {
|
||||
boolOption(OptionConfig("help", 'h', "Display this help message"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and registers a string-based command-line option based on the provided configuration.
|
||||
*
|
||||
* @param config The configuration object containing the name, short name, description,
|
||||
* and requirements for the string option.
|
||||
* @return An instance of OptionValue that delegates access to the parsed value of the option.
|
||||
*/
|
||||
override fun stringOption(config: OptionConfig): OptionValue<String> =
|
||||
createOption(config, OptionType.STRING)
|
||||
|
||||
/**
|
||||
* Creates and registers a command-line option that parses and returns a `Long` value.
|
||||
*
|
||||
* @param config The configuration object containing the name, short name, description,
|
||||
* and requirements for the long option.
|
||||
* @return An instance of `OptionValue<Long>` that provides access to the parsed value
|
||||
* of the long option.
|
||||
*/
|
||||
override fun longOption(config: OptionConfig): OptionValue<Long> =
|
||||
createOption(config, OptionType.LONG)
|
||||
|
||||
/**
|
||||
* Creates and registers a command-line option that parses and returns a `Boolean` value.
|
||||
*
|
||||
* @param config The configuration object containing the name, short name, description,
|
||||
* and requirements for the boolean option.
|
||||
* @return An instance of `OptionValue<Boolean>` that provides access to the parsed value
|
||||
* of the boolean option.
|
||||
*/
|
||||
override fun boolOption(config: OptionConfig): OptionValue<Boolean> =
|
||||
createOption(
|
||||
config.copy(required = false),
|
||||
OptionType.BOOLEAN
|
||||
)
|
||||
|
||||
override fun <T> createOption(
|
||||
config: OptionConfig,
|
||||
type: OptionType<T>,
|
||||
): OptionValue<T> {
|
||||
try {
|
||||
if (isParsed) throw ArgumentParserException.noMoreOptions()
|
||||
DefaultOptionValidator.validateOption(config, options)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return type.createOption(config).also { options.add(it as Option<Any>) }.optionValue
|
||||
} catch (e: ArgumentParserException) {
|
||||
handleParsingError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided array of command-line arguments.
|
||||
*
|
||||
* This method processes the input arguments, identifies the options and parameters
|
||||
* based on the argument parser's configuration, and validates the required options.
|
||||
* If the arguments have already been parsed, it raises an exception.
|
||||
* In case of errors during parsing or validation, an error message is displayed
|
||||
* and the help information is shown.
|
||||
*
|
||||
* @param args The array of command-line arguments to be parsed.
|
||||
*/
|
||||
override fun parse(args: Array<String>) {
|
||||
parseArguments(args, options)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class AbstractCommandLineParser(private val helpString: String) {
|
||||
|
||||
private val parameters = ParameterValue()
|
||||
protected var isParsed = false
|
||||
protected val argumentParser: ArgumentParser = DefaultArgumentParser()
|
||||
protected abstract val helpFormatter: HelpFormatter
|
||||
|
||||
companion object {
|
||||
const val NAME = """[a-zA-Z][\w-]+"""
|
||||
const val SHORT_NAME = """[a-zA-Z]"""
|
||||
val ARGUMENT_PATTERN = """^(?:-(${SHORT_NAME})|--(${NAME}))(?:=(.+)|$)$""".toRegex()
|
||||
val NAME_PATTERN = """^${NAME}$""".toRegex()
|
||||
val SHORT_NAME_PATTERN = """^${SHORT_NAME}$""".toRegex()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of parameters provided to the argument parser.
|
||||
*
|
||||
* The parameters are parsed from the command-line input following the provided argument options.
|
||||
* This method allows accessing any additional parameters that are not directly associated with
|
||||
* the defined options.
|
||||
*
|
||||
* @return A list of strings representing the positional parameters passed after all options.
|
||||
*/
|
||||
fun parametersList() = parameters
|
||||
|
||||
abstract fun parse(args: Array<String>)
|
||||
|
||||
protected fun parseArguments(args: Array<String>, options: MutableList<Option<Any>>) {
|
||||
if (isParsed) throw ArgumentParserException.noMoreParsing()
|
||||
try {
|
||||
argumentParser.parseArguments(args, options, parameters)
|
||||
isParsed = true
|
||||
DefaultOptionValidator.validateRequiredOptions(options)
|
||||
handleHelpOption(options)
|
||||
} catch (e: ArgumentParserException) {
|
||||
handleParsingError(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun showHelp(errorMessage: String = ""): Nothing {
|
||||
helpFormatter.show()
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
println()
|
||||
println(errorMessage)
|
||||
println()
|
||||
}
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun handleHelpOption(options: MutableList<Option<Any>>) {
|
||||
if (findOption("help", options)?.optionValue?.value == true) {
|
||||
showHelp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findOption(name: String, options: MutableList<Option<Any>>): Option<Any>? =
|
||||
if (name.length > 1) options.find { it.config.name == name }
|
||||
else options.find { it.config.shortName == name[0] }
|
||||
|
||||
protected fun handleParsingError(e: ArgumentParserException): Nothing {
|
||||
showHelp(ArgumentParserException.ERROR_MSG.format(e.message))
|
||||
}
|
||||
|
||||
// argument parsing
|
||||
|
||||
interface ArgumentParser {
|
||||
fun parseArguments(
|
||||
args: Array<String>,
|
||||
options: List<Option<Any>>,
|
||||
parameters: ParameterValue,
|
||||
)
|
||||
|
||||
fun parseCommand(cmd: String): Boolean
|
||||
}
|
||||
|
||||
private class DefaultArgumentParser : ArgumentParser {
|
||||
override fun parseArguments(
|
||||
args: Array<String>,
|
||||
options: List<Option<Any>>,
|
||||
parameters: ParameterValue,
|
||||
) {
|
||||
var index = 0
|
||||
parameters.value = emptyList()
|
||||
while (index < args.size) {
|
||||
val parseResult = parseArgument(args[index])
|
||||
if (parseResult != null) {
|
||||
val (key, value) = parseResult
|
||||
index += handleOption(key, value, args, index, options)
|
||||
index++
|
||||
} else {
|
||||
parameters.value = args.drop(index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseCommand(cmd: String) =
|
||||
NAME_PATTERN.matches(cmd)
|
||||
|
||||
private fun parseArgument(arg: String): Pair<String, String>? =
|
||||
ARGUMENT_PATTERN.matchEntire(arg)?.groupValues?.let { Pair(it[1] + it[2], it[3]) }
|
||||
|
||||
private fun handleOption(
|
||||
key: String,
|
||||
value: String,
|
||||
args: Array<String>,
|
||||
index: Int,
|
||||
options: List<Option<Any>>,
|
||||
): Int {
|
||||
val option = options.find {
|
||||
it.config.name == key || it.config.shortName?.toString() == key
|
||||
} ?: throw ArgumentParserException.unknownOption(key)
|
||||
|
||||
return when {
|
||||
option.type.hasParameter -> handleParameterOption(option, value, args, index)
|
||||
else -> handleFlagOption(option, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleParameterOption(
|
||||
option: Option<Any>,
|
||||
value: String,
|
||||
args: Array<String>,
|
||||
index: Int,
|
||||
): Int = when {
|
||||
value.isNotBlank() -> {
|
||||
option.type.convertAndSet(value, option)
|
||||
0
|
||||
}
|
||||
|
||||
index < args.size - 1 && !ARGUMENT_PATTERN.matches(args[index + 1]) -> {
|
||||
option.type.convertAndSet(args[index + 1], option)
|
||||
1
|
||||
}
|
||||
|
||||
else -> throw ArgumentParserException.optionRequiresValue(option.config.name)
|
||||
}
|
||||
|
||||
private fun handleFlagOption(option: Option<Any>, value: String): Int {
|
||||
if (value.isNotBlank()) {
|
||||
throw ArgumentParserException.optionNoValue(option.config.name)
|
||||
}
|
||||
option.type.convertAndSet("true", option)
|
||||
return 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// help output
|
||||
|
||||
protected interface HelpFormatter {
|
||||
fun show(indent: String = "")
|
||||
}
|
||||
|
||||
protected class CommandHelpFormatter(
|
||||
protected val commands: CommandList,
|
||||
private val infoMessage: String,
|
||||
) : HelpFormatter {
|
||||
override fun show(indent: String) {
|
||||
println()
|
||||
println(infoMessage)
|
||||
for (cmd in commands) {
|
||||
val aliases = buildString {
|
||||
if (cmd.aliases.isNotEmpty()) append(
|
||||
"|'${
|
||||
cmd.aliases.joinToString("'|'")
|
||||
}'"
|
||||
)
|
||||
}
|
||||
OptionHelpFormatter(
|
||||
cmd.options,
|
||||
"Command '${cmd.name}'${aliases}: ${cmd.description}"
|
||||
).show(" ")
|
||||
println()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class OptionHelpFormatter(
|
||||
private val options: List<Option<Any>>,
|
||||
private val infoMessage: String,
|
||||
) : HelpFormatter {
|
||||
override fun show(indent: String) {
|
||||
println(infoMessage)
|
||||
if (options.isNotEmpty()) {
|
||||
if (indent.isEmpty()) println("Options:")
|
||||
printOptions(indent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun printOptions(indent: String) {
|
||||
val maxNameLength = calculateMaxNameLength() + 4 + indent.length
|
||||
options.forEach { printOption(it, maxNameLength, indent) }
|
||||
}
|
||||
|
||||
private fun calculateMaxNameLength(): Int =
|
||||
options.maxOfOrNull { opt ->
|
||||
opt.config.name.length + 2 + (opt.config.shortName?.let { 4 } ?: 0)
|
||||
} ?: 0
|
||||
|
||||
private fun printOption(option: Option<Any>, maxNameLength: Int, indent: String) {
|
||||
val shortName = option.config.shortName?.let { "-$it, " } ?: ""
|
||||
val optionText = StringBuilder()
|
||||
.append(indent)
|
||||
.append(shortName)
|
||||
.append("--")
|
||||
.append(option.config.name)
|
||||
.padEnd(maxNameLength)
|
||||
println("$optionText ${option.config.description}")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// option validation
|
||||
|
||||
private interface OptionValidator {
|
||||
fun validateOption(config: OptionConfig, existingOptions: List<Option<Any>>)
|
||||
fun validateRequiredOptions(options: List<Option<Any>>)
|
||||
}
|
||||
|
||||
private object DefaultOptionValidator : OptionValidator {
|
||||
override fun validateOption(config: OptionConfig, existingOptions: List<Option<Any>>) {
|
||||
validateOptionName(config.name)
|
||||
validateShortName(config.shortName)
|
||||
validateOptionUniqueness(config, existingOptions)
|
||||
}
|
||||
|
||||
override fun validateRequiredOptions(options: List<Option<Any>>) {
|
||||
val emptyNames = options.filter { it.config.required && it.isEmpty() }
|
||||
.joinToString(", ") { it.config.name }
|
||||
if (emptyNames.isNotEmpty()) throw ArgumentParserException.optionRequired(emptyNames)
|
||||
}
|
||||
|
||||
private fun validateOptionName(name: String) {
|
||||
if (!name.matches(NAME_PATTERN)) {
|
||||
throw ArgumentParserException.invalidName(name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateShortName(shortName: Char?) {
|
||||
if (shortName != null && !shortName.toString().matches(SHORT_NAME_PATTERN)) {
|
||||
throw ArgumentParserException.invalidShortName(shortName.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateOptionUniqueness(
|
||||
config: OptionConfig,
|
||||
existingOptions: List<Option<Any>>,
|
||||
) {
|
||||
val nameExists = existingOptions.any { it.config.name == config.name }
|
||||
val shortNameExists = config.shortName?.let { shortName ->
|
||||
existingOptions.any { it.config.shortName == shortName }
|
||||
} ?: false
|
||||
|
||||
if (nameExists || shortNameExists) {
|
||||
throw ArgumentParserException.optionExists(config.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.keinekekse.config
|
||||
|
||||
import net.keinekekse.util.VERSION
|
||||
import net.keinekekse.version.Version
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Abstract base class for initialization configuration.
|
||||
* Provides version information and configuration file management.
|
||||
*
|
||||
* @property configFileName Name of the configuration file to use
|
||||
*/
|
||||
abstract class AbstractInitConfig(
|
||||
val applicationVersion: Version,
|
||||
val configFileName: String = DEFAULT_CONFIG_FILENAME,
|
||||
) {
|
||||
// abstract fun start()
|
||||
|
||||
/** Current application version */
|
||||
// val applicationVersion: Version by lazy { Version(this::class.java) }
|
||||
|
||||
/** Library version information */
|
||||
val libraryVersion: Version = VERSION //by lazy { Version(Version::class.java) }
|
||||
|
||||
/** Configuration directory path */
|
||||
val configDirectory: String by lazy {
|
||||
buildPath(
|
||||
System.getProperty(USER_HOME_PROPERTY),
|
||||
CONFIG_DIR,
|
||||
applicationVersion.name
|
||||
)
|
||||
}
|
||||
|
||||
/** Current working directory */
|
||||
val workingDirectory: String by lazy {
|
||||
System.getProperty(USER_DIR_PROPERTY) + File.separator
|
||||
}
|
||||
|
||||
/** User-specific configuration */
|
||||
val userConfig by lazy {
|
||||
UserConfigProperties(buildPath(configDirectory, configFileName))
|
||||
}
|
||||
|
||||
/** System-wide configuration */
|
||||
val systemConfig = SystemConfigProperties
|
||||
|
||||
private fun buildPath(vararg parts: String): String =
|
||||
parts.joinToString(File.separator)
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_CONFIG_FILENAME = "config.properties"
|
||||
private const val CONFIG_DIR = ".config"
|
||||
private const val USER_HOME_PROPERTY = "user.home"
|
||||
private const val USER_DIR_PROPERTY = "user.dir"
|
||||
}
|
||||
}
|
||||
+12
-11
@@ -7,6 +7,7 @@ private const val CONFIG_FILE_NAME = "config.properties"
|
||||
private const val ERROR_LOADING_CONFIG = "Could not load configuration from $CONFIG_FILE_NAME"
|
||||
|
||||
// TODO mit WAR testen wg. Pfad zu config.properties
|
||||
|
||||
/**
|
||||
* Projekt-Properties
|
||||
*
|
||||
@@ -14,27 +15,27 @@ private const val ERROR_LOADING_CONFIG = "Could not load configuration from $CON
|
||||
* (-> in ```main/resources``` anlegen)
|
||||
*
|
||||
* ```
|
||||
* val prop: String = ConfigProperties.get("some.prop")
|
||||
* val prop: String = ConfigProperties["some.prop"] ?: "default"
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
object ConfigProperties {
|
||||
object SystemConfigProperties {
|
||||
private val properties = Properties()
|
||||
operator fun get(key: String): String? = properties.getProperty(key)
|
||||
|
||||
init {
|
||||
loadConfigFile()
|
||||
}
|
||||
|
||||
private fun loadConfigFile() {
|
||||
val configFileStream: InputStream? =
|
||||
javaClass.classLoader.getResourceAsStream(CONFIG_FILE_NAME)
|
||||
if (configFileStream == null) {
|
||||
throw IllegalStateException(ERROR_LOADING_CONFIG)
|
||||
}
|
||||
|
||||
configFileStream.use {
|
||||
properties.load(it)
|
||||
getConfigStream().use { stream ->
|
||||
properties.load(stream)
|
||||
}
|
||||
}
|
||||
|
||||
fun get(key: String): String = properties.getProperty(key)
|
||||
private fun getConfigStream(): InputStream {
|
||||
return javaClass.classLoader.getResourceAsStream(CONFIG_FILE_NAME)
|
||||
?: throw IllegalStateException(ERROR_LOADING_CONFIG)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package net.keinekekse.config
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Manages user-specific configuration properties from a file.
|
||||
* Creates the configuration file if it doesn't exist.
|
||||
*
|
||||
* @property configPath Path to the configuration file
|
||||
* @throws FileNotFoundException if the configuration file cannot be created or accessed
|
||||
*/
|
||||
class UserConfigProperties(private val configPath: String) {
|
||||
private companion object {
|
||||
const val ERROR_CREATE_CONFIG = "Failed to create configuration file"
|
||||
const val ERROR_LOAD_CONFIG = "Failed to load configuration from file"
|
||||
}
|
||||
|
||||
private val properties = Properties()
|
||||
|
||||
init {
|
||||
loadProperties()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value associated with the given key from the properties.
|
||||
*
|
||||
* @param key The property key
|
||||
* @return The property value or null if the key doesn't exist
|
||||
*/
|
||||
operator fun get(key: String): String? = properties.getProperty(key)
|
||||
|
||||
private fun loadProperties() {
|
||||
ensureConfigFileExists()
|
||||
try {
|
||||
File(configPath).inputStream().use { stream ->
|
||||
properties.load(stream)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw FileNotFoundException("$ERROR_LOAD_CONFIG: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureConfigFileExists() {
|
||||
val configFile = File(configPath)
|
||||
if (!configFile.exists()) {
|
||||
try {
|
||||
configFile.parentFile?.mkdirs()
|
||||
if (!configFile.createNewFile()) {
|
||||
throw FileNotFoundException(ERROR_CREATE_CONFIG)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw FileNotFoundException("$ERROR_CREATE_CONFIG: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles JSON file reading and parsing with a relaxed configuration.
|
||||
* Supports comments, case-insensitive enum parsing, and ignores unknown keys.
|
||||
*
|
||||
* ```
|
||||
* val obj: T = JsonConfig.parseFromFile<T>("config.json")
|
||||
* // or
|
||||
* val json = JsonConfig.read("config.json")
|
||||
* val obj: T = JsonConfig.parse<T>(json)
|
||||
* ```
|
||||
*/
|
||||
object JsonConfig {
|
||||
private const val FILE_NOT_FOUND = "File not found: "
|
||||
private const val FILE_READ_ERROR = "Failed to read file: "
|
||||
|
||||
/**
|
||||
* JSON parser configuration optimized for flexibility and ease of use.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val jsonConfiguration = Json {
|
||||
ignoreUnknownKeys = true
|
||||
allowComments = true
|
||||
decodeEnumsCaseInsensitive = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom exception for JSON-related operations
|
||||
*/
|
||||
class JsonConfigException(message: String, cause: Throwable? = null) :
|
||||
RuntimeException(message, cause)
|
||||
|
||||
/**
|
||||
* Reads content from a JSON file.
|
||||
* @param filePath Path to the JSON file
|
||||
* @return String content of the file
|
||||
* @throws JsonConfigException if the file doesn't exist or cannot be read
|
||||
*/
|
||||
fun read(filePath: String): String {
|
||||
val jsonFile = File(filePath)
|
||||
return when {
|
||||
!jsonFile.exists() -> throw JsonConfigException(FILE_NOT_FOUND + filePath)
|
||||
else -> try {
|
||||
jsonFile.readText()
|
||||
} catch (e: Exception) {
|
||||
throw JsonConfigException(FILE_READ_ERROR + filePath, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO exceptions hier fangen?
|
||||
|
||||
JSON Exceptions:
|
||||
|
||||
malformed, falscher Datentyp: JsonDecodingException
|
||||
missing: MissingFieldException
|
||||
unbekannter Name für enum: SerializationException
|
||||
|
||||
wenn ein key mehrmals vorkommt, wird nur das letzte gelesen (kein Fehler!)
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parses a JSON string into the specified type.
|
||||
* @param json JSON string to parse
|
||||
* @return Parsed object of type T
|
||||
*/
|
||||
inline fun <reified T> parse(json: String): T =
|
||||
jsonConfiguration.decodeFromString<T>(json)
|
||||
|
||||
/**
|
||||
* Parses a JSON file located at the given path into an object of the specified type.
|
||||
*
|
||||
* @param T The type of the object to parse.
|
||||
* @param filePath The path to the JSON file to parse.
|
||||
* @return An object of type T parsed from the JSON file.
|
||||
* @throws JsonConfigException If the file doesn't exist, can't be read,
|
||||
* or if there's an error during parsing.
|
||||
*/
|
||||
inline fun <reified T> parseFromFile(filePath: String): T {
|
||||
val json = read(filePath)
|
||||
return parse(json)
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,8 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
class CommandExecutionException(message: String) : Exception(message)
|
||||
|
||||
private const val IO_EXCEPTION_CODE = 0x10000
|
||||
private const val INTERRUPTED_EXCEPTION_CODE = 0x10001
|
||||
private const val GENERIC_EXCEPTION_CODE = 0x10002
|
||||
|
||||
/**
|
||||
* Runs program in list [arguments] and saves result to [stdout], [stderr] and [rc].
|
||||
* Runs program in list [arguments] and outputs results to [stdout], [stderr] and [rc].
|
||||
*
|
||||
* A return code of >=0x10000 in [rc] signals an exception with the stack trace
|
||||
* in [stderr].
|
||||
@@ -20,7 +16,7 @@ private const val GENERIC_EXCEPTION_CODE = 0x10002
|
||||
* val runResult = ExecutionResult(listOf("ebook-meta", "/path/to/epub", "--title", "New Title"))
|
||||
* println("RC = ${runResult.rc}")
|
||||
* ```
|
||||
* @param arguments program and arguments to run
|
||||
* @param arguments program and arguments to run as [List]
|
||||
* @param throwAtError throw exception at `rc != 0` (defaults to true)
|
||||
* @param workingDir working directory for the program (defaults to ".")
|
||||
* @param timeoutSec timeout in seconds (defaults to 60)
|
||||
@@ -41,6 +37,12 @@ class ExecutionResult(
|
||||
val stderr: String
|
||||
val rc: Int
|
||||
|
||||
companion object {
|
||||
private const val IO_EXCEPTION_CODE = 0x10000
|
||||
private const val INTERRUPTED_EXCEPTION_CODE = 0x10001
|
||||
private const val GENERIC_EXCEPTION_CODE = 0x10002
|
||||
}
|
||||
|
||||
init {
|
||||
val executionResult = try {
|
||||
executeProcess()
|
||||
|
||||
@@ -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,41 @@
|
||||
package net.keinekekse.util
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* A delegate interface that provides a mechanism to access a value of type `T` via delegation.
|
||||
*
|
||||
* This interface allows the value to be retrieved indirectly through property delegation while
|
||||
* abstracting the underlying property storage mechanism. It is particularly useful when implementing
|
||||
* custom delegation logic for properties.
|
||||
*
|
||||
* @param T the type of the value to be delegated.
|
||||
*
|
||||
* ```
|
||||
* val myValue: T? by MyValueDelegate()
|
||||
* ```
|
||||
*/
|
||||
interface ValueDelegate<T> {
|
||||
var value: T?
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A mutable delegate interface that extends the capabilities of [ValueDelegate].
|
||||
*
|
||||
* This interface enables both retrieving and setting a value of type `T` through property delegation,
|
||||
* providing a mechanism to modify the delegated property in addition to accessing it.
|
||||
*
|
||||
* @param T the type of the value to be delegated.
|
||||
*
|
||||
* ```
|
||||
* var myValue: T? by MyMutableValueDelegate()
|
||||
* ```
|
||||
*
|
||||
* @see ValueDelegate
|
||||
*/
|
||||
interface MutableValueDelegate<T> : ValueDelegate<T> {
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.keinekekse.util
|
||||
|
||||
/**
|
||||
* Checks if a network connection to the specified host and port is possible within the given timeout.
|
||||
*
|
||||
* @param host the host to connect to
|
||||
* @param port the port to connect to on the host
|
||||
* @param timeoutMs the timeout in milliseconds for the connection attempt, default is 1000ms
|
||||
* @return true if the connection is successful, false otherwise
|
||||
*/
|
||||
fun isConnectionPossible(host: String, port: Int, timeoutMs: Int = 1000): Boolean {
|
||||
return try {
|
||||
java.net.Socket().use { socket ->
|
||||
socket.connect(java.net.InetSocketAddress(host, port), timeoutMs)
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is java.net.ConnectException,
|
||||
is java.net.SocketTimeoutException,
|
||||
is java.net.UnknownHostException,
|
||||
-> false
|
||||
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+58
-48
@@ -1,14 +1,10 @@
|
||||
package net.keinekekse.version
|
||||
package net.keinekekse.version.manifest
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.jar.Manifest
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
@@ -19,9 +15,15 @@ import java.util.regex.Pattern
|
||||
*
|
||||
* Fürs Setzen der Werte in der MANIFEST.MF ist das Gradle version-plugin verantwortlich.
|
||||
*
|
||||
* Das Auslesen der Datei funktioniert für .jar (liegt in /META-INF) und .war (liegt in
|
||||
* /WEB-INF/classes/META-INF) die auf einem Wildfly-Server deployed wurden (für alles Andere wäre es
|
||||
* zu testen)
|
||||
* Das Version-Plugin setzt folgende Werte
|
||||
* ```
|
||||
* Implementation-Title = name;displayName;description
|
||||
* Implementation-Vendor = copyright
|
||||
* Implementation-Version = version;buildNumber;Instant.now()
|
||||
* ```
|
||||
* Dadurch könne die Werte über die Standardfunktionen, wie z.B. [Package.implementationTitle],
|
||||
* geholt werden. Das erübrigt eigene Routinen für das Finden der MANIFEST.MF, auf die per
|
||||
* [Class.getPackage] zugegriffen werden kann.
|
||||
*
|
||||
* @param clazz Klasse aus der jeweiligen .jar oder .war
|
||||
*
|
||||
@@ -31,56 +33,65 @@ import java.util.regex.Pattern
|
||||
* ```
|
||||
*/
|
||||
class Version(clazz: Class<*>) {
|
||||
|
||||
val versionObject: VersionNumber
|
||||
private val attributes: MutableMap<String, String>
|
||||
|
||||
init {
|
||||
val manifestPath = determineManifestPath(clazz)
|
||||
attributes = loadManifestAttributes(manifestPath)
|
||||
val manifest = determineManifestPath(clazz)
|
||||
attributes = loadManifestAttributes(manifest)
|
||||
versionObject = initializeVersionNumbers()
|
||||
}
|
||||
|
||||
// TODO noch nicht für WAR getestet!!
|
||||
private fun determineManifestPath(clazz: Class<*>): String {
|
||||
val classResourcePath = clazz.getResource("${clazz.simpleName}.class")?.toString() ?: ""
|
||||
val baseFolder = if (classResourcePath.contains(WEB_INF_PATH)) {
|
||||
classResourcePath.substringBefore(WEB_INF_PATH) + MANIFEST_FILE_PATH
|
||||
} else {
|
||||
classResourcePath.substringBefore(
|
||||
clazz.name.replace(
|
||||
'.',
|
||||
File.separatorChar
|
||||
) + CLASS_EXTENSION
|
||||
) + MANIFEST_FILE_PATH
|
||||
}
|
||||
return baseFolder
|
||||
private fun determineManifestPath(clazz: Class<*>): Package? {
|
||||
return clazz.`package`
|
||||
|
||||
// val classResourcePath = clazz.getResource("${clazz.simpleName}.class")?.toString() ?: ""
|
||||
// val baseFolder = if (classResourcePath.contains(WEB_INF_PATH)) {
|
||||
// classResourcePath.substringBefore(WEB_INF_PATH) + MANIFEST_FILE_PATH
|
||||
// } else {
|
||||
// classResourcePath.substringBefore(
|
||||
// clazz.name.replace(
|
||||
// '.',
|
||||
// File.separatorChar
|
||||
// ) + CLASS_EXTENSION
|
||||
// ) + MANIFEST_FILE_PATH
|
||||
// }
|
||||
// return baseFolder
|
||||
}
|
||||
|
||||
private fun loadManifestAttributes(manifestPath: String): MutableMap<String, String> {
|
||||
private fun loadManifestAttributes(manifest: Package?): MutableMap<String, String> {
|
||||
val attributes = ConcurrentHashMap<String, String>()
|
||||
val implTitle = (manifest?.implementationTitle ?: "").split(";")
|
||||
attributes.put(PROP_NAME, implTitle.getOrNull(0) ?: "")
|
||||
attributes.put(PROP_DISPLAYNAME, implTitle.getOrNull(1) ?: "")
|
||||
attributes.put(PROP_DESCRIPTION, implTitle.getOrNull(2) ?: "")
|
||||
|
||||
try {
|
||||
URI(manifestPath).toURL().openStream().use {
|
||||
val manifest = Manifest(it)
|
||||
manifest.mainAttributes.forEach { key, value ->
|
||||
attributes[key.toString()] = value.toString()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException("Failed to read MANIFEST.MF at $manifestPath", e)
|
||||
}
|
||||
val version = (manifest?.implementationVersion ?: "").split(";")
|
||||
attributes.put(PROP_VERSION, version.getOrNull(0) ?: "")
|
||||
attributes.put(PROP_BUILD_NUMBER, version.getOrNull(1) ?: "")
|
||||
attributes.put(PROP_BUILD_TIMESTAMP, version.getOrNull(2) ?: "")
|
||||
|
||||
attributes.put(PROP_COPYRIGHT, manifest?.implementationVendor ?: "")
|
||||
// try {
|
||||
// URI(manifestPath).toURL().openStream().use {
|
||||
// val manifest = Manifest(it)
|
||||
// manifest.mainAttributes.forEach { key, value ->
|
||||
// attributes[key.toString()] = value.toString()
|
||||
// }
|
||||
// }
|
||||
// } catch (e: IOException) {
|
||||
// throw RuntimeException("Failed to read MANIFEST.MF at $manifestPath", e)
|
||||
// }
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
private fun initializeVersionNumbers(): VersionNumber {
|
||||
return VersionNumber(
|
||||
private fun initializeVersionNumbers(): VersionNumber =
|
||||
VersionNumber(
|
||||
attributes[PROP_VERSION] ?: "",
|
||||
attributes[PROP_BUILD_TIMESTAMP] ?: "",
|
||||
attributes[PROP_BUILD_NUMBER] ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
val infoString: String
|
||||
get() = "$displayname $fullVersion $copyright"
|
||||
@@ -122,7 +133,7 @@ class Version(clazz: Class<*>) {
|
||||
get() = versionObject.buildTimestamp
|
||||
|
||||
companion object {
|
||||
private const val PROP_NAME = "module-name"
|
||||
private const val PROP_NAME = "name"
|
||||
private const val PROP_DISPLAYNAME = "displayname"
|
||||
private const val PROP_COPYRIGHT = "copyright"
|
||||
private const val PROP_DESCRIPTION = "description"
|
||||
@@ -130,9 +141,9 @@ class Version(clazz: Class<*>) {
|
||||
private const val PROP_BUILD_TIMESTAMP = "build-timestamp"
|
||||
private const val PROP_BUILD_NUMBER = "build-number"
|
||||
|
||||
private const val MANIFEST_FILE_PATH = "META-INF/MANIFEST.MF"
|
||||
private const val CLASS_EXTENSION = ".class"
|
||||
private const val WEB_INF_PATH = "WEB-INF/classes"
|
||||
// private const val MANIFEST_FILE_PATH = "META-INF/MANIFEST.MF"
|
||||
// private const val CLASS_EXTENSION = ".class"
|
||||
// private const val WEB_INF_PATH = "WEB-INF/classes"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,11 +212,10 @@ class VersionNumber(val version: String, timestamp: String, buildNumber: String
|
||||
|
||||
private fun parseVersionComponent(value: String?) = value?.toIntOrNull() ?: 0
|
||||
|
||||
private fun resolveBuildMetadata(extractedBuild: String): String {
|
||||
return if (extractedBuild.isNotEmpty()) extractedBuild else buildEpochSeconds.toString()
|
||||
}
|
||||
private fun resolveBuildMetadata(extractedBuild: String): String =
|
||||
if (extractedBuild.isNotEmpty()) extractedBuild else buildEpochSeconds.toString()
|
||||
|
||||
private fun initializeBuildTimestamp(timestamp: String) {
|
||||
private fun initializeBuildTimestamp(timestamp: String) =
|
||||
try {
|
||||
val instant = Instant.parse(timestamp)
|
||||
val date = instant.atZone(TimeZone.getDefault().toZoneId())
|
||||
@@ -214,5 +224,5 @@ class VersionNumber(val version: String, timestamp: String, buildNumber: String
|
||||
} catch (e: DateTimeParseException) {
|
||||
buildTimestamp = "Invalid timestamp"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user