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") +}