Compare commits
16
Commits
907e8c7770
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8598ead36 | ||
|
|
09a000239c | ||
|
|
c5b7dbab8b | ||
|
|
561fe601a9 | ||
|
|
66244db097 | ||
|
|
b580e64e76 | ||
|
|
9df3e0701d | ||
|
|
02db9910ff | ||
|
|
fdde00b40a | ||
|
|
9f051d57e2 | ||
|
|
4a73301eb3 | ||
|
|
1ee8dd356f | ||
|
|
3ec788db13 | ||
|
|
22e1e04673 | ||
|
|
d2948e500c | ||
|
|
8aa418c0f1 |
No files matched your search
+18
-4
@@ -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/
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-js-0.2.8">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-js-0.2.8.jar">
|
||||
<element id="module-output" name="util.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-js-0.3.0-beta.1">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-js-0.3.0-beta.1.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-js-0.3.0-beta.11">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-js-0.3.0-beta.11.jar">
|
||||
<element id="module-output" name="util.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-js-0.3.0-beta.19">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-js-0.3.0-beta.19.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-js-0.3.0-beta.2">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-js-0.3.0-beta.2.jar">
|
||||
<element id="module-output" name="util.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-js-0.3.0-beta.4">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-js-0.3.0-beta.4.jar">
|
||||
<element id="module-output" name="util.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-js-0.3.0-beta.8">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-js-0.3.0-beta.8.jar">
|
||||
<element id="module-output" name="util.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-jvm-0.2.8">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-jvm-0.2.8.jar">
|
||||
<element id="module-output" name="util.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-jvm-0.3.0-beta.1">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-jvm-0.3.0-beta.1.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-jvm-0.3.0-beta.11">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-jvm-0.3.0-beta.11.jar">
|
||||
<element id="module-output" name="util.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-jvm-0.3.0-beta.19">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-jvm-0.3.0-beta.19.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-jvm-0.3.0-beta.2">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-jvm-0.3.0-beta.2.jar">
|
||||
<element id="module-output" name="util.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-jvm-0.3.0-beta.4">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-jvm-0.3.0-beta.4.jar">
|
||||
<element id="module-output" name="util.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-jvm-0.3.0-beta.8">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-jvm-0.3.0-beta.8.jar">
|
||||
<element id="module-output" name="util.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-js-0.2.8">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-js-0.2.8.jar">
|
||||
<element id="module-output" name="util.wasmJsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-js-0.3.0-beta.1">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-js-0.3.0-beta.1.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-js-0.3.0-beta.11">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-js-0.3.0-beta.11.jar">
|
||||
<element id="module-output" name="util.wasmJsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-js-0.3.0-beta.19">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-js-0.3.0-beta.19.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-js-0.3.0-beta.2">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-js-0.3.0-beta.2.jar">
|
||||
<element id="module-output" name="util.wasmJsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-js-0.3.0-beta.4">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-js-0.3.0-beta.4.jar">
|
||||
<element id="module-output" name="util.wasmJsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-js-0.3.0-beta.8">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-js-0.3.0-beta.8.jar">
|
||||
<element id="module-output" name="util.wasmJsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-wasi-0.2.8">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-wasi-0.2.8.jar">
|
||||
<element id="module-output" name="util.wasmWasiMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.1">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-wasi-0.3.0-beta.1.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.11">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-wasi-0.3.0-beta.11.jar">
|
||||
<element id="module-output" name="util.wasmWasiMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.19">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-wasi-0.3.0-beta.19.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.2">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-wasi-0.3.0-beta.2.jar">
|
||||
<element id="module-output" name="util.wasmWasiMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.4">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-wasi-0.3.0-beta.4.jar">
|
||||
<element id="module-output" name="util.wasmWasiMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="util-wasm-wasi-0.3.0-beta.8">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="util-wasm-wasi-0.3.0-beta.8.jar">
|
||||
<element id="module-output" name="util.wasmWasiMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module version="4">
|
||||
<component name="AdditionalModuleElements">
|
||||
<content url="file://$MODULE_DIR$/../../src/jvmMain" dumb="true">
|
||||
<sourceFolder url="file://$MODULE_DIR$/../../src/jvmMain/kotlin" isTestSource="false" />
|
||||
</content>
|
||||
</component>
|
||||
</module>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+98
-22
@@ -1,10 +1,12 @@
|
||||
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
//@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
@file:OptIn(ExperimentalWasmDsl::class)
|
||||
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version masterLibs.versions.kotlinVersion
|
||||
`java-library`
|
||||
kotlin("multiplatform") version masterLibs.versions.kotlinVersion
|
||||
// `java-library`
|
||||
`maven-publish`
|
||||
alias(masterLibs.plugins.dokkaPlugin)
|
||||
alias(masterLibs.plugins.versionPlugin)
|
||||
@@ -14,29 +16,103 @@ group = "net.keinekekse"
|
||||
version = masterLibs.versions.utilLibraryVersion.get()
|
||||
description = "Kotlin Utility Library"
|
||||
extra["displayname"] = "Utility Library"
|
||||
extra["copyright"] = "Copyright (c) 2025 Hartmut Bauer"
|
||||
extra["copyright"] = "Copyright (c) 2025-2026 Hartmut Bauer"
|
||||
|
||||
repositories {
|
||||
maven("https://nexus.keinekekse.net/repository/maven-public/")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(masterLibs.kotlinxSerialization)
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(masterLibs.versions.javaVersion.get()))
|
||||
}
|
||||
withSourcesJar()
|
||||
}
|
||||
//dependencies {
|
||||
// implementation(masterLibs.kotlinxSerialization)
|
||||
// // f. cmd line via annotation KMP?
|
||||
// implementation(kotlin("reflect"))
|
||||
//
|
||||
//}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(masterLibs.versions.javaVersion.get().toInt())
|
||||
// compilerOptions {
|
||||
// languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3)
|
||||
// explicitApi()
|
||||
|
||||
jvm {
|
||||
compilerOptions.jvmTarget =
|
||||
JvmTarget.valueOf("JVM_" + masterLibs.versions.javaVersion.get()) //JvmTarget.JVM_1_8
|
||||
}
|
||||
|
||||
js {
|
||||
browser {
|
||||
testTask {
|
||||
useKarma {
|
||||
useChromeHeadless()
|
||||
}
|
||||
}
|
||||
}
|
||||
nodejs()
|
||||
}
|
||||
|
||||
wasmJs {
|
||||
browser {
|
||||
testTask {
|
||||
useKarma {
|
||||
useChromeHeadless()
|
||||
}
|
||||
}
|
||||
}
|
||||
nodejs()
|
||||
}
|
||||
|
||||
wasmWasi {
|
||||
nodejs()
|
||||
}
|
||||
|
||||
// macosX64()
|
||||
macosArm64()
|
||||
|
||||
// iosX64()
|
||||
// iosArm64()
|
||||
// iosSimulatorArm64()
|
||||
|
||||
// watchosArm32()
|
||||
// watchosArm64()
|
||||
// watchosX64()
|
||||
// watchosSimulatorArm64()
|
||||
// watchosDeviceArm64()
|
||||
|
||||
// tvosArm64()
|
||||
// tvosX64()
|
||||
// tvosSimulatorArm64()
|
||||
|
||||
// mingwX64()
|
||||
|
||||
linuxX64()
|
||||
// linuxArm64()
|
||||
|
||||
// androidNativeArm32()
|
||||
// androidNativeArm64()
|
||||
// androidNativeX86()
|
||||
// androidNativeX64()
|
||||
|
||||
applyDefaultHierarchyTemplate()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(masterLibs.kotlinxSerialization)
|
||||
}
|
||||
jvmMain.dependencies {
|
||||
implementation(kotlin("reflect"))
|
||||
}
|
||||
|
||||
// commonTest.dependencies {
|
||||
// implementation(libs.kotlin.test)
|
||||
// implementation(libs.serialization.json)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
//kotlin {
|
||||
// jvmToolchain(masterLibs.versions.javaVersion.get().toInt())
|
||||
//// compilerOptions {
|
||||
//// languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3)
|
||||
//// }
|
||||
//}
|
||||
|
||||
/* Generierte Dokka-Doku als javadoc.jar ausgeben (s.a. "publishing")
|
||||
tasks.register<Jar>("dokkaJar") {
|
||||
@@ -59,10 +135,10 @@ publishing {
|
||||
url = uri("https://nexus.keinekekse.net/repository/maven-releases/")
|
||||
}
|
||||
}
|
||||
publications.create<MavenPublication>("maven") {
|
||||
from(components["java"])
|
||||
// artifact(tasks["dokkaJar"])
|
||||
}
|
||||
// publications.create<MavenPublication>("maven") {
|
||||
// from(components["java"])
|
||||
//// artifact(tasks["dokkaJar"])
|
||||
// }
|
||||
}
|
||||
|
||||
// ./gradlew dokkaGenerate
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#auto generated
|
||||
#Sat Dec 20 00:41:12 CET 2025
|
||||
build.number=103
|
||||
#Mon Jan 19 22:42:11 CET 2026
|
||||
build.number=130
|
||||
+6
-1
@@ -1,3 +1,8 @@
|
||||
#Kotlin
|
||||
kotlin.code.style=official
|
||||
org.gradle.configuration-cache=true
|
||||
kotlin.daemon.jvmargs=-Xmx3072M
|
||||
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
|
||||
#Gradle
|
||||
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
+1
-1
@@ -19,7 +19,7 @@ dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
create("masterLibs") {
|
||||
// from(files("gradle/masterlibs.versions.toml"))
|
||||
from("net.keinekekse:version-catalog:100")
|
||||
from("net.keinekekse:version-catalog:128")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
package net.keinekekse.args
|
||||
|
||||
import net.keinekekse.util.VERSION
|
||||
import net.keinekekse.version.Version
|
||||
|
||||
/**
|
||||
* Terminates the currently running application with the specified exit code.
|
||||
* This function does not return as it stops the application process.
|
||||
*
|
||||
* Works only for JVM and macOS-/Linux-native targets.
|
||||
*
|
||||
* @param rc The exit code to return when the application terminates. Defaults to 0,
|
||||
* indicating normal termination.
|
||||
* @return This function does not return as the application is terminated.
|
||||
*/
|
||||
expect fun exitApplication(rc: Int = 0): Nothing
|
||||
|
||||
/**
|
||||
* Checks if standard input is ready for reading.
|
||||
* This function is used to determine if there is data available to be read from standard input.
|
||||
*
|
||||
* Works only for JVM and macOS-/Linux-native targets.
|
||||
*
|
||||
* @return True if standard input is ready for reading, false otherwise.
|
||||
*/
|
||||
expect fun isStdInputReady(): Boolean
|
||||
|
||||
class VerifyParameterException(message: String) : RuntimeException(message)
|
||||
|
||||
/**
|
||||
* Throws a [VerifyParameterException] with the specified error message.
|
||||
* This function is used to signal that there was an error during argument parsing.
|
||||
*
|
||||
* For use in the application. The exception will be caught by the command-line parser
|
||||
* and displayed to the user together with the help information.
|
||||
*
|
||||
* @param msg The error message to include in the exception.
|
||||
* @throws VerifyParameterException with the provided error message.
|
||||
*/
|
||||
fun showParsingError(msg: String): Nothing {
|
||||
throw VerifyParameterException(msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface representing a runnable program execution logic.
|
||||
*
|
||||
* This interface allows defining a lambda function or block of code that
|
||||
* processes a list of remaining arguments. It is intended to be used as
|
||||
* a mechanism to encapsulate the execution logic for commands or subcommands
|
||||
* in a command-line parsing framework.
|
||||
*
|
||||
* The primary use case for this interface is to define execution behavior
|
||||
* for subcommands or global commands, commonly used in CLI applications.
|
||||
*
|
||||
* @constructor A single abstract method is provided to execute the specified logic.
|
||||
*/
|
||||
fun interface RunProgram {
|
||||
fun run(remaining: List<String>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a subcommand that can be added to a command-line argument parser.
|
||||
*
|
||||
* A `SubCommand` is used to define and configure subcommands in a CLI application,
|
||||
* allowing for structured argument parsing and execution of specific logic based
|
||||
* on the subcommand invoked. It provides mechanisms to add parameters to the
|
||||
* subcommand, define execution logic, and manage case sensitivity for argument
|
||||
* names.
|
||||
*
|
||||
* @property obj Internal data container for subcommand-specific configuration and parameters.
|
||||
* @property ignoreCase Defines whether argument names are case-insensitive. Defaults to `false`.
|
||||
*/
|
||||
class SubCommand private constructor(
|
||||
internal val obj: SubCommandData,
|
||||
val ignoreCase: Boolean = false,
|
||||
) : AddParameter {
|
||||
companion object {
|
||||
internal fun create(obj: SubCommandData, ignoreCase: Boolean = false): SubCommand =
|
||||
SubCommand(obj, ignoreCase)
|
||||
}
|
||||
|
||||
override fun addStringParameter(
|
||||
long: String,
|
||||
short: Char?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterValue<String> {
|
||||
return createParameter(ParameterType.STRING, long, short, description, isMandatory)
|
||||
}
|
||||
|
||||
override fun addLongParameter(
|
||||
long: String,
|
||||
short: Char?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterValue<Long> {
|
||||
return createParameter(ParameterType.LONG, long, short, description, isMandatory)
|
||||
}
|
||||
|
||||
override fun addBooleanParameter(
|
||||
long: String,
|
||||
short: Char?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterValue<Boolean> {
|
||||
return createParameter(ParameterType.BOOLEAN, long, short, description, isMandatory)
|
||||
}
|
||||
|
||||
override fun setExecution(run: RunProgram) {
|
||||
obj.setExecution(run)
|
||||
}
|
||||
|
||||
private fun <T> createParameter(
|
||||
type: ParameterType<T>,
|
||||
long: String,
|
||||
short: Char?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterValue<T> {
|
||||
val processedLong = long.handleCase(ignoreCase)
|
||||
val processedShort = short?.toString()?.handleCase(ignoreCase)
|
||||
|
||||
return runCatching {
|
||||
type.createOption(processedLong, processedShort, description, isMandatory).apply {
|
||||
verifyParameter(this)
|
||||
registerParameter(this, processedLong, processedShort)
|
||||
}.prop
|
||||
}.getOrElse { showDefiningError(it.message ?: "") }
|
||||
}
|
||||
|
||||
private fun registerParameter(parameter: ParameterData<*>, long: String, short: String?) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val data = parameter as ParameterData<Any>
|
||||
obj.params[long] = data
|
||||
short?.let { obj.params[it] = data }
|
||||
}
|
||||
|
||||
private fun verifyParameter(parameter: ParameterData<*>) {
|
||||
val longName = parameter.long
|
||||
if (!NAME_PATTERN.matches(longName)) {
|
||||
throw ArgumentParserException.invalidName(longName)
|
||||
}
|
||||
if (obj.params.containsKey(longName)) {
|
||||
throw ArgumentParserException.optionExists(longName)
|
||||
}
|
||||
|
||||
parameter.short?.let { shortName ->
|
||||
if (!SHORT_NAME_PATTERN.matches(shortName)) {
|
||||
throw ArgumentParserException.invalidShortName(shortName)
|
||||
}
|
||||
if (obj.params.containsKey(shortName)) {
|
||||
throw ArgumentParserException.optionExists(shortName)
|
||||
}
|
||||
}
|
||||
|
||||
val lowerLong = longName.lowercase()
|
||||
if (lowerLong == "help" || lowerLong == "version") {
|
||||
throw ArgumentParserException.noHelpOrVersion()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a command-line interface for parsing and executing commands, subcommands, and parameters.
|
||||
*
|
||||
* This class facilitates defining global parameters and subcommands, parsing provided arguments,
|
||||
* validating inputs, and executing the corresponding actions. It supports various types of parameters,
|
||||
* including strings, long integers, and booleans, with both optional and mandatory requirements.
|
||||
* Additionally, it provides options for ignoring case sensitivity and handling remaining parameters.
|
||||
*
|
||||
* @param programVersion The version information of the program, including display name and copyright.
|
||||
* @param ignoreCase Determines whether the parsing should ignore case sensitivity for commands and parameters.
|
||||
* @param remainingDescription The description for remaining command-line arguments, or null if no arguments are allowed.
|
||||
* @param emptyCommandLineAllowed Indicates whether the parser should allow an empty command line to be parsed. Defaults to false.
|
||||
*
|
||||
* ```
|
||||
* // global parameters
|
||||
*
|
||||
* val commandLine = CommandLine(
|
||||
* programVersion = VERSION,
|
||||
* remainingDescription = "EPUB-Dateien",
|
||||
* ignoreCase = true,
|
||||
* )
|
||||
*
|
||||
* val param1 by commandLine.addStringParameter(
|
||||
* long = "param",
|
||||
* short = 'P',
|
||||
* description = "parameter description"
|
||||
* )
|
||||
* // more global parameters
|
||||
*
|
||||
* commandLine.setExecution { additionalParameters: List<String> ->
|
||||
* // called when no subcommand is specified
|
||||
* }
|
||||
*
|
||||
* // subcommand
|
||||
*
|
||||
* val commandSubCommand = commandLine.addSubCommand(
|
||||
* "command",
|
||||
* "My custom command",
|
||||
* listOf("cmd") // aliases
|
||||
* )
|
||||
*
|
||||
* val par1 by commandSubCommand.addStringParameter(
|
||||
* long = "par1",
|
||||
* description = "Parameter Par1 ",
|
||||
* isMandatory = true
|
||||
* )
|
||||
* // more parameters
|
||||
*
|
||||
* commandSubCommand.setExecution { additionalParameters ->
|
||||
* // called when this subcommand is found in the command line
|
||||
* }
|
||||
*
|
||||
* // more subcommands
|
||||
*
|
||||
* // start program
|
||||
* commandLine.parseAndRun(*args)
|
||||
* ```
|
||||
*/
|
||||
class CommandLine(
|
||||
val programVersion: Version,
|
||||
val ignoreCase: Boolean = false,
|
||||
val remainingDescription: String? = null, // null -> no remaining allowed
|
||||
val emptyCommandLineAllowed: Boolean = false,
|
||||
) : AddParameter {
|
||||
private var hasRun = false
|
||||
private val globalSubCommand = SubCommand.create(SubCommandData(), ignoreCase)
|
||||
private var subCommands = mutableMapOf<String, SubCommandData>()
|
||||
|
||||
override fun addStringParameter(
|
||||
long: String,
|
||||
short: Char?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterValue<String> {
|
||||
return globalSubCommand.addStringParameter(long, short, description, isMandatory)
|
||||
}
|
||||
|
||||
override fun addLongParameter(
|
||||
long: String,
|
||||
short: Char?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterValue<Long> {
|
||||
return globalSubCommand.addLongParameter(long, short, description, isMandatory)
|
||||
}
|
||||
|
||||
override fun addBooleanParameter(
|
||||
long: String,
|
||||
short: Char?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterValue<Boolean> {
|
||||
return globalSubCommand.addBooleanParameter(long, short, description, isMandatory)
|
||||
}
|
||||
|
||||
override fun setExecution(run: RunProgram) {
|
||||
globalSubCommand.setExecution(run)
|
||||
}
|
||||
|
||||
fun addSubCommand(
|
||||
name: String,
|
||||
description: String,
|
||||
aliases: List<String> = emptyList(),
|
||||
): SubCommand = runCatching {
|
||||
val normalizedName = normalizeAndVerifySubCommand(name, ignoreCase)
|
||||
val subData = SubCommandData(normalizedName, description, aliases)
|
||||
subCommands[normalizedName] = subData
|
||||
aliases.forEach { alias ->
|
||||
val alias = normalizeAndVerifySubCommand(alias, ignoreCase)
|
||||
subCommands[alias] = subData
|
||||
}
|
||||
SubCommand.create(subData, ignoreCase)
|
||||
}.getOrElse { showDefiningError(it.message ?: "") }
|
||||
|
||||
fun parseAndRun(vararg args: String) {
|
||||
runCatching {
|
||||
val selectedSubCommand = parse(args.toList())
|
||||
selectedSubCommand.execute(selectedSubCommand.additionalParameters)
|
||||
}.onFailure { exception ->
|
||||
handleErrorAndShowHelp(exception as Exception)
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeAndVerifySubCommand(cmd: String, ignoreCase: Boolean): String {
|
||||
val normalized = cmd.handleCase(ignoreCase)
|
||||
if (!matchSubCommand(normalized)) throw ArgumentParserException.invalidCommand(normalized)
|
||||
return normalized
|
||||
}
|
||||
|
||||
private fun parse(args: List<String>): SubCommandData {
|
||||
if (hasRun) throw ArgumentParserException.noMoreParsing()
|
||||
hasRun = true
|
||||
if (args.isEmpty() && !emptyCommandLineAllowed) throw ArgumentParserException.commandRequired()
|
||||
checkExecutors()
|
||||
|
||||
val remainingAfterGlobal = parseParameters(args, globalSubCommand.obj)
|
||||
validateMandatoryParameters(globalSubCommand.obj)
|
||||
|
||||
val resultSubCommand = parseSubCommand(remainingAfterGlobal)
|
||||
validateMandatoryParameters(resultSubCommand)
|
||||
|
||||
if (remainingDescription != null) {
|
||||
resultSubCommand.additionalParameters += resolveRemainingFromPipe()
|
||||
} else if (resultSubCommand.additionalParameters.isNotEmpty()) {
|
||||
throw ArgumentParserException.noMoreOptions()
|
||||
}
|
||||
return resultSubCommand
|
||||
}
|
||||
|
||||
private fun resolveRemainingFromPipe(): List<String> {
|
||||
return if (isStdInputReady()) getRemainingParametersFromPipe() else emptyList()
|
||||
}
|
||||
|
||||
private fun parseSubCommand(args: List<String>): SubCommandData {
|
||||
val firstArg = args.firstOrNull()?.handleCase(ignoreCase)
|
||||
val sub = subCommands[firstArg] ?: return globalSubCommand.obj.apply {
|
||||
additionalParameters = args
|
||||
}
|
||||
return sub.apply { additionalParameters = parseParameters(args.drop(1), this) }
|
||||
}
|
||||
|
||||
private fun validateMandatoryParameters(subCommand: SubCommandData) {
|
||||
subCommand.params.filter { it.key == it.value.long }.forEach { (_, parameter) ->
|
||||
if (parameter.isMandatory && parameter.prop.value == null) {
|
||||
throw ArgumentParserException.optionRequired(parameter.long)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseParameters(args: List<String>, subCommand: SubCommandData): List<String> {
|
||||
var index = 0
|
||||
while (index < args.size) {
|
||||
val (key, inlineValue) = matchArgument(args[index]) ?: return args.drop(index)
|
||||
|
||||
checkForHelpOrVersion(key)
|
||||
val normalizedKey = key.handleCase(ignoreCase)
|
||||
val param =
|
||||
subCommand.params[normalizedKey] ?: throw ArgumentParserException.unknownOption(
|
||||
normalizedKey
|
||||
)
|
||||
|
||||
val (value, consumedExtra) = resolveParameterValue(
|
||||
args,
|
||||
index,
|
||||
normalizedKey,
|
||||
inlineValue,
|
||||
param
|
||||
)
|
||||
param.type.convertAndSet(value, param.prop)
|
||||
index += 1 + consumedExtra
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun resolveParameterValue(
|
||||
args: List<String>,
|
||||
currentIndex: Int,
|
||||
key: String,
|
||||
inlineValue: String,
|
||||
param: ParameterData<Any>,
|
||||
): Pair<String, Int> {
|
||||
return when {
|
||||
param.type == ParameterType.BOOLEAN -> {
|
||||
if (inlineValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key)
|
||||
TRUE to 0
|
||||
}
|
||||
|
||||
// --name=--irgendwas ist OK
|
||||
inlineValue.isNotBlank() -> inlineValue to 0
|
||||
|
||||
// --name --irgendwas ist nicht OK
|
||||
currentIndex < args.size - 1
|
||||
&& !ARGUMENT_PATTERN.matches(args[currentIndex + 1])
|
||||
&& args[currentIndex + 1].isNotBlank() -> {
|
||||
args[currentIndex + 1] to 1
|
||||
}
|
||||
|
||||
else -> throw ArgumentParserException.optionRequiresValue(key)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRemainingParametersFromPipe(): List<String> {
|
||||
if (isStdInputReady()) {
|
||||
val pipe = mutableListOf<String>()
|
||||
while (true) {
|
||||
val input = readlnOrNull() ?: break
|
||||
pipe.add(input)
|
||||
}
|
||||
return pipe
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun checkExecutors() {
|
||||
globalSubCommand.obj.checkExecutors()
|
||||
subCommands.values.forEach { it.checkExecutors() }
|
||||
}
|
||||
|
||||
private fun checkForHelpOrVersion(key: String) {
|
||||
val k = key.lowercase()
|
||||
if (k == "help") {
|
||||
throw ArgumentParserException.showHelp()
|
||||
} else if (k == "version") {
|
||||
showVersion()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleErrorAndShowHelp(
|
||||
e: Exception?,
|
||||
): Nothing {
|
||||
val hasSubCommands = subCommands.isNotEmpty()
|
||||
val hasGlobalParams = globalSubCommand.obj.params.isNotEmpty()
|
||||
val allowsRemaining = remainingDescription != null
|
||||
|
||||
println("\n${programVersion.displayName} ${programVersion.version} ${programVersion.copyright}\n")
|
||||
println("${programVersion.description}\n")
|
||||
|
||||
val usagePrefix = "usage: ${programVersion.name}"
|
||||
val globalPart =
|
||||
if (hasGlobalParams) "[${if (hasSubCommands) "GLOBAL " else ""}PARAMETER]" else ""
|
||||
val subPart = if (hasSubCommands) " subcommand [PARAMETER]" else ""
|
||||
val remainPart = if (allowsRemaining) " <$remainingDescription>" else ""
|
||||
|
||||
println("$usagePrefix $globalPart$subPart$remainPart\n")
|
||||
if (hasGlobalParams) {
|
||||
println("${if (hasSubCommands) "Global " else ""}Parameters:")
|
||||
}
|
||||
showParameters(globalSubCommand.obj.params)
|
||||
if (hasSubCommands) {
|
||||
println()
|
||||
println("Subcommands:")
|
||||
showSubcommands(subCommands)
|
||||
}
|
||||
e?.message?.let {
|
||||
println()
|
||||
println(ArgumentParserException.PARSING_ERROR_MSG + it + "\n")
|
||||
}
|
||||
println()
|
||||
exitApplication(0)
|
||||
}
|
||||
|
||||
private fun showSubcommands(parameters: MutableMap<String, SubCommandData>) {
|
||||
parameters.filter { it.key == it.value.name }.forEach { (_, param) ->
|
||||
val aliases = buildString {
|
||||
param.aliases.forEach { alias ->
|
||||
append(" | $alias")
|
||||
}
|
||||
}
|
||||
println("$INDENT${param.name}$aliases ${param.description}")
|
||||
showParameters(param.params)
|
||||
println()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showParameters(parameters: MutableMap<String, ParameterData<Any>>) {
|
||||
var len = 0
|
||||
parameters.filter { it.key == it.value.long }.map { (_, param) ->
|
||||
val parmStr = INDENT + INDENT +
|
||||
(param.short?.let { "-$it, " } ?: " ") +
|
||||
"--${param.long}" + if (param.type != ParameterType.BOOLEAN) " <...>" else ""
|
||||
len = maxOf(len, parmStr.length)
|
||||
Pair(parmStr, "${param.description} ${if (param.isMandatory) "*" else ""}")
|
||||
}.forEach {
|
||||
println(it.first.padEnd(len + 3) + it.second)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showVersion(): Nothing {
|
||||
println("\n${programVersion.displayName} ${programVersion.fullVersion} ${programVersion.copyright}\n\n(${VERSION.displayName} ${VERSION.fullVersion} ${VERSION.copyright})\n")
|
||||
exitApplication(0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package net.keinekekse.args
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface ValueDelegate<T> {
|
||||
var value: T?
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = value
|
||||
|
||||
fun updateValue(value: T?) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
fun isEmpty() = value == null // needed?
|
||||
}
|
||||
|
||||
interface MutableValueDelegate<T> : ValueDelegate<T> {
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
class ParameterValue<T> : ValueDelegate<T> {
|
||||
override var value: T? = null
|
||||
}
|
||||
|
||||
internal sealed class ParameterType<T> {
|
||||
|
||||
abstract fun createOption(
|
||||
long: String,
|
||||
short: String?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterData<T>
|
||||
|
||||
abstract fun convertAndSet(value: String, opt: ValueDelegate<T>)
|
||||
|
||||
object STRING : ParameterType<String>() {
|
||||
override fun createOption(
|
||||
long: String,
|
||||
short: String?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterData<String> {
|
||||
return ParameterData(long, short, description, isMandatory, ParameterValue(), this)
|
||||
}
|
||||
|
||||
override fun convertAndSet(value: String, opt: ValueDelegate<String>) {
|
||||
opt.updateValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
object LONG : ParameterType<Long>() {
|
||||
override fun createOption(
|
||||
long: String,
|
||||
short: String?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterData<Long> {
|
||||
return ParameterData(long, short, description, isMandatory, ParameterValue(), this)
|
||||
}
|
||||
|
||||
override fun convertAndSet(value: String, opt: ValueDelegate<Long>) {
|
||||
opt.updateValue(
|
||||
value.trim().toLongOrNull() ?: throw ArgumentParserException.invalidLongValue(value)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object BOOLEAN : ParameterType<Boolean>() {
|
||||
override fun createOption(
|
||||
long: String,
|
||||
short: String?,
|
||||
description: String,
|
||||
isMandatory: Boolean,
|
||||
): ParameterData<Boolean> {
|
||||
return ParameterData(
|
||||
long,
|
||||
short,
|
||||
description,
|
||||
false,
|
||||
ParameterValue<Boolean>().apply { updateValue(false) },
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertAndSet(value: String, opt: ValueDelegate<Boolean>) {
|
||||
opt.updateValue(value == TRUE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal data class SubCommandData(
|
||||
val name: String = "",
|
||||
val description: String = "",
|
||||
val aliases: List<String> = emptyList(),
|
||||
val params: MutableMap<String, ParameterData<Any>> = mutableMapOf(),
|
||||
var additionalParameters: List<String> = emptyList<String>(),
|
||||
) {
|
||||
private var exec: RunProgram = {}
|
||||
private var isSet: Boolean = false
|
||||
|
||||
fun setExecution(run: RunProgram) {
|
||||
this.exec = run
|
||||
isSet = true
|
||||
}
|
||||
|
||||
fun checkExecutors() {
|
||||
if (!isSet) throw ArgumentParserException.noExecutorDefined(name.ifEmpty { null }
|
||||
?: "global parameters")
|
||||
}
|
||||
|
||||
fun execute(remaining: List<String>) {
|
||||
exec.run(remaining)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class ParameterData<T>(
|
||||
val long: String,
|
||||
val short: String? = null,
|
||||
val description: String = "",
|
||||
val isMandatory: Boolean = false,
|
||||
val prop: ParameterValue<T>,
|
||||
val type: ParameterType<T>,
|
||||
)
|
||||
|
||||
interface AddParameter {
|
||||
fun addStringParameter(
|
||||
long: String,
|
||||
short: Char? = null,
|
||||
description: String,
|
||||
isMandatory: Boolean = false,
|
||||
): ParameterValue<String>
|
||||
|
||||
fun addLongParameter(
|
||||
long: String,
|
||||
short: Char? = null,
|
||||
description: String,
|
||||
isMandatory: Boolean = false,
|
||||
): ParameterValue<Long>
|
||||
|
||||
fun addBooleanParameter(
|
||||
long: String,
|
||||
short: Char? = null,
|
||||
description: String,
|
||||
isMandatory: Boolean = false,
|
||||
): ParameterValue<Boolean>
|
||||
|
||||
fun setExecution(run: RunProgram)
|
||||
}
|
||||
|
||||
// Fehler beim Check der Parameter-Definitionen und beim Parsen der Kommandozeile
|
||||
internal class ArgumentParserException(message: String?) : RuntimeException(message) {
|
||||
companion object {
|
||||
const val PARSING_ERROR_MSG = "\n*** ERROR parsing arguments: "
|
||||
const val DEFINITION_ERROR_MSG = "\n***\n*** ERROR defining arguments in source code: "
|
||||
fun showHelp() = ArgumentParserException(null)
|
||||
fun noHelpOrVersion() =
|
||||
ArgumentParserException("Option 'help' and option 'version' are not allowed.")
|
||||
|
||||
fun noExecutorDefined(name: String?) =
|
||||
ArgumentParserException("No lambda found for '$name'.")
|
||||
|
||||
fun optionExists(name: String) = ArgumentParserException("Option '$name' already exists.")
|
||||
|
||||
fun unknownOption(name: String) = ArgumentParserException("Unknown option '$name'.")
|
||||
fun optionRequiresValue(name: String) =
|
||||
ArgumentParserException("Option '$name' requires a value.")
|
||||
|
||||
fun optionNoValue(name: String) =
|
||||
ArgumentParserException("Option '$name' does not accept a value.")
|
||||
|
||||
fun optionRequired(name: String) = ArgumentParserException("Option '$name' is required.")
|
||||
fun commandRequired() = ArgumentParserException("At least one command is required.")
|
||||
fun invalidLongValue(value: String) =
|
||||
ArgumentParserException("Value '$value' is not a long integer.")
|
||||
|
||||
fun invalidShortName(name: String) =
|
||||
ArgumentParserException("Option short name '$name' is invalid.")
|
||||
|
||||
fun invalidName(name: String) = ArgumentParserException("Option name '$name' is invalid.")
|
||||
fun invalidCommand(name: String) =
|
||||
ArgumentParserException("Command name '$name' is invalid.")
|
||||
|
||||
fun noMoreOptions() = ArgumentParserException("Too many arguments.")
|
||||
fun noMoreParsing() = ArgumentParserException("Arguments have already been parsed.")
|
||||
}
|
||||
}
|
||||
|
||||
internal const val INDENT = " "
|
||||
internal const val TRUE = "true"
|
||||
|
||||
internal const val NAME = """[a-zA-Z][\w-]+"""
|
||||
internal const val SHORT_NAME = """[a-zA-Z]"""
|
||||
internal val ARGUMENT_PATTERN = """^(?:-(${SHORT_NAME})|--(${NAME}))(?:[=:](.*)|$)$""".toRegex()
|
||||
internal val NAME_PATTERN = """^${NAME}$""".toRegex()
|
||||
internal val SHORT_NAME_PATTERN = """^${SHORT_NAME}$""".toRegex()
|
||||
|
||||
internal fun String.handleCase(ignoreCase: Boolean): String {
|
||||
return if (ignoreCase) lowercase() else this
|
||||
}
|
||||
|
||||
internal fun matchSubCommand(cmd: String) =
|
||||
NAME_PATTERN.matches(cmd)
|
||||
|
||||
internal fun matchArgument(arg: String): Pair<String, String>? =
|
||||
ARGUMENT_PATTERN.matchEntire(arg)?.groupValues?.let { Pair(it[1] + it[2], it[3]) }
|
||||
|
||||
// Fehler im Quellcode anzeigen
|
||||
internal fun showDefiningError(msg: String): Nothing {
|
||||
println(ArgumentParserException.DEFINITION_ERROR_MSG + msg + "\n")
|
||||
exitApplication(1)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.keinekekse.util
|
||||
|
||||
import net.keinekekse.version.Version
|
||||
|
||||
object VERSION: Version {
|
||||
override val version = "0.3.0"
|
||||
override val fullVersion = "0.3.0+build.130"
|
||||
override val major = 0
|
||||
override val minor = 3
|
||||
override val patch = 0
|
||||
override val preRelease = ""
|
||||
override val buildMetadata = "build.130"
|
||||
override val buildDate = "2026-01-19T21:42:11.032491Z"
|
||||
override val displayName = "Utility Library"
|
||||
override val copyright = "Copyright (c) 2025-2026 Hartmut Bauer"
|
||||
override val name = "util"
|
||||
override val description = "Kotlin Utility Library"
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,16 @@
|
||||
package net.keinekekse.version
|
||||
|
||||
interface Version {
|
||||
val version: String
|
||||
val fullVersion: String
|
||||
val major: Int
|
||||
val minor: Int
|
||||
val patch: Int
|
||||
val preRelease: String
|
||||
val buildMetadata: String
|
||||
val buildDate: String
|
||||
val displayName: String
|
||||
val copyright: String
|
||||
val name: String
|
||||
val description: String
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.keinekekse.args.annotation
|
||||
|
||||
/**
|
||||
* Annotation to define metadata for a command-line argument.
|
||||
*
|
||||
* This annotation can be applied to properties to specify their representation
|
||||
* as command-line arguments. It provides options for defining the argument's
|
||||
* name, shortened single-character alias, description, and whether it is mandatory.
|
||||
*
|
||||
* The property must be declared as mutable and nullable.
|
||||
*
|
||||
* @property name The full name of the command-line argument. Defaults to the name of the property.
|
||||
* @property shortName The single-character alias for the argument. Defaults to [Char.MIN_VALUE],
|
||||
* indicating no short name is defined.
|
||||
* @property description A brief description of the argument's purpose. Defaults to an empty string.
|
||||
* @property isMandatory Specifies if the argument is mandatory. Defaults to `false`.
|
||||
*/
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
annotation class CmdLineArgument(
|
||||
val name: String = "",
|
||||
val shortName: Char = Char.MIN_VALUE,
|
||||
val description: String = "",
|
||||
val isMandatory: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Annotation to define a subcommand for a command-line interface.
|
||||
*
|
||||
* This annotation can be applied to objects to designate them as subcommands
|
||||
* for a command-line tool. It specifies the subcommand's name, a brief description,
|
||||
* and optional aliases that can be used to invoke the subcommand.
|
||||
*
|
||||
* @property name The primary identifier for the subcommand. Defaults to the simple name of the class.
|
||||
* @property description A short description of the subcommand's purpose. Defaults to an empty string.
|
||||
* @property aliases Alternative names that can be used to invoke the subcommand. Defaults to an
|
||||
* empty array.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class CmdLineSubCommand(
|
||||
val name: String = "",
|
||||
val description: String = "",
|
||||
val aliases: Array<String> = [],
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents a set of parameters associated globally with the program.
|
||||
* This class adds additional properties and customization options to manage
|
||||
* the behavior of the command line interpretation, such as allowing additional parameters,
|
||||
* handling case sensitivity, and keeping track of command execution status.
|
||||
*
|
||||
* The parameters have to be annotated with the [CmdLineArgument] annotation.
|
||||
* Subcommands can be defined using the [CmdLineSubCommand] annotation on objects,
|
||||
* derived from the [SubCommand] class.
|
||||
*
|
||||
* @property programDescription A description of the program's purpose or functionality.
|
||||
* @property additionalParametersAllowed Indicates whether the program allows additional,
|
||||
* unspecified parameters during execution.
|
||||
* @property ignoreCase Determines if parameter matching should be case-insensitive.
|
||||
*/
|
||||
abstract class Parameters(
|
||||
val programDescription: String = "",
|
||||
val additionalParametersAllowed: Boolean = true,
|
||||
val ignoreCase: Boolean = false,
|
||||
var didRun: Boolean = false,
|
||||
) : SubCommand()
|
||||
|
||||
/**
|
||||
* Represents an abstract subcommand that can be executed with specific behavior and validation logic.
|
||||
*
|
||||
* Subclasses of this class are expected to provide implementation for validating their parameters
|
||||
* and defining the execution logic of the subcommand. Each subcommand can also handle additional
|
||||
* parameters.
|
||||
*
|
||||
* @property additionalParameters A list of additional command line parameters associated with this
|
||||
* subcommand.
|
||||
*/
|
||||
abstract class SubCommand {
|
||||
var additionalParameters: List<String> = emptyList()
|
||||
|
||||
abstract fun verifyParameters(): Boolean
|
||||
abstract fun run(globals: Parameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the program logic using the specified command-line arguments.
|
||||
* This method internally invokes the `run` method of the `Parameters` class
|
||||
* to parse, validate, and execute the input arguments.
|
||||
*
|
||||
* @param args A list of command-line arguments to be processed by the program.
|
||||
*/
|
||||
fun Parameters.runProgramWith(args: List<String>) {
|
||||
run(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown to indicate that there is an error related to verifying
|
||||
* the validity of command-line parameters provided to the application.
|
||||
*
|
||||
* This exception is thrown by the [SubCommand.verifyParameters] method to indicate
|
||||
* that the parameters provided to the subcommand are invalid.
|
||||
*
|
||||
* The exception is caught by the [Parameters.runProgramWith] method and the program
|
||||
* execution will be terminated with an error message and the usage information.
|
||||
*
|
||||
* @constructor
|
||||
* Constructs a new [VerifyParameterException] with the specified detail message.
|
||||
*
|
||||
* @param message The detail message describing the parameter verification error.
|
||||
*/
|
||||
class VerifyParameterException(message: String) : RuntimeException(message)
|
||||
@@ -0,0 +1,461 @@
|
||||
package net.keinekekse.args.annotation
|
||||
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.hasAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
// entry point
|
||||
fun Parameters.run(args: List<String>) {
|
||||
var allParameters: Map<String, CmdSubCommand> = emptyMap()
|
||||
try {
|
||||
if (didRun) throw ArgumentParserException.noMoreParsing() else didRun = true
|
||||
allParameters = fetchAllParameters()
|
||||
|
||||
val subCommand = parseCommandLine(args, allParameters, ignoreCase)
|
||||
if (subCommand.verifyParameters()) {
|
||||
subCommand.run(this)
|
||||
}
|
||||
} catch (e: ArgumentParserException) {
|
||||
showHelp(e, allParameters, programDescription)
|
||||
} catch (e: VerifyParameterException) {
|
||||
showHelp(e, allParameters, programDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Fehler beim Check der Parameter-Definitionen und beim Parsen der Kommandozeile
|
||||
private class ArgumentParserException(message: String?) : RuntimeException(message) {
|
||||
companion object {
|
||||
const val ERROR_MSG = "\n*** ERROR parsing arguments: %s\n"
|
||||
fun showHelp() = ArgumentParserException(null)
|
||||
fun noHelpOrVersion() =
|
||||
ArgumentParserException("Option 'help' and option 'version' are not allowed.")
|
||||
|
||||
fun noParametersDefined() = ArgumentParserException("No parameters found for command line.")
|
||||
|
||||
fun optionExists(name: String) = ArgumentParserException("Option '$name' already exists.")
|
||||
|
||||
fun invalidAnnotationTargetParameter(name: String) =
|
||||
ArgumentParserException("Annotation @CmdLineParameter can only be used on mutable properties, '$name' isn't a mutable property.")
|
||||
|
||||
fun notNullableAnnotationTargetParameter(name: String) =
|
||||
ArgumentParserException("Annotation @CmdLineParameter can only be used on nullable properties, '$name' isn't a nullable property.")
|
||||
|
||||
fun invalidAnnotationTargetOption() =
|
||||
ArgumentParserException("Annotation @CmdLineOption can only be used on CommandLine Objects.")
|
||||
|
||||
fun optionNameRequired() = ArgumentParserException("Option name is required.")
|
||||
fun unknownOption(name: String) = ArgumentParserException("Unknown option '$name'.")
|
||||
fun unknownCommand(name: String) = ArgumentParserException("Unknown command '$name'.")
|
||||
fun optionRequiresValue(name: String) =
|
||||
ArgumentParserException("Option '$name' requires a value.")
|
||||
|
||||
fun optionNoValue(name: String) =
|
||||
ArgumentParserException("Option '$name' does not accept a value.")
|
||||
|
||||
fun booleanNotMandatory(name: String) =
|
||||
ArgumentParserException("Boolean option '$name' can not be mandatory.")
|
||||
|
||||
fun optionRequired(name: String) = ArgumentParserException("Option '$name' is required.")
|
||||
fun commandRequired() = ArgumentParserException("At least one command is required.")
|
||||
fun invalidLongValue(value: String) =
|
||||
ArgumentParserException("Value '$value' is not a long integer.")
|
||||
|
||||
fun invalidType(name: String, type: String) =
|
||||
ArgumentParserException("Unsupported parameter type: ${type} for parameter $name")
|
||||
|
||||
fun invalidShortName(name: String) =
|
||||
ArgumentParserException("Option short name '$name' is invalid.")
|
||||
|
||||
fun invalidName(name: String) = ArgumentParserException("Option name '$name' is invalid.")
|
||||
fun invalidCommand(name: String) =
|
||||
ArgumentParserException("Command name '$name' is invalid.")
|
||||
|
||||
fun noMoreOptions() = ArgumentParserException("Too many arguments.")
|
||||
fun noMoreParsing() = ArgumentParserException("Arguments have already been parsed.")
|
||||
}
|
||||
}
|
||||
|
||||
private const val GLOBAL_PARAMETERS = "#global#"
|
||||
private const val TRUE = "true"
|
||||
|
||||
private const val NAME = """[a-zA-Z][\w-]+"""
|
||||
private const val SHORT_NAME = """[a-zA-Z]"""
|
||||
private val ARGUMENT_PATTERN = """^(?:-(${SHORT_NAME})|--(${NAME}))(?:=(.+)|$)$""".toRegex()
|
||||
private val NAME_PATTERN = """^${NAME}$""".toRegex()
|
||||
private val SHORT_NAME_PATTERN = """^${SHORT_NAME}$""".toRegex()
|
||||
|
||||
private sealed interface ParameterType {
|
||||
fun convertAndSet(
|
||||
value: String,
|
||||
obj: SubCommand,
|
||||
prop: KMutableProperty1<out SubCommand, *>,
|
||||
)
|
||||
|
||||
object STRING : ParameterType {
|
||||
override fun convertAndSet(
|
||||
value: String,
|
||||
obj: SubCommand,
|
||||
prop: KMutableProperty1<out SubCommand, *>,
|
||||
) {
|
||||
prop.setter.call(obj, value)
|
||||
}
|
||||
}
|
||||
|
||||
object LONG : ParameterType {
|
||||
override fun convertAndSet(
|
||||
value: String,
|
||||
obj: SubCommand,
|
||||
prop: KMutableProperty1<out SubCommand, *>,
|
||||
) {
|
||||
val valueLong =
|
||||
value.trim().toLongOrNull() ?: throw ArgumentParserException.invalidLongValue(value)
|
||||
prop.setter.call(obj, valueLong)
|
||||
}
|
||||
}
|
||||
|
||||
object BOOLEAN : ParameterType {
|
||||
override fun convertAndSet(
|
||||
value: String,
|
||||
obj: SubCommand,
|
||||
prop: KMutableProperty1<out SubCommand, *>,
|
||||
) {
|
||||
val valueBool = value.toBoolean()
|
||||
prop.setter.call(obj, valueBool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private typealias CmdParameterMap = Map<String, CmdParameter>
|
||||
private typealias CmdSubCommandMap = Map<String, CmdSubCommand>
|
||||
|
||||
private data class CmdSubCommand(
|
||||
val option: String = GLOBAL_PARAMETERS,
|
||||
val description: String = "",
|
||||
val aliases: List<String> = emptyList(),
|
||||
val obj: SubCommand,
|
||||
val params: CmdParameterMap,
|
||||
)
|
||||
|
||||
private data class CmdParameter(
|
||||
val long: String,
|
||||
val short: Char? = null,
|
||||
val description: String = "",
|
||||
val isMandatory: Boolean = false,
|
||||
val type: ParameterType,
|
||||
val prop: KMutableProperty1<out SubCommand, *>,
|
||||
)
|
||||
|
||||
// handle annotations
|
||||
|
||||
private fun Parameters.fetchAllParameters(): CmdSubCommandMap {
|
||||
verifyParametersExist()
|
||||
val globalParams = getParameters(ignoreCase)
|
||||
|
||||
return mutableMapOf<String, CmdSubCommand>().apply {
|
||||
put(GLOBAL_PARAMETERS, CmdSubCommand(obj = this@fetchAllParameters, params = globalParams))
|
||||
getSubCommands(this, ignoreCase)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SubCommand.getParameters(ignoreCase: Boolean): CmdParameterMap {
|
||||
val params = mutableMapOf<String, CmdParameter>()
|
||||
this::class.memberProperties
|
||||
.filter { it.hasAnnotation<CmdLineArgument>() }
|
||||
.forEach { property ->
|
||||
val mutableProp = validateProperty(property)
|
||||
|
||||
val param = mutableProp.toCmdParameter(this, ignoreCase)
|
||||
if (params.containsKey(param.long)) throw ArgumentParserException.optionExists(param.long)
|
||||
params[param.long] = param
|
||||
|
||||
param.short?.let {
|
||||
val s = it.toString()
|
||||
if (params.containsKey(s)) throw ArgumentParserException.optionExists(s)
|
||||
params[s] = param
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
private fun KMutableProperty1<out SubCommand, *>.toCmdParameter(
|
||||
instance: SubCommand,
|
||||
ignoreCase: Boolean,
|
||||
): CmdParameter {
|
||||
val annotation = findAnnotation<CmdLineArgument>()!!
|
||||
|
||||
val name = (annotation.name.takeIf { it.isNotBlank() } ?: name).handle(ignoreCase)
|
||||
verifyNoHelpOrVersion(name)
|
||||
if (!NAME_PATTERN.matches(name)) throw ArgumentParserException.invalidName(name)
|
||||
|
||||
val short = annotation.shortName.takeIf { it.isLetter() || it.isDigit() }?.let {
|
||||
if (!SHORT_NAME_PATTERN.matches(it.toString())) {
|
||||
throw ArgumentParserException.invalidShortName(it.toString())
|
||||
}
|
||||
if (ignoreCase) it.lowercaseChar() else it
|
||||
}
|
||||
|
||||
return CmdParameter(
|
||||
long = name,
|
||||
short = short,
|
||||
description = annotation.description,
|
||||
isMandatory = annotation.isMandatory,
|
||||
type = this.determineParameterType(name, annotation.isMandatory, instance),
|
||||
prop = this
|
||||
)
|
||||
}
|
||||
|
||||
private fun KMutableProperty1<out SubCommand, *>.determineParameterType(
|
||||
name: String,
|
||||
isMandatory: Boolean,
|
||||
instance: SubCommand,
|
||||
): ParameterType {
|
||||
return when (returnType.classifier) {
|
||||
String::class -> ParameterType.STRING
|
||||
Long::class -> ParameterType.LONG
|
||||
Boolean::class -> {
|
||||
if (isMandatory) throw ArgumentParserException.booleanNotMandatory(name)
|
||||
ParameterType.BOOLEAN.also { it.convertAndSet("false", instance, this) }
|
||||
}
|
||||
|
||||
else -> throw ArgumentParserException.invalidType(name, returnType.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateProperty(property: KProperty1<out SubCommand, *>): KMutableProperty1<out SubCommand, *> {
|
||||
// 1. mutable
|
||||
val mutableProp = property as? KMutableProperty1<out SubCommand, *>
|
||||
?: throw ArgumentParserException.invalidAnnotationTargetParameter(property.name)
|
||||
// 2. nullable
|
||||
if (!mutableProp.returnType.isMarkedNullable) throw ArgumentParserException.notNullableAnnotationTargetParameter(
|
||||
property.name
|
||||
)
|
||||
// 3. override 'private'
|
||||
mutableProp.isAccessible = true
|
||||
|
||||
return mutableProp
|
||||
}
|
||||
|
||||
private fun SubCommand.getSubCommands(
|
||||
optionList: MutableMap<String, CmdSubCommand>,
|
||||
ignoreCase: Boolean,
|
||||
) {
|
||||
val subCommandClasses =
|
||||
this::class.nestedClasses.filter { it.hasAnnotation<CmdLineSubCommand>() }
|
||||
|
||||
subCommandClasses.forEach { kClass ->
|
||||
val annotation = kClass.findAnnotation<CmdLineSubCommand>()!!
|
||||
val cmdObj = kClass.objectInstance as? SubCommand
|
||||
?: throw ArgumentParserException.invalidAnnotationTargetOption()
|
||||
|
||||
val name = resolveCommandName(kClass, annotation, ignoreCase)
|
||||
val cmdSubCommand = CmdSubCommand(
|
||||
option = name,
|
||||
description = annotation.description,
|
||||
aliases = annotation.aliases.toList().map { it.handle(ignoreCase) },
|
||||
obj = cmdObj,
|
||||
params = cmdObj.getParameters(ignoreCase)
|
||||
)
|
||||
|
||||
optionList[name] = cmdSubCommand
|
||||
annotation.aliases.forEach { alias ->
|
||||
optionList[alias] = cmdSubCommand
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveCommandName(
|
||||
kClass: kotlin.reflect.KClass<*>,
|
||||
annotation: CmdLineSubCommand,
|
||||
ignoreCase: Boolean,
|
||||
): String {
|
||||
val rawName = annotation.name.takeIf { it.isNotBlank() }
|
||||
?: kClass.simpleName
|
||||
?: throw ArgumentParserException.optionNameRequired()
|
||||
|
||||
return rawName.handle(ignoreCase)
|
||||
}
|
||||
|
||||
private fun Parameters.verifyParametersExist() {
|
||||
val me = this::class
|
||||
val hasParam = me.memberProperties.any { it.hasAnnotation<CmdLineArgument>() }
|
||||
val hasSubcommand = me.nestedClasses.any { it.hasAnnotation<CmdLineSubCommand>() }
|
||||
if (!hasParam && !hasSubcommand) {
|
||||
throw ArgumentParserException.noParametersDefined()
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyNoHelpOrVersion(name: String) {
|
||||
val n = name.lowercase()
|
||||
if (n == "help" || n == "version") {
|
||||
throw ArgumentParserException.noHelpOrVersion()
|
||||
}
|
||||
}
|
||||
|
||||
// parse command line arguments
|
||||
|
||||
private fun Parameters.parseCommandLine(
|
||||
args: List<String>,
|
||||
parameters: CmdSubCommandMap,
|
||||
ignoreCase: Boolean,
|
||||
): SubCommand {
|
||||
if (args.isEmpty()) {
|
||||
throw ArgumentParserException.commandRequired()
|
||||
}
|
||||
|
||||
val globals = parameters[GLOBAL_PARAMETERS]!!
|
||||
val remaining = parseParameters(args, globals, ignoreCase)
|
||||
validateMandatoryParameters(globals)
|
||||
|
||||
val resultSubCommand = if (parameters.size > 1) {
|
||||
parseSubCommand(remaining, parameters, ignoreCase)
|
||||
} else {
|
||||
globals.obj.apply { additionalParameters = remaining }
|
||||
}
|
||||
|
||||
if (resultSubCommand.additionalParameters.isNotEmpty() && !additionalParametersAllowed) {
|
||||
throw ArgumentParserException.noMoreOptions()
|
||||
}
|
||||
|
||||
return resultSubCommand
|
||||
}
|
||||
|
||||
private fun parseSubCommand(
|
||||
args: List<String>,
|
||||
parameters: CmdSubCommandMap,
|
||||
ignoreCase: Boolean,
|
||||
): SubCommand {
|
||||
val subCommandData = getSubCommand(args, parameters, ignoreCase)
|
||||
val remainingAfterSub = parseParameters(args.drop(1), subCommandData, ignoreCase)
|
||||
return subCommandData.obj.apply {
|
||||
additionalParameters = remainingAfterSub
|
||||
}
|
||||
}
|
||||
|
||||
// property has to be nullable for this to work
|
||||
private fun validateMandatoryParameters(arguments: CmdSubCommand) {
|
||||
arguments.params.forEach { (_, param) ->
|
||||
if (param.isMandatory && param.prop.getter.call(arguments.obj) == null) {
|
||||
throw ArgumentParserException.optionRequired(param.long)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSubCommand(
|
||||
args: List<String>,
|
||||
parameters: CmdSubCommandMap,
|
||||
ignoreCase: Boolean,
|
||||
): CmdSubCommand {
|
||||
val commandName =
|
||||
args.firstOrNull()?.handle(ignoreCase) ?: throw ArgumentParserException.commandRequired()
|
||||
|
||||
if (!matchSubCommand(commandName)) {
|
||||
throw ArgumentParserException.invalidCommand(commandName)
|
||||
}
|
||||
|
||||
return parameters[commandName] ?: throw ArgumentParserException.unknownCommand(commandName)
|
||||
}
|
||||
|
||||
private fun parseParameters(
|
||||
args: List<String>,
|
||||
option: CmdSubCommand,
|
||||
ignoreCase: Boolean,
|
||||
): List<String> {
|
||||
var index = 0
|
||||
while (index < args.size) {
|
||||
val parseResult = matchArgument(args[index]) ?: return args.drop(index)
|
||||
|
||||
var (key, inlineValue) = parseResult
|
||||
checkForHelpOrVersion(key)
|
||||
key = key.handle(ignoreCase)
|
||||
|
||||
val param = option.params[key] ?: throw ArgumentParserException.unknownOption(key)
|
||||
|
||||
val (value, consumedExtra) = resolveParameterValue(args, index, key, inlineValue, param)
|
||||
param.type.convertAndSet(value, option.obj, param.prop)
|
||||
|
||||
index += 1 + consumedExtra
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun resolveParameterValue(
|
||||
args: List<String>,
|
||||
currentIndex: Int,
|
||||
key: String,
|
||||
parameterValue: String,
|
||||
param: CmdParameter,
|
||||
): Pair<String, Int> {
|
||||
return when {
|
||||
param.type == ParameterType.BOOLEAN -> {
|
||||
if (parameterValue.isNotBlank()) throw ArgumentParserException.optionNoValue(key)
|
||||
TRUE to 0
|
||||
}
|
||||
|
||||
parameterValue.isNotBlank() -> parameterValue to 0
|
||||
currentIndex < args.size - 1 && !ARGUMENT_PATTERN.matches(args[currentIndex + 1]) -> {
|
||||
args[currentIndex + 1] to 1
|
||||
}
|
||||
|
||||
else -> throw ArgumentParserException.optionRequiresValue(key)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.handle(ignoreCase: Boolean): String {
|
||||
return if (ignoreCase) lowercase() else this
|
||||
}
|
||||
|
||||
private fun matchSubCommand(cmd: String) =
|
||||
NAME_PATTERN.matches(cmd)
|
||||
|
||||
private fun matchArgument(arg: String): Pair<String, String>? =
|
||||
ARGUMENT_PATTERN.matchEntire(arg)?.groupValues?.let { Pair(it[1] + it[2], it[3]) }
|
||||
|
||||
// show help
|
||||
|
||||
private fun checkForHelpOrVersion(key: String) {
|
||||
val k = key.lowercase()
|
||||
if (k == "help") {
|
||||
throw ArgumentParserException.showHelp()
|
||||
} else if (k == "version") {
|
||||
showVersion()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showHelp(
|
||||
e: Exception,
|
||||
allParameters: CmdSubCommandMap,
|
||||
programDescription: String,
|
||||
): Nothing {
|
||||
showParameters(allParameters)
|
||||
|
||||
e.message?.let {
|
||||
println()
|
||||
println(ArgumentParserException.ERROR_MSG.format(it))
|
||||
println()
|
||||
}
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun showParameters(parameters: CmdSubCommandMap) {
|
||||
parameters.forEach { (name, param) ->
|
||||
if (name == param.option) {
|
||||
println(" - ${name} -> Option = ${param.option}, desc = ${param.description}")
|
||||
param.params.forEach { (pName, pParam) ->
|
||||
if (pName == pParam.long) {
|
||||
println(" * ${pName} -> Param = ${pParam.long}, short = ${pParam.short}, desc = ${pParam.description}, mandatory = ${pParam.isMandatory}, type = ${pParam.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun showVersion(): Nothing {
|
||||
// TODO woher Version?
|
||||
println("v1.0.0")
|
||||
exitProcess(0)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.keinekekse.args
|
||||
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
actual fun isStdInputReady(): Boolean =
|
||||
(System.`in`.available() > 0)
|
||||
|
||||
actual fun exitApplication(rc: Int): Nothing =
|
||||
exitProcess(rc)
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package net.keinekekse.args
|
||||
package net.keinekekse.args.v1
|
||||
|
||||
import net.keinekekse.args.AbstractCommandLineParser.Companion.NAME_PATTERN
|
||||
import net.keinekekse.args.AbstractCommandLineParser.Companion.SHORT_NAME_PATTERN
|
||||
import net.keinekekse.args.v1.AbstractCommandLineParser.Companion.NAME_PATTERN
|
||||
import net.keinekekse.args.v1.AbstractCommandLineParser.Companion.SHORT_NAME_PATTERN
|
||||
import net.keinekekse.util.ValueDelegate
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
+4
-2
@@ -1,5 +1,6 @@
|
||||
package net.keinekekse.config
|
||||
|
||||
import net.keinekekse.util.VERSION
|
||||
import net.keinekekse.version.Version
|
||||
import java.io.File
|
||||
|
||||
@@ -10,15 +11,16 @@ import java.io.File
|
||||
* @property configFileName Name of the configuration file to use
|
||||
*/
|
||||
abstract class AbstractInitConfig(
|
||||
val applicationVersion: Version,
|
||||
val configFileName: String = DEFAULT_CONFIG_FILENAME,
|
||||
) {
|
||||
// abstract fun start()
|
||||
|
||||
/** Current application version */
|
||||
val applicationVersion: Version by lazy { Version(this::class.java) }
|
||||
// val applicationVersion: Version by lazy { Version(this::class.java) }
|
||||
|
||||
/** Library version information */
|
||||
val libraryVersion: Version by lazy { Version(Version::class.java) }
|
||||
val libraryVersion: Version = VERSION //by lazy { Version(Version::class.java) }
|
||||
|
||||
/** Configuration directory path */
|
||||
val configDirectory: String by lazy {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
||||
package net.keinekekse.version
|
||||
package net.keinekekse.version.manifest
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.keinekekse.args
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.*
|
||||
|
||||
// macOS + Linux
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
|
||||
actual fun isStdInputReady(): Boolean {
|
||||
return memScoped {
|
||||
val pfd = alloc<pollfd>()
|
||||
pfd.fd = STDIN_FILENO
|
||||
pfd.events = POLLIN.toShort()
|
||||
// timeout 0 means return immediately
|
||||
poll(pfd.ptr.reinterpret(), 1u, 10) > 0
|
||||
}
|
||||
}
|
||||
|
||||
actual fun exitApplication(rc: Int): Nothing {
|
||||
exit(rc)
|
||||
return Unit as Nothing
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.keinekekse.args
|
||||
|
||||
actual fun isStdInputReady(): Boolean {
|
||||
TODO("Not implemented on this platform")
|
||||
}
|
||||
|
||||
actual fun exitApplication(rc: Int): Nothing {
|
||||
TODO("Not implemented on this platform")
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package net.keinekekse.args
|
||||
|
||||
// js + wasmJs
|
||||
|
||||
actual fun isStdInputReady(): Boolean {
|
||||
TODO("Not implemented for this platform")
|
||||
}
|
||||
|
||||
actual fun exitApplication(rc: Int): Nothing {
|
||||
TODO("Not implemented for this platform")
|
||||
}
|
||||
Reference in new issue
Block a user