Compare commits

..
16 Commits
60 changed files with 1701 additions and 37 deletions

No files matched your search

+18 -4
View File
@@ -1,5 +1,19 @@
# Ignore Gradle project-specific cache directory
*.iml
.kotlin
.gradle
# Ignore Gradle build output directory
build
**/build/
xcuserdata
!src/**/build/
local.properties
.idea
.DS_Store
captures
.externalNativeBuild
.cxx
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-js-0.2.8">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-js-0.2.8.jar">
<element id="module-output" name="util.jsMain" />
</root>
</artifact>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-js-0.3.0-beta.1">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-js-0.3.0-beta.1.jar" />
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-js-0.3.0-beta.11">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-js-0.3.0-beta.11.jar">
<element id="module-output" name="util.jsMain" />
</root>
</artifact>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-js-0.3.0-beta.19">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-js-0.3.0-beta.19.jar" />
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-js-0.3.0-beta.2">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-js-0.3.0-beta.2.jar">
<element id="module-output" name="util.jsMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-js-0.3.0-beta.4">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-js-0.3.0-beta.4.jar">
<element id="module-output" name="util.jsMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-js-0.3.0-beta.8">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-js-0.3.0-beta.8.jar">
<element id="module-output" name="util.jsMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-jvm-0.2.8">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-jvm-0.2.8.jar">
<element id="module-output" name="util.jvmMain" />
</root>
</artifact>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-jvm-0.3.0-beta.1">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-jvm-0.3.0-beta.1.jar" />
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-jvm-0.3.0-beta.11">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-jvm-0.3.0-beta.11.jar">
<element id="module-output" name="util.jvmMain" />
</root>
</artifact>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-jvm-0.3.0-beta.19">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-jvm-0.3.0-beta.19.jar" />
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-jvm-0.3.0-beta.2">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-jvm-0.3.0-beta.2.jar">
<element id="module-output" name="util.jvmMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-jvm-0.3.0-beta.4">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-jvm-0.3.0-beta.4.jar">
<element id="module-output" name="util.jvmMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-jvm-0.3.0-beta.8">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-jvm-0.3.0-beta.8.jar">
<element id="module-output" name="util.jvmMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-js-0.2.8">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-js-0.2.8.jar">
<element id="module-output" name="util.wasmJsMain" />
</root>
</artifact>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-js-0.3.0-beta.1">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-js-0.3.0-beta.1.jar" />
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-js-0.3.0-beta.11">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-js-0.3.0-beta.11.jar">
<element id="module-output" name="util.wasmJsMain" />
</root>
</artifact>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-js-0.3.0-beta.19">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-js-0.3.0-beta.19.jar" />
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-js-0.3.0-beta.2">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-js-0.3.0-beta.2.jar">
<element id="module-output" name="util.wasmJsMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-js-0.3.0-beta.4">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-js-0.3.0-beta.4.jar">
<element id="module-output" name="util.wasmJsMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-js-0.3.0-beta.8">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-js-0.3.0-beta.8.jar">
<element id="module-output" name="util.wasmJsMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-wasi-0.2.8">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-wasi-0.2.8.jar">
<element id="module-output" name="util.wasmWasiMain" />
</root>
</artifact>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.1">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-wasi-0.3.0-beta.1.jar" />
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.11">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-wasi-0.3.0-beta.11.jar">
<element id="module-output" name="util.wasmWasiMain" />
</root>
</artifact>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.19">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-wasi-0.3.0-beta.19.jar" />
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.2">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-wasi-0.3.0-beta.2.jar">
<element id="module-output" name="util.wasmWasiMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.4">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-wasi-0.3.0-beta.4.jar">
<element id="module-output" name="util.wasmWasiMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.8">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="util-wasm-wasi-0.3.0-beta.8.jar">
<element id="module-output" name="util.wasmWasiMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../src/jvmMain" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/../../src/jvmMain/kotlin" isTestSource="false" />
</content>
</component>
</module>
+99 -23
View File
@@ -1,10 +1,12 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
//@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
@file:OptIn(ExperimentalWasmDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version masterLibs.versions.kotlinVersion
`java-library`
kotlin("multiplatform") version masterLibs.versions.kotlinVersion
// `java-library`
`maven-publish`
alias(masterLibs.plugins.dokkaPlugin)
alias(masterLibs.plugins.versionPlugin)
@@ -14,30 +16,104 @@ 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 {
implementation(masterLibs.kotlinxSerialization)
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(masterLibs.versions.javaVersion.get()))
}
withSourcesJar()
}
//dependencies {
// implementation(masterLibs.kotlinxSerialization)
// // f. cmd line via annotation KMP?
// implementation(kotlin("reflect"))
//
//}
kotlin {
jvmToolchain(masterLibs.versions.javaVersion.get().toInt())
// compilerOptions {
// languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3)
// }
// explicitApi()
jvm {
compilerOptions.jvmTarget =
JvmTarget.valueOf("JVM_" + masterLibs.versions.javaVersion.get()) //JvmTarget.JVM_1_8
}
js {
browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
nodejs()
}
wasmJs {
browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
nodejs()
}
wasmWasi {
nodejs()
}
// macosX64()
macosArm64()
// iosX64()
// iosArm64()
// iosSimulatorArm64()
// watchosArm32()
// watchosArm64()
// watchosX64()
// watchosSimulatorArm64()
// watchosDeviceArm64()
// tvosArm64()
// tvosX64()
// tvosSimulatorArm64()
// mingwX64()
linuxX64()
// linuxArm64()
// androidNativeArm32()
// androidNativeArm64()
// androidNativeX86()
// androidNativeX64()
applyDefaultHierarchyTemplate()
sourceSets {
commonMain.dependencies {
implementation(masterLibs.kotlinxSerialization)
}
jvmMain.dependencies {
implementation(kotlin("reflect"))
}
// commonTest.dependencies {
// implementation(libs.kotlin.test)
// implementation(libs.serialization.json)
// }
}
}
//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"
@@ -59,10 +135,10 @@ publishing {
url = uri("https://nexus.keinekekse.net/repository/maven-releases/")
}
}
publications.create<MavenPublication>("maven") {
from(components["java"])
// artifact(tasks["dokkaJar"])
}
// publications.create<MavenPublication>("maven") {
// from(components["java"])
//// artifact(tasks["dokkaJar"])
// }
}
// ./gradlew dokkaGenerate
+2 -2
View File
@@ -1,3 +1,3 @@
#auto generated
#Sat Dec 20 00:41:12 CET 2025
build.number=103
#Mon Jan 19 22:42:11 CET 2026
build.number=130
+6 -1
View File
@@ -1,3 +1,8 @@
#Kotlin
kotlin.code.style=official
org.gradle.configuration-cache=true
kotlin.daemon.jvmargs=-Xmx3072M
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
#Gradle
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
org.gradle.caching=true
+1 -1
View File
@@ -19,7 +19,7 @@ dependencyResolutionManagement {
versionCatalogs {
create("masterLibs") {
// from(files("gradle/masterlibs.versions.toml"))
from("net.keinekekse:version-catalog:100")
from("net.keinekekse:version-catalog:128")
}
}
}
@@ -0,0 +1,477 @@
package net.keinekekse.args
import net.keinekekse.util.VERSION
import net.keinekekse.version.Version
/**
* Terminates the currently running application with the specified exit code.
* This function does not return as it stops the application process.
*
* Works only for JVM and macOS-/Linux-native targets.
*
* @param rc The exit code to return when the application terminates. Defaults to 0,
* indicating normal termination.
* @return This function does not return as the application is terminated.
*/
expect fun exitApplication(rc: Int = 0): Nothing
/**
* Checks if standard input is ready for reading.
* This function is used to determine if there is data available to be read from standard input.
*
* Works only for JVM and macOS-/Linux-native targets.
*
* @return True if standard input is ready for reading, false otherwise.
*/
expect fun isStdInputReady(): Boolean
class VerifyParameterException(message: String) : RuntimeException(message)
/**
* Throws a [VerifyParameterException] with the specified error message.
* This function is used to signal that there was an error during argument parsing.
*
* For use in the application. The exception will be caught by the command-line parser
* and displayed to the user together with the help information.
*
* @param msg The error message to include in the exception.
* @throws VerifyParameterException with the provided error message.
*/
fun showParsingError(msg: String): Nothing {
throw VerifyParameterException(msg)
}
/**
* 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>)
}
/**
* Represents a subcommand that can be added to a command-line argument parser.
*
* A `SubCommand` is used to define and configure subcommands in a CLI application,
* allowing for structured argument parsing and execution of specific logic based
* on the subcommand invoked. It provides mechanisms to add parameters to the
* subcommand, define execution logic, and manage case sensitivity for argument
* names.
*
* @property obj Internal data container for subcommand-specific configuration and parameters.
* @property ignoreCase Defines whether argument names are case-insensitive. Defaults to `false`.
*/
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 processedLong = long.handleCase(ignoreCase)
val processedShort = short?.toString()?.handleCase(ignoreCase)
return runCatching {
type.createOption(processedLong, processedShort, description, isMandatory).apply {
verifyParameter(this)
registerParameter(this, processedLong, processedShort)
}.prop
}.getOrElse { showDefiningError(it.message ?: "") }
}
private fun registerParameter(parameter: ParameterData<*>, long: String, short: String?) {
@Suppress("UNCHECKED_CAST")
val data = parameter as ParameterData<Any>
obj.params[long] = data
short?.let { obj.params[it] = data }
}
private fun verifyParameter(parameter: ParameterData<*>) {
val longName = parameter.long
if (!NAME_PATTERN.matches(longName)) {
throw ArgumentParserException.invalidName(longName)
}
if (obj.params.containsKey(longName)) {
throw ArgumentParserException.optionExists(longName)
}
parameter.short?.let { shortName ->
if (!SHORT_NAME_PATTERN.matches(shortName)) {
throw ArgumentParserException.invalidShortName(shortName)
}
if (obj.params.containsKey(shortName)) {
throw ArgumentParserException.optionExists(shortName)
}
}
val lowerLong = longName.lowercase()
if (lowerLong == "help" || lowerLong == "version") {
throw ArgumentParserException.noHelpOrVersion()
}
}
}
/**
* Represents a command-line interface for parsing and executing commands, subcommands, and parameters.
*
* This class facilitates defining global parameters and subcommands, parsing provided arguments,
* validating inputs, and executing the corresponding actions. It supports various types of parameters,
* including strings, long integers, and booleans, with both optional and mandatory requirements.
* Additionally, it provides options for ignoring case sensitivity and handling remaining parameters.
*
* @param programVersion The version information of the program, including display name and copyright.
* @param ignoreCase Determines whether the parsing should ignore case sensitivity for commands and parameters.
* @param remainingDescription The description for remaining command-line arguments, or null if no arguments are allowed.
* @param emptyCommandLineAllowed Indicates whether the parser should allow an empty command line to be parsed. Defaults to false.
*
* ```
* // global parameters
*
* val commandLine = CommandLine(
* programVersion = VERSION,
* remainingDescription = "EPUB-Dateien",
* ignoreCase = true,
* )
*
* val param1 by commandLine.addStringParameter(
* long = "param",
* short = 'P',
* description = "parameter description"
* )
* // more global parameters
*
* commandLine.setExecution { additionalParameters: List<String> ->
* // called when no subcommand is specified
* }
*
* // subcommand
*
* val commandSubCommand = commandLine.addSubCommand(
* "command",
* "My custom command",
* listOf("cmd") // aliases
* )
*
* val par1 by commandSubCommand.addStringParameter(
* long = "par1",
* description = "Parameter Par1 ",
* isMandatory = true
* )
* // more parameters
*
* commandSubCommand.setExecution { additionalParameters ->
* // called when this subcommand is found in the command line
* }
*
* // more subcommands
*
* // start program
* commandLine.parseAndRun(*args)
* ```
*/
class CommandLine(
val programVersion: Version,
val ignoreCase: Boolean = false,
val remainingDescription: String? = null, // null -> no remaining allowed
val emptyCommandLineAllowed: Boolean = false,
) : AddParameter {
private var hasRun = false
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 = runCatching {
val normalizedName = normalizeAndVerifySubCommand(name, ignoreCase)
val subData = SubCommandData(normalizedName, description, aliases)
subCommands[normalizedName] = subData
aliases.forEach { alias ->
val alias = normalizeAndVerifySubCommand(alias, ignoreCase)
subCommands[alias] = subData
}
SubCommand.create(subData, ignoreCase)
}.getOrElse { showDefiningError(it.message ?: "") }
fun parseAndRun(vararg args: String) {
runCatching {
val selectedSubCommand = parse(args.toList())
selectedSubCommand.execute(selectedSubCommand.additionalParameters)
}.onFailure { exception ->
handleErrorAndShowHelp(exception as Exception)
}
}
private fun normalizeAndVerifySubCommand(cmd: String, ignoreCase: Boolean): String {
val normalized = cmd.handleCase(ignoreCase)
if (!matchSubCommand(normalized)) throw ArgumentParserException.invalidCommand(normalized)
return normalized
}
private fun parse(args: List<String>): SubCommandData {
if (hasRun) throw ArgumentParserException.noMoreParsing()
hasRun = true
if (args.isEmpty() && !emptyCommandLineAllowed) throw ArgumentParserException.commandRequired()
checkExecutors()
val remainingAfterGlobal = parseParameters(args, globalSubCommand.obj)
validateMandatoryParameters(globalSubCommand.obj)
val resultSubCommand = parseSubCommand(remainingAfterGlobal)
validateMandatoryParameters(resultSubCommand)
if (remainingDescription != null) {
resultSubCommand.additionalParameters += resolveRemainingFromPipe()
} else if (resultSubCommand.additionalParameters.isNotEmpty()) {
throw ArgumentParserException.noMoreOptions()
}
return resultSubCommand
}
private fun resolveRemainingFromPipe(): List<String> {
return if (isStdInputReady()) getRemainingParametersFromPipe() else emptyList()
}
private fun parseSubCommand(args: List<String>): SubCommandData {
val firstArg = args.firstOrNull()?.handleCase(ignoreCase)
val sub = subCommands[firstArg] ?: return globalSubCommand.obj.apply {
additionalParameters = args
}
return sub.apply { additionalParameters = parseParameters(args.drop(1), this) }
}
private fun validateMandatoryParameters(subCommand: SubCommandData) {
subCommand.params.filter { it.key == it.value.long }.forEach { (_, parameter) ->
if (parameter.isMandatory && parameter.prop.value == null) {
throw ArgumentParserException.optionRequired(parameter.long)
}
}
}
private fun parseParameters(args: List<String>, subCommand: SubCommandData): List<String> {
var index = 0
while (index < args.size) {
val (key, inlineValue) = matchArgument(args[index]) ?: return args.drop(index)
checkForHelpOrVersion(key)
val normalizedKey = key.handleCase(ignoreCase)
val param =
subCommand.params[normalizedKey] ?: throw ArgumentParserException.unknownOption(
normalizedKey
)
val (value, consumedExtra) = resolveParameterValue(
args,
index,
normalizedKey,
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].isNotBlank() -> {
args[currentIndex + 1] to 1
}
else -> throw ArgumentParserException.optionRequiresValue(key)
}
}
private fun getRemainingParametersFromPipe(): List<String> {
if (isStdInputReady()) {
val pipe = mutableListOf<String>()
while (true) {
val input = readlnOrNull() ?: break
pipe.add(input)
}
return pipe
}
return emptyList()
}
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 handleErrorAndShowHelp(
e: Exception?,
): Nothing {
val hasSubCommands = subCommands.isNotEmpty()
val hasGlobalParams = globalSubCommand.obj.params.isNotEmpty()
val allowsRemaining = remainingDescription != null
println("\n${programVersion.displayName} ${programVersion.version} ${programVersion.copyright}\n")
println("${programVersion.description}\n")
val usagePrefix = "usage: ${programVersion.name}"
val globalPart =
if (hasGlobalParams) "[${if (hasSubCommands) "GLOBAL " else ""}PARAMETER]" else ""
val subPart = if (hasSubCommands) " subcommand [PARAMETER]" else ""
val remainPart = if (allowsRemaining) " <$remainingDescription>" else ""
println("$usagePrefix $globalPart$subPart$remainPart\n")
if (hasGlobalParams) {
println("${if (hasSubCommands) "Global " else ""}Parameters:")
}
showParameters(globalSubCommand.obj.params)
if (hasSubCommands) {
println()
println("Subcommands:")
showSubcommands(subCommands)
}
e?.message?.let {
println()
println(ArgumentParserException.PARSING_ERROR_MSG + it + "\n")
}
println()
exitApplication(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) ->
val parmStr = INDENT + INDENT +
(param.short?.let { "-$it, " } ?: " ") +
"--${param.long}" + if (param.type != ParameterType.BOOLEAN) " <...>" else ""
len = maxOf(len, parmStr.length)
Pair(parmStr, "${param.description} ${if (param.isMandatory) "*" else ""}")
}.forEach {
println(it.first.padEnd(len + 3) + it.second)
}
}
private fun showVersion(): Nothing {
println("\n${programVersion.displayName} ${programVersion.fullVersion} ${programVersion.copyright}\n\n(${VERSION.displayName} ${VERSION.fullVersion} ${VERSION.copyright})\n")
exitApplication(0)
}
}
@@ -0,0 +1,212 @@
package net.keinekekse.args
import kotlin.reflect.KProperty
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(),
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: "
const val DEFINITION_ERROR_MSG = "\n***\n*** ERROR defining arguments in source code: "
fun showHelp() = ArgumentParserException(null)
fun noHelpOrVersion() =
ArgumentParserException("Option 'help' and option 'version' are not allowed.")
fun noExecutorDefined(name: String?) =
ArgumentParserException("No lambda found for '$name'.")
fun optionExists(name: String) = ArgumentParserException("Option '$name' already exists.")
fun unknownOption(name: String) = ArgumentParserException("Unknown option '$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 one 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("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]) }
// Fehler im Quellcode anzeigen
internal fun showDefiningError(msg: String): Nothing {
println(ArgumentParserException.DEFINITION_ERROR_MSG + msg + "\n")
exitApplication(1)
}
@@ -0,0 +1,18 @@
package net.keinekekse.util
import net.keinekekse.version.Version
object VERSION: Version {
override val version = "0.3.0"
override val fullVersion = "0.3.0+build.130"
override val major = 0
override val minor = 3
override val patch = 0
override val preRelease = ""
override val buildMetadata = "build.130"
override val buildDate = "2026-01-19T21:42:11.032491Z"
override val displayName = "Utility Library"
override val copyright = "Copyright (c) 2025-2026 Hartmut Bauer"
override val name = "util"
override val description = "Kotlin Utility Library"
}
@@ -0,0 +1,16 @@
package net.keinekekse.version
interface Version {
val version: String
val fullVersion: String
val major: Int
val minor: Int
val patch: Int
val preRelease: String
val buildMetadata: String
val buildDate: String
val displayName: String
val copyright: String
val name: String
val description: String
}
@@ -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,461 @@
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,
parameterValue: String,
param: CmdParameter,
): Pair<String, Int> {
return when {
param.type == ParameterType.BOOLEAN -> {
if (parameterValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key)
TRUE to 0
}
parameterValue.isNotBlank() -> parameterValue to 0
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,9 @@
package net.keinekekse.args
import kotlin.system.exitProcess
actual fun isStdInputReady(): Boolean =
(System.`in`.available() > 0)
actual fun exitApplication(rc: Int): Nothing =
exitProcess(rc)
@@ -1,7 +1,7 @@
package net.keinekekse.args
package net.keinekekse.args.v1
import net.keinekekse.args.AbstractCommandLineParser.Companion.NAME_PATTERN
import net.keinekekse.args.AbstractCommandLineParser.Companion.SHORT_NAME_PATTERN
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
@@ -1,5 +1,6 @@
package net.keinekekse.config
import net.keinekekse.util.VERSION
import net.keinekekse.version.Version
import java.io.File
@@ -10,15 +11,16 @@ import java.io.File
* @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) }
// val applicationVersion: Version by lazy { Version(this::class.java) }
/** Library version information */
val libraryVersion: Version by lazy { Version(Version::class.java) }
val libraryVersion: Version = VERSION //by lazy { Version(Version::class.java) }
/** Configuration directory path */
val configDirectory: String by lazy {
@@ -1,4 +1,4 @@
package net.keinekekse.version
package net.keinekekse.version.manifest
import java.time.Instant
import java.time.format.DateTimeFormatter
@@ -0,0 +1,22 @@
package net.keinekekse.args
import kotlinx.cinterop.*
import platform.posix.*
// macOS + Linux
@OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
actual fun isStdInputReady(): Boolean {
return memScoped {
val pfd = alloc<pollfd>()
pfd.fd = STDIN_FILENO
pfd.events = POLLIN.toShort()
// timeout 0 means return immediately
poll(pfd.ptr.reinterpret(), 1u, 10) > 0
}
}
actual fun exitApplication(rc: Int): Nothing {
exit(rc)
return Unit as Nothing
}
@@ -0,0 +1,9 @@
package net.keinekekse.args
actual fun isStdInputReady(): Boolean {
TODO("Not implemented on this platform")
}
actual fun exitApplication(rc: Int): Nothing {
TODO("Not implemented on this platform")
}
@@ -0,0 +1,11 @@
package net.keinekekse.args
// js + wasmJs
actual fun isStdInputReady(): Boolean {
TODO("Not implemented for this platform")
}
actual fun exitApplication(rc: Int): Nothing {
TODO("Not implemented for this platform")
}