From de79b6bcd5d4b06c88845f9118d71f6003f68594 Mon Sep 17 00:00:00 2001 From: darwincereska Date: Wed, 19 Nov 2025 16:24:56 -0500 Subject: [PATCH] feat(optional-option): added optional option --- build.gradle.kts | 7 ++-- src/main/kotlin/org/kargs/ArgType.kt | 14 ++++++++ src/main/kotlin/org/kargs/OptionalOption.kt | 37 +++++++++++++++++++++ src/main/kotlin/org/kargs/Parser.kt | 13 ++++++++ src/main/kotlin/org/kargs/Subcommand.kt | 18 +++++++++- 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/org/kargs/OptionalOption.kt diff --git a/build.gradle.kts b/build.gradle.kts index 79ed2db..323c778 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "org.kargs" -version = "1.0.3" +version = "1.0.4" repositories { mavenCentral() @@ -12,8 +12,8 @@ repositories { dependencies { testImplementation(kotlin("test")) - implementation(kotlin("stdlib")) - implementation(kotlin("reflect")) + // implementation(kotlin("stdlib")) + // implementation(kotlin("reflect")) // JUnit 5 testing dependencies testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") @@ -68,3 +68,4 @@ mavenPublishing { } } } + diff --git a/src/main/kotlin/org/kargs/ArgType.kt b/src/main/kotlin/org/kargs/ArgType.kt index c8c8cdf..e1b425f 100644 --- a/src/main/kotlin/org/kargs/ArgType.kt +++ b/src/main/kotlin/org/kargs/ArgType.kt @@ -75,6 +75,15 @@ sealed class ArgType(val typeName: String) { override fun getValidationDescription(): String = "one of: ${choices.joinToString(", ")}" } + /** + * Optional value type - can be used as flag or with value + */ + class OptionalValue(val defaultWhenPresent: String = "true") : ArgType("OptionalValue") { + override fun convert(value: String): String = value + + override fun getValidationDescription(): String = "optional value or flag" + } + companion object { val String: ArgType = StringType val Int: ArgType = IntType @@ -90,5 +99,10 @@ sealed class ArgType(val typeName: String) { * Create a choice type from valid options */ fun choice(vararg options: kotlin.String) = Choice(options.toList()) + + /** + * Create an optional value or flag + */ + fun optionalValue(defaultWhenPresent: String = "true") = OptionalValue(defaultWhenPresent) } } diff --git a/src/main/kotlin/org/kargs/OptionalOption.kt b/src/main/kotlin/org/kargs/OptionalOption.kt new file mode 100644 index 0000000..c57f872 --- /dev/null +++ b/src/main/kotlin/org/kargs/OptionalOption.kt @@ -0,0 +1,37 @@ +package org.kargs + +/** + * Option that can be used as a flag or with a value + */ +class OptionalOption( + val longName: String, + val shortName: String? = null, + description: String? = null, + private val defaultWhenPresent: String = "true" +) : KargsProperty(description) { + private var wasExplicitlySet = false + + init { + require(longName.isNotBlank()) { "Long name cannot be blank" } + require(!longName.startsWith("-")) { "Long name should not start with dashes" } + shortName?.let { + require(it.length == 1) { "Short name must be exactly one character" } + require(!it.startsWith("-")) { "Short name should not start with dashes" } + } + } + + override fun parseValue(str: String) { + value = str + wasExplicitlySet = true + } + + /** + * Set as flag (no value provided) + */ + fun setAsFlag() { + value = defaultWhenPresent + wasExplicitlySet = true + } + + fun isSet(): Boolean = wasExplicitlySet +} diff --git a/src/main/kotlin/org/kargs/Parser.kt b/src/main/kotlin/org/kargs/Parser.kt index db01c12..e94c208 100644 --- a/src/main/kotlin/org/kargs/Parser.kt +++ b/src/main/kotlin/org/kargs/Parser.kt @@ -108,6 +108,7 @@ class Parser( private fun parseLongOption(cmd: Subcommand, key: String, args: Array, index: Int): Int { val option = cmd.options.firstOrNull { it.longName == key } val flag = cmd.flags.firstOrNull { it.longName == key } + val optionalOption = cmd.optionalOptions.firstOrNull { it.longName == key } return when { option != null -> { @@ -122,6 +123,18 @@ class Parser( } } + optionalOption != null -> { + // Check if next arg exists and doesn't start with - + if (index + 1 < args.size && !args[index + 1].startsWith("-")) { + // Has value + optionalOption.parseValue(args[index + 1]) + index + 1 + } else { + // Used as flag + optionalOption.setAsFlag() + index + } + } flag != null -> { flag.setFlag() index diff --git a/src/main/kotlin/org/kargs/Subcommand.kt b/src/main/kotlin/org/kargs/Subcommand.kt index 0f65529..d17ff48 100644 --- a/src/main/kotlin/org/kargs/Subcommand.kt +++ b/src/main/kotlin/org/kargs/Subcommand.kt @@ -16,6 +16,7 @@ abstract class Subcommand( private val _options = mutableListOf>() private val _flags = mutableListOf() private val _arguments = mutableListOf>() + private val _optionalOptions = mutableListOf() init { require(name.isNotBlank()) { "Subcommand name cannot be blank" } @@ -30,6 +31,7 @@ abstract class Subcommand( is Option<*> -> _options += prop is Flag -> _flags += prop is Argument<*> -> _arguments += prop + is OptionalOption -> _optionalOptions += prop } } @@ -37,6 +39,7 @@ abstract class Subcommand( val options: List> get() = _options val flags: List get() = _flags val arguments: List> get() = _arguments + val optionalOptions: List get() = _optionalOptions /** * Execute this subcommand - must be implemented by subclasses @@ -70,6 +73,18 @@ abstract class Subcommand( } } + if (optionalOptions.isNotEmpty()) { + println() + println("Optional Value Options:") + optionalOptions.forEach { option -> + val shortName = option.shortName?.let { "-$it, " } ?: " " + println(" $shortName--${option.longName} [value]") + option.description?.let { desc -> + println(" $desc (can be used as flag or with value)") + } + } + } + if (flags.isNotEmpty()) { println() println("Flags:") @@ -117,12 +132,13 @@ abstract class Subcommand( */ private fun getTypeInfo(type: ArgType<*>): String { return when (type) { - is ArgType.StringType -> "" + is ArgType.StringType -> " " is ArgType.IntType -> " " is ArgType.DoubleType -> " " is ArgType.BooleanType -> " " is ArgType.IntRange -> " <${type.min}-${type.max}>" is ArgType.Choice -> " <${type.choices.joinToString("|")}>" + is ArgType.OptionalValue -> " [string]" } } }