diff --git a/.gitignore b/.gitignore
index 1b6985c..adfa9bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/.idea/artifacts/util_js_0_2_8.xml b/.idea/artifacts/util_js_0_2_8.xml
new file mode 100644
index 0000000..0ba6a2b
--- /dev/null
+++ b/.idea/artifacts/util_js_0_2_8.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_js_0_3_0_beta_1.xml b/.idea/artifacts/util_js_0_3_0_beta_1.xml
new file mode 100644
index 0000000..00ae358
--- /dev/null
+++ b/.idea/artifacts/util_js_0_3_0_beta_1.xml
@@ -0,0 +1,6 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_js_0_3_0_beta_11.xml b/.idea/artifacts/util_js_0_3_0_beta_11.xml
new file mode 100644
index 0000000..d84ee32
--- /dev/null
+++ b/.idea/artifacts/util_js_0_3_0_beta_11.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_js_0_3_0_beta_2.xml b/.idea/artifacts/util_js_0_3_0_beta_2.xml
new file mode 100644
index 0000000..5eb4e8f
--- /dev/null
+++ b/.idea/artifacts/util_js_0_3_0_beta_2.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_js_0_3_0_beta_4.xml b/.idea/artifacts/util_js_0_3_0_beta_4.xml
new file mode 100644
index 0000000..b612884
--- /dev/null
+++ b/.idea/artifacts/util_js_0_3_0_beta_4.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_js_0_3_0_beta_8.xml b/.idea/artifacts/util_js_0_3_0_beta_8.xml
new file mode 100644
index 0000000..113590e
--- /dev/null
+++ b/.idea/artifacts/util_js_0_3_0_beta_8.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_jvm_0_2_8.xml b/.idea/artifacts/util_jvm_0_2_8.xml
new file mode 100644
index 0000000..8fa3d1f
--- /dev/null
+++ b/.idea/artifacts/util_jvm_0_2_8.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_jvm_0_3_0_beta_1.xml b/.idea/artifacts/util_jvm_0_3_0_beta_1.xml
new file mode 100644
index 0000000..8019946
--- /dev/null
+++ b/.idea/artifacts/util_jvm_0_3_0_beta_1.xml
@@ -0,0 +1,6 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_jvm_0_3_0_beta_11.xml b/.idea/artifacts/util_jvm_0_3_0_beta_11.xml
new file mode 100644
index 0000000..71efc8f
--- /dev/null
+++ b/.idea/artifacts/util_jvm_0_3_0_beta_11.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_jvm_0_3_0_beta_2.xml b/.idea/artifacts/util_jvm_0_3_0_beta_2.xml
new file mode 100644
index 0000000..46fbd93
--- /dev/null
+++ b/.idea/artifacts/util_jvm_0_3_0_beta_2.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_jvm_0_3_0_beta_4.xml b/.idea/artifacts/util_jvm_0_3_0_beta_4.xml
new file mode 100644
index 0000000..e64ce9a
--- /dev/null
+++ b/.idea/artifacts/util_jvm_0_3_0_beta_4.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_jvm_0_3_0_beta_8.xml b/.idea/artifacts/util_jvm_0_3_0_beta_8.xml
new file mode 100644
index 0000000..987a037
--- /dev/null
+++ b/.idea/artifacts/util_jvm_0_3_0_beta_8.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_js_0_2_8.xml b/.idea/artifacts/util_wasm_js_0_2_8.xml
new file mode 100644
index 0000000..50d7e15
--- /dev/null
+++ b/.idea/artifacts/util_wasm_js_0_2_8.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_js_0_3_0_beta_1.xml b/.idea/artifacts/util_wasm_js_0_3_0_beta_1.xml
new file mode 100644
index 0000000..188f1b2
--- /dev/null
+++ b/.idea/artifacts/util_wasm_js_0_3_0_beta_1.xml
@@ -0,0 +1,6 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_js_0_3_0_beta_11.xml b/.idea/artifacts/util_wasm_js_0_3_0_beta_11.xml
new file mode 100644
index 0000000..4ebd95d
--- /dev/null
+++ b/.idea/artifacts/util_wasm_js_0_3_0_beta_11.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_js_0_3_0_beta_2.xml b/.idea/artifacts/util_wasm_js_0_3_0_beta_2.xml
new file mode 100644
index 0000000..64917ca
--- /dev/null
+++ b/.idea/artifacts/util_wasm_js_0_3_0_beta_2.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_js_0_3_0_beta_4.xml b/.idea/artifacts/util_wasm_js_0_3_0_beta_4.xml
new file mode 100644
index 0000000..971f2c1
--- /dev/null
+++ b/.idea/artifacts/util_wasm_js_0_3_0_beta_4.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_js_0_3_0_beta_8.xml b/.idea/artifacts/util_wasm_js_0_3_0_beta_8.xml
new file mode 100644
index 0000000..7f73424
--- /dev/null
+++ b/.idea/artifacts/util_wasm_js_0_3_0_beta_8.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_wasi_0_2_8.xml b/.idea/artifacts/util_wasm_wasi_0_2_8.xml
new file mode 100644
index 0000000..587e6c4
--- /dev/null
+++ b/.idea/artifacts/util_wasm_wasi_0_2_8.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_wasi_0_3_0_beta_1.xml b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_1.xml
new file mode 100644
index 0000000..efdc567
--- /dev/null
+++ b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_1.xml
@@ -0,0 +1,6 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_wasi_0_3_0_beta_11.xml b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_11.xml
new file mode 100644
index 0000000..45d90a1
--- /dev/null
+++ b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_11.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_wasi_0_3_0_beta_2.xml b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_2.xml
new file mode 100644
index 0000000..b2d02c0
--- /dev/null
+++ b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_2.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_wasi_0_3_0_beta_4.xml b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_4.xml
new file mode 100644
index 0000000..ff662b4
--- /dev/null
+++ b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_4.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/util_wasm_wasi_0_3_0_beta_8.xml b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_8.xml
new file mode 100644
index 0000000..ccef720
--- /dev/null
+++ b/.idea/artifacts/util_wasm_wasi_0_3_0_beta_8.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index b86273d..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 7c47dd4..c0278ba 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,5 +4,5 @@
-
+
\ No newline at end of file
diff --git a/.idea/modules/util.jvmMain.iml b/.idea/modules/util.jvmMain.iml
new file mode 100644
index 0000000..2c83a31
--- /dev/null
+++ b/.idea/modules/util.jvmMain.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 4843d18..518c3d3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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)
@@ -20,27 +22,98 @@ repositories {
maven("https://nexus.keinekekse.net/repository/maven-public/")
}
-dependencies {
- implementation(masterLibs.kotlinxSerialization)
- // f. cmd line via annotation KMP?
- implementation(kotlin("reflect"))
-
-}
-
-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("dokkaJar") {
group = "build"
@@ -62,10 +135,10 @@ publishing {
url = uri("https://nexus.keinekekse.net/repository/maven-releases/")
}
}
- publications.create("maven") {
- from(components["java"])
-// artifact(tasks["dokkaJar"])
- }
+// publications.create("maven") {
+// from(components["java"])
+//// artifact(tasks["dokkaJar"])
+// }
}
// ./gradlew dokkaGenerate
diff --git a/buildnumber.properties b/buildnumber.properties
index 4d39833..b1b6e94 100644
--- a/buildnumber.properties
+++ b/buildnumber.properties
@@ -1,3 +1,3 @@
#auto generated
-#Sun Jan 18 02:02:24 CET 2026
-build.number=106
+#Mon Jan 19 21:39:57 CET 2026
+build.number=129
diff --git a/gradle.properties b/gradle.properties
index 9832217..e594967 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -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
diff --git a/settings.gradle.kts b/settings.gradle.kts
index ab23bf9..ff040b5 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,7 +19,7 @@ dependencyResolutionManagement {
versionCatalogs {
create("masterLibs") {
// from(files("gradle/masterlibs.versions.toml"))
- from("net.keinekekse:version-catalog:107")
+ from("net.keinekekse:version-catalog:127")
}
}
}
diff --git a/src/commonMain/kotlin/net/keinekekse/args/impl.kt b/src/commonMain/kotlin/net/keinekekse/args/impl.kt
new file mode 100644
index 0000000..52c27c9
--- /dev/null
+++ b/src/commonMain/kotlin/net/keinekekse/args/impl.kt
@@ -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)
+}
+
+/**
+ * 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 {
+ return createParameter(ParameterType.STRING, long, short, description, isMandatory)
+ }
+
+ override fun addLongParameter(
+ long: String,
+ short: Char?,
+ description: String,
+ isMandatory: Boolean,
+ ): ParameterValue {
+ return createParameter(ParameterType.LONG, long, short, description, isMandatory)
+ }
+
+ override fun addBooleanParameter(
+ long: String,
+ short: Char?,
+ description: String,
+ isMandatory: Boolean,
+ ): ParameterValue {
+ return createParameter(ParameterType.BOOLEAN, long, short, description, isMandatory)
+ }
+
+ override fun setExecution(run: RunProgram) {
+ obj.setExecution(run)
+ }
+
+ private fun createParameter(
+ type: ParameterType,
+ long: String,
+ short: Char?,
+ description: String,
+ isMandatory: Boolean,
+ ): ParameterValue {
+ val 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
+ 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 ->
+ * // 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()
+
+ override fun addStringParameter(
+ long: String,
+ short: Char?,
+ description: String,
+ isMandatory: Boolean,
+ ): ParameterValue {
+ return globalSubCommand.addStringParameter(long, short, description, isMandatory)
+ }
+
+ override fun addLongParameter(
+ long: String,
+ short: Char?,
+ description: String,
+ isMandatory: Boolean,
+ ): ParameterValue {
+ return globalSubCommand.addLongParameter(long, short, description, isMandatory)
+ }
+
+ override fun addBooleanParameter(
+ long: String,
+ short: Char?,
+ description: String,
+ isMandatory: Boolean,
+ ): ParameterValue {
+ return globalSubCommand.addBooleanParameter(long, short, description, isMandatory)
+ }
+
+ override fun setExecution(run: RunProgram) {
+ globalSubCommand.setExecution(run)
+ }
+
+ fun addSubCommand(
+ name: String,
+ description: String,
+ aliases: List = emptyList(),
+ ): SubCommand = 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): 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 {
+ return if (isStdInputReady()) getRemainingParametersFromPipe() else emptyList()
+ }
+
+ private fun parseSubCommand(args: List): 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, subCommand: SubCommandData): List {
+ 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,
+ currentIndex: Int,
+ key: String,
+ inlineValue: String,
+ param: ParameterData,
+ ): Pair {
+ return when {
+ param.type == ParameterType.BOOLEAN -> {
+ if (inlineValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key)
+ TRUE to 0
+ }
+
+ // --name=--irgendwas ist OK
+ inlineValue.isNotBlank() -> inlineValue to 0
+
+ // --name --irgendwas ist nicht OK
+ currentIndex < args.size - 1
+ && !ARGUMENT_PATTERN.matches(args[currentIndex + 1])
+ && args[currentIndex + 1].isNotBlank() -> {
+ args[currentIndex + 1] to 1
+ }
+
+ else -> throw ArgumentParserException.optionRequiresValue(key)
+ }
+ }
+
+ private fun getRemainingParametersFromPipe(): List {
+ if (isStdInputReady()) {
+ val pipe = mutableListOf()
+ 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) {
+ parameters.filter { it.key == it.value.name }.forEach { (_, param) ->
+ val aliases = buildString {
+ param.aliases.forEach { alias ->
+ append(" | $alias")
+ }
+ }
+ println("$INDENT${param.name}$aliases ${param.description}")
+ showParameters(param.params)
+ println()
+ }
+ }
+
+ private fun showParameters(parameters: MutableMap>) {
+ var len = 0
+ parameters.filter { it.key == it.value.long }.map { (_, param) ->
+ 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)
+ }
+
+}
diff --git a/src/main/kotlin/net/keinekekse/args/pub.kt b/src/commonMain/kotlin/net/keinekekse/args/internal.kt
similarity index 81%
rename from src/main/kotlin/net/keinekekse/args/pub.kt
rename to src/commonMain/kotlin/net/keinekekse/args/internal.kt
index c2d8b70..1c96776 100644
--- a/src/main/kotlin/net/keinekekse/args/pub.kt
+++ b/src/commonMain/kotlin/net/keinekekse/args/internal.kt
@@ -1,7 +1,6 @@
package net.keinekekse.args
import kotlin.reflect.KProperty
-import kotlin.system.exitProcess
interface ValueDelegate {
var value: T?
@@ -152,48 +151,29 @@ interface AddParameter {
// 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"
+ 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 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.")
@@ -211,7 +191,7 @@ 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 ARGUMENT_PATTERN = """^(?:-(${SHORT_NAME})|--(${NAME}))(?:[=:](.*)|$)$""".toRegex()
internal val NAME_PATTERN = """^${NAME}$""".toRegex()
internal val SHORT_NAME_PATTERN = """^${SHORT_NAME}$""".toRegex()
@@ -225,7 +205,8 @@ internal fun matchSubCommand(cmd: String) =
internal fun matchArgument(arg: String): Pair? =
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.format(msg))
- exitProcess(1)
+ println(ArgumentParserException.DEFINITION_ERROR_MSG + msg + "\n")
+ exitApplication(1)
}
diff --git a/src/main/kotlin/net/keinekekse/util/Version.kt b/src/commonMain/kotlin/net/keinekekse/util/Version.kt
similarity index 51%
rename from src/main/kotlin/net/keinekekse/util/Version.kt
rename to src/commonMain/kotlin/net/keinekekse/util/Version.kt
index 30695fb..7ed8f34 100644
--- a/src/main/kotlin/net/keinekekse/util/Version.kt
+++ b/src/commonMain/kotlin/net/keinekekse/util/Version.kt
@@ -3,14 +3,14 @@ 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 version = "0.3.0-beta.19+build.129"
+ override val fullVersion = "0.3.0-beta.19+build.129"
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 minor = 3
+ override val patch = 0
+ override val preRelease = "beta.19"
+ override val buildMetadata = "build.129"
+ override val buildDate = "2026-01-19T20:39:57.564140Z"
override val displayName = "Utility Library"
override val copyright = "Copyright (c) 2025-2026 Hartmut Bauer"
override val name = "util"
diff --git a/src/main/kotlin/net/keinekekse/util/delegate.kt b/src/commonMain/kotlin/net/keinekekse/util/delegate.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/util/delegate.kt
rename to src/commonMain/kotlin/net/keinekekse/util/delegate.kt
diff --git a/src/main/kotlin/net/keinekekse/version/Version.kt b/src/commonMain/kotlin/net/keinekekse/version/Version.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/version/Version.kt
rename to src/commonMain/kotlin/net/keinekekse/version/Version.kt
diff --git a/src/main/kotlin/net/keinekekse/args/annotation/api.kt b/src/jvmMain/kotlin/net/keinekekse/args/annotation/api.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/args/annotation/api.kt
rename to src/jvmMain/kotlin/net/keinekekse/args/annotation/api.kt
diff --git a/src/main/kotlin/net/keinekekse/args/annotation/impl.kt b/src/jvmMain/kotlin/net/keinekekse/args/annotation/impl.kt
similarity index 98%
rename from src/main/kotlin/net/keinekekse/args/annotation/impl.kt
rename to src/jvmMain/kotlin/net/keinekekse/args/annotation/impl.kt
index 5a784ec..b90c00e 100644
--- a/src/main/kotlin/net/keinekekse/args/annotation/impl.kt
+++ b/src/jvmMain/kotlin/net/keinekekse/args/annotation/impl.kt
@@ -386,19 +386,16 @@ private fun resolveParameterValue(
args: List,
currentIndex: Int,
key: String,
- inlineValue: String,
+ parameterValue: String,
param: CmdParameter,
): Pair {
return when {
param.type == ParameterType.BOOLEAN -> {
- if (inlineValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key)
+ if (parameterValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key)
TRUE to 0
}
- // --name=--irgendwas ist OK
- inlineValue.isNotBlank() -> inlineValue to 0
-
- // --name --irgendwas ist nicht OK
+ parameterValue.isNotBlank() -> parameterValue to 0
currentIndex < args.size - 1 && !ARGUMENT_PATTERN.matches(args[currentIndex + 1]) -> {
args[currentIndex + 1] to 1
}
diff --git a/src/jvmMain/kotlin/net/keinekekse/args/impl.jvm.kt b/src/jvmMain/kotlin/net/keinekekse/args/impl.jvm.kt
new file mode 100644
index 0000000..b9dccb4
--- /dev/null
+++ b/src/jvmMain/kotlin/net/keinekekse/args/impl.jvm.kt
@@ -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)
diff --git a/src/main/kotlin/net/keinekekse/args/v1/arguments.kt b/src/jvmMain/kotlin/net/keinekekse/args/v1/arguments.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/args/v1/arguments.kt
rename to src/jvmMain/kotlin/net/keinekekse/args/v1/arguments.kt
diff --git a/src/main/kotlin/net/keinekekse/config/init.kt b/src/jvmMain/kotlin/net/keinekekse/config/init.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/config/init.kt
rename to src/jvmMain/kotlin/net/keinekekse/config/init.kt
diff --git a/src/main/kotlin/net/keinekekse/config/system_config.kt b/src/jvmMain/kotlin/net/keinekekse/config/system_config.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/config/system_config.kt
rename to src/jvmMain/kotlin/net/keinekekse/config/system_config.kt
diff --git a/src/main/kotlin/net/keinekekse/config/user_config.kt b/src/jvmMain/kotlin/net/keinekekse/config/user_config.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/config/user_config.kt
rename to src/jvmMain/kotlin/net/keinekekse/config/user_config.kt
diff --git a/src/main/kotlin/net/keinekekse/exec/exec.kt b/src/jvmMain/kotlin/net/keinekekse/exec/exec.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/exec/exec.kt
rename to src/jvmMain/kotlin/net/keinekekse/exec/exec.kt
diff --git a/src/main/kotlin/net/keinekekse/util/net.kt b/src/jvmMain/kotlin/net/keinekekse/util/net.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/util/net.kt
rename to src/jvmMain/kotlin/net/keinekekse/util/net.kt
diff --git a/src/main/kotlin/net/keinekekse/version/manifest/version.kt b/src/jvmMain/kotlin/net/keinekekse/version/manifest/version.kt
similarity index 100%
rename from src/main/kotlin/net/keinekekse/version/manifest/version.kt
rename to src/jvmMain/kotlin/net/keinekekse/version/manifest/version.kt
diff --git a/src/main/kotlin/net/keinekekse/args/priv.kt b/src/main/kotlin/net/keinekekse/args/priv.kt
deleted file mode 100644
index 15ec705..0000000
--- a/src/main/kotlin/net/keinekekse/args/priv.kt
+++ /dev/null
@@ -1,352 +0,0 @@
-package net.keinekekse.args
-
-import net.keinekekse.version.Version
-import kotlin.system.exitProcess
-
-class VerifyParameterException(message: String) : RuntimeException(message)
-
-fun showParsingError(msg: String): Nothing {
- println(ArgumentParserException.PARSING_ERROR_MSG.format(msg))
- exitProcess(2)
-}
-
-/**
- * Functional interface representing a runnable program execution logic.
- *
- * This interface allows defining a lambda function or block of code that
- * processes a list of remaining arguments. It is intended to be used as
- * a mechanism to encapsulate the execution logic for commands or subcommands
- * in a command-line parsing framework.
- *
- * The primary use case for this interface is to define execution behavior
- * for subcommands or global commands, commonly used in CLI applications.
- *
- * @constructor A single abstract method is provided to execute the specified logic.
- */
-fun interface RunProgram {
- fun run(remaining: List)
-}
-
-class SubCommand private constructor(
- internal val obj: SubCommandData,
- val ignoreCase: Boolean = false,
-) : AddParameter {
- companion object {
- internal fun create(obj: SubCommandData, ignoreCase: Boolean = false): SubCommand =
- SubCommand(obj, ignoreCase)
- }
-
- override fun addStringParameter(
- long: String,
- short: Char?,
- description: String,
- isMandatory: Boolean,
- ): ParameterValue {
- return createParameter(ParameterType.STRING, long, short, description, isMandatory)
- }
-
- override fun addLongParameter(
- long: String,
- short: Char?,
- description: String,
- isMandatory: Boolean,
- ): ParameterValue {
- return createParameter(ParameterType.LONG, long, short, description, isMandatory)
- }
-
- override fun addBooleanParameter(
- long: String,
- short: Char?,
- description: String,
- isMandatory: Boolean,
- ): ParameterValue {
- return createParameter(ParameterType.BOOLEAN, long, short, description, isMandatory)
- }
-
- override fun setExecution(run: RunProgram) {
- obj.setExecution(run)
- }
-
- private fun createParameter(
- type: ParameterType,
- long: String,
- short: Char?,
- description: String,
- isMandatory: Boolean,
- ): ParameterValue {
- val result = runCatching {
- val long = long.handleCase(ignoreCase)
- val short = short?.toString()?.handleCase(ignoreCase)
- type.createOption(long, short, description, isMandatory).apply {
- verifyParameter(this)
-
- @Suppress("UNCHECKED_CAST")
- obj.params[long] = this as ParameterData
- short?.let {
- obj.params[it] = this
- }
- }.prop
- }
- return result.getOrElse { showDefiningError(it.message ?: "") }
- }
-
- private fun verifyParameter(parameter: ParameterData<*>) {
- if (!NAME_PATTERN.matches(parameter.long)) throw ArgumentParserException.invalidName(
- parameter.long
- )
- if (obj.params.containsKey(parameter.long)) throw ArgumentParserException.optionExists(
- parameter.long
- )
- parameter.short?.let {
- if (!SHORT_NAME_PATTERN.matches(it)) {
- throw ArgumentParserException.invalidShortName(it)
- }
- if (obj.params.containsKey(it)) throw ArgumentParserException.optionExists(it)
- }
- val n = parameter.long.lowercase()
- if (n == "help" || n == "version") {
- throw ArgumentParserException.noHelpOrVersion()
- }
-
- }
-
-}
-
-class CommandLine(
- val programVersion: Version,
- val ignoreCase: Boolean = false,
- val remainingDescription: String? = null, // null -> no remaining allowed
-) : AddParameter {
- private val globalSubCommand = SubCommand.create(SubCommandData(), ignoreCase)
- private var subCommands = mutableMapOf()
-
- override fun addStringParameter(
- long: String,
- short: Char?,
- description: String,
- isMandatory: Boolean,
- ): ParameterValue {
- return globalSubCommand.addStringParameter(long, short, description, isMandatory)
- }
-
- override fun addLongParameter(
- long: String,
- short: Char?,
- description: String,
- isMandatory: Boolean,
- ): ParameterValue {
- return globalSubCommand.addLongParameter(long, short, description, isMandatory)
- }
-
- override fun addBooleanParameter(
- long: String,
- short: Char?,
- description: String,
- isMandatory: Boolean,
- ): ParameterValue {
- return globalSubCommand.addBooleanParameter(long, short, description, isMandatory)
- }
-
- override fun setExecution(run: RunProgram) {
- globalSubCommand.setExecution(run)
- }
-
- fun addSubCommand(
- name: String,
- description: String,
- aliases: List = emptyList(),
- ): SubCommand {
- val result = runCatching {
- val name = verifySubCommand(name, ignoreCase)
- val subData = SubCommandData(name, description, aliases)
- subCommands[name] = subData
- aliases.forEach { alias ->
- val alias = verifySubCommand(alias, ignoreCase)
- subCommands[alias] = subData
- }
- SubCommand.create(subData, ignoreCase)
- }
- return result.getOrElse { showDefiningError(it.message ?: "") }
- }
-
- fun parseAndRun(vararg args: String) {
- val result = runCatching {
- val selectedSubCommand = parse(args.toList())
- selectedSubCommand.execute(selectedSubCommand.additionalParameters)
- }
- result.exceptionOrNull()?.let { showHelp(it as Exception) }
- }
-
- private fun verifySubCommand(cmd: String, ignoreCase: Boolean): String {
- val cmd = cmd.handleCase(ignoreCase)
- if (!matchSubCommand(cmd)) throw ArgumentParserException.invalidCommand(cmd)
- return cmd
- }
-
- private fun parse(args: List): SubCommandData {
- if (args.isEmpty()) {
- throw ArgumentParserException.commandRequired()
- }
- checkExecutors()
-
- val remaining = parseParameters(args, globalSubCommand.obj)
- validateMandatoryParameters(globalSubCommand.obj)
-
- val resultSubCommand = parseSubCommand(remaining)
- validateMandatoryParameters(resultSubCommand)
-
- if (resultSubCommand.additionalParameters.isNotEmpty() && remainingDescription != null) {
- throw ArgumentParserException.noMoreOptions()
- }
-
- return resultSubCommand
- }
-
- private fun parseSubCommand(args: List): SubCommandData {
- val sub = args.firstOrNull()?.handleCase(ignoreCase)?.let { subCommands[it] }
- ?: return globalSubCommand.obj.apply { additionalParameters = args }
- return sub.apply { additionalParameters = parseParameters(args.drop(1), this) }
- }
-
- private fun validateMandatoryParameters(subCommand: SubCommandData) {
- subCommand.params.forEach { (_, parameter) ->
- if (parameter.isMandatory && parameter.prop.value == null) {
- throw ArgumentParserException.optionRequiresValue(parameter.long)
- }
- }
- }
-
- private fun parseParameters(args: List, subCommand: SubCommandData): List {
- var index = 0
- while (index < args.size) {
- val parseResult = matchArgument(args[index]) ?: return args.drop(index)
-
- var (key, inlineValue) = parseResult
- checkForHelpOrVersion(key)
- key = key.handleCase(ignoreCase)
-
- val param = subCommand.params[key] ?: throw ArgumentParserException.unknownOption(key)
-
- val (value, consumedExtra) = resolveParameterValue(args, index, key, inlineValue, param)
- param.type.convertAndSet(value, param.prop)
-
- index += 1 + consumedExtra
- }
- return emptyList()
- }
-
- private fun resolveParameterValue(
- args: List,
- currentIndex: Int,
- key: String,
- inlineValue: String,
- param: ParameterData,
- ): Pair {
- return when {
- param.type == ParameterType.BOOLEAN -> {
- if (inlineValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key)
- TRUE to 0
- }
-
- // --name=--irgendwas ist OK
- inlineValue.isNotBlank() -> inlineValue to 0
-
- // --name --irgendwas ist nicht OK
- currentIndex < args.size - 1 && !ARGUMENT_PATTERN.matches(args[currentIndex + 1]) -> {
- args[currentIndex + 1] to 1
- }
-
- else -> throw ArgumentParserException.optionRequiresValue(key)
- }
- }
-
- private fun checkExecutors() {
- globalSubCommand.obj.checkExecutors()
- subCommands.values.forEach { it.checkExecutors() }
- }
-
- private fun checkForHelpOrVersion(key: String) {
- val k = key.lowercase()
- if (k == "help") {
- throw ArgumentParserException.showHelp()
- } else if (k == "version") {
- showVersion()
- }
- }
-
- private fun showHelp(
- e: Exception?,
- ): Nothing {
- val sub = !subCommands.isEmpty()
- val globalParam = globalSubCommand.obj.params.isNotEmpty()
- val remain = remainingDescription != null
-
- println("${programVersion.displayName} ${programVersion.version}, ${programVersion.copyright}\n")
- println(programVersion.description)
- println()
- println(
- "usage: ${programVersion.name} ${
- if (sub)
- if (globalParam) {
- "[GLOBAL PARAMETER]"
- } else {
- ""
- } + " subcommand [PARAMETER]"
- else "[PARAMETER]"
- } ${if (remain) "<$remainingDescription>" else ""}"
- )
- println()
- if (globalParam) {
- if (sub) {
- println("Global Parameters:")
- } else {
- println("Parameters:")
- }
- }
- showParameters(globalSubCommand.obj.params)
- if (sub) {
- println()
- println("Subcommands:")
- showSubcommands(subCommands)
- }
- e?.message?.let {
- println()
- println(ArgumentParserException.PARSING_ERROR_MSG.format(it))
- println()
- }
- exitProcess(0)
- }
-
- private fun showSubcommands(parameters: MutableMap) {
- parameters.filter { it.key == it.value.name }.forEach { (_, param) ->
- val aliases = buildString {
- param.aliases.forEach { alias ->
- append(" | $alias")
- }
- }
- println("$INDENT${param.name}$aliases ${param.description}")
- showParameters(param.params)
- println()
-
- }
-
- }
-
- private fun showParameters(parameters: MutableMap>) {
- var len = 0
- parameters.filter { it.key == it.value.long }.map { (_, param) ->
- len = maxOf(len, param.long.length + INDENT.length * 2 + 4 + 2)
- Pair(INDENT + INDENT + (param.short?.let { "-$it, " } ?: " ") + "--${param.long}",
- "${param.description} ${if (param.isMandatory) "*" else ""}")
- }.forEach {
- println(it.first.padEnd(len + 3) + it.second)
- }
-
- }
-
- private fun showVersion(): Nothing {
- println("${programVersion.displayName} ${programVersion.fullVersion}, ${programVersion.copyright}\n")
- exitProcess(0)
- }
-
-}
diff --git a/src/nativeMain/kotlin/net/keinekekse/args/impl.native.kt b/src/nativeMain/kotlin/net/keinekekse/args/impl.native.kt
new file mode 100644
index 0000000..aba074d
--- /dev/null
+++ b/src/nativeMain/kotlin/net/keinekekse/args/impl.native.kt
@@ -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()
+ 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
+}
diff --git a/src/wasmWasiMain/kotlin/net/keinekekse/args/impl.wasmWasi.kt b/src/wasmWasiMain/kotlin/net/keinekekse/args/impl.wasmWasi.kt
new file mode 100644
index 0000000..71e7ed7
--- /dev/null
+++ b/src/wasmWasiMain/kotlin/net/keinekekse/args/impl.wasmWasi.kt
@@ -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")
+}
diff --git a/src/webMain/kotlin/net/keinekekse/args/impl.web.kt b/src/webMain/kotlin/net/keinekekse/args/impl.web.kt
new file mode 100644
index 0000000..024b93a
--- /dev/null
+++ b/src/webMain/kotlin/net/keinekekse/args/impl.web.kt
@@ -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")
+}