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