From afcf7f091492d809377866649389ed354a0615cf Mon Sep 17 00:00:00 2001 From: darwincereska Date: Wed, 19 Nov 2025 08:55:42 -0500 Subject: [PATCH] fix(help-and-test): fixed help, and tests --- build.gradle.kts | 2 +- src/main/kotlin/org/kargs/ArgType.kt | 4 +-- src/main/kotlin/org/kargs/Parser.kt | 46 +++++++++++++++++++------ src/main/kotlin/org/kargs/Subcommand.kt | 24 +++++++++++-- 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5861697..79ed2db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "org.kargs" -version = "1.0.2" +version = "1.0.3" repositories { mavenCentral() diff --git a/src/main/kotlin/org/kargs/ArgType.kt b/src/main/kotlin/org/kargs/ArgType.kt index e5ca732..c8c8cdf 100644 --- a/src/main/kotlin/org/kargs/ArgType.kt +++ b/src/main/kotlin/org/kargs/ArgType.kt @@ -49,7 +49,7 @@ sealed class ArgType(val typeName: String) { /** * Create a constrained integer type with min/max bounds */ - class IntRange(private val min: kotlin.Int, private val max: kotlin.Int) : ArgType("Int") { + class IntRange(val min: kotlin.Int, val max: kotlin.Int) : ArgType("Int") { override fun convert(value: String): kotlin.Int { val intValue = value.toIntOrNull() ?: throw ArgumentParseException("`$value` is not a valid integer") if (intValue !in min..max) { @@ -64,7 +64,7 @@ sealed class ArgType(val typeName: String) { /** * Create an enum type from list of valid choices */ - class Choice(private val choices: List) : ArgType("Choice") { + class Choice(val choices: List) : ArgType("Choice") { override fun convert(value: String): kotlin.String { if (value !in choices) { throw ArgumentParseException("`$value` is not a valid choice. Valid options: ${choices.joinToString(", ")}") diff --git a/src/main/kotlin/org/kargs/Parser.kt b/src/main/kotlin/org/kargs/Parser.kt index 508ab78..db01c12 100644 --- a/src/main/kotlin/org/kargs/Parser.kt +++ b/src/main/kotlin/org/kargs/Parser.kt @@ -32,12 +32,6 @@ class Parser( return } - // Check for global help - if (args.contains("--help") || args.contains("-h")) { - printGlobalHelp() - return - } - val cmdName = args[0] val cmd = findCommand(cmdName) @@ -49,7 +43,7 @@ class Parser( return } - // Check for command specific help + // Check for help, global or command if (args.contains("--help") || args.contains("-h")) { cmd.printHelp() return @@ -60,9 +54,8 @@ class Parser( validateRequiredOptions(cmd) cmd.execute() } catch (e: ArgumentParseException) { - printError(e.message ?: "Parse error") - cmd.printHelp() - throw e + handleParseError(e, cmd) + throw e } } @@ -225,9 +218,22 @@ class Parser( */ private fun validateRequiredOptions(cmd: Subcommand) { val missingRequired = cmd.options.filter { it.required && it.value == null } + val missingArgs = cmd.arguments.filter { it.required && it.value == null } + + val errors = mutableListOf() + if (missingRequired.isNotEmpty()) { val missing = missingRequired.joinToString(", ") { "--${it.longName}" } - throw ArgumentParseException("Missing required options: $missing") + errors.add("Missing required options: $missing") + } + + if (missingArgs.isNotEmpty()) { + val missing = missingArgs.joinToString(", ") { it.name } + errors.add("Missing required arguments: $missing") + } + + if (errors.isNotEmpty()) { + throw ArgumentParseException(errors.joinToString(", ")) } } @@ -284,6 +290,24 @@ class Parser( YELLOW("\u001B[33m"), BOLD("\u001B[1m") } + + /** + * Handles parse errors + * + * @throw ArgumentParseException if in debug mode + */ + private fun handleParseError(e: ArgumentParseException, cmd: Subcommand) { + printError(e.message ?: "Parse error") + cmd.printHelp() + + // Only show stack trace in debug mode + if (System.getProperty("debug") == "true") { + e.printStackTrace() + } + + // Exit gracefully instead of throwing + // kotlin.system.exitProcess(1) + } } /** diff --git a/src/main/kotlin/org/kargs/Subcommand.kt b/src/main/kotlin/org/kargs/Subcommand.kt index 9686866..0f65529 100644 --- a/src/main/kotlin/org/kargs/Subcommand.kt +++ b/src/main/kotlin/org/kargs/Subcommand.kt @@ -47,7 +47,8 @@ abstract class Subcommand( * Print help information for this subcommand */ fun printHelp() { - println("Usage: $name [options]${if (arguments.isNotEmpty()) " ${arguments.joinToString(" ") { "<${it.name}>" }}" else ""}") + println("Usage: $name [options]${if (arguments.isNotEmpty()) " ${arguments.joinToString(" ") { if (it.required) "<${it.name}>" else "[${it.name}]" }}" else ""}") + if (description.isNotEmpty()) { println() println(description) @@ -60,7 +61,9 @@ abstract class Subcommand( val shortName = option.shortName?.let { "-$it, " } ?: " " val required = if (option.required) " (required)" else "" val defaultVal = option.getValueOrDefault()?.let { " [default: $it]" } ?: "" - println(" $shortName--${option.longName}") + val typeInfo = getTypeInfo(option.type) + + println(" $shortName--${option.longName}${typeInfo}") option.description?.let { desc -> println(" $desc$required$defaultVal") } @@ -84,7 +87,8 @@ abstract class Subcommand( println("Arguments:") arguments.forEach { arg -> val required = if (arg.required) " (required)" else " (optional)" - println(" ${arg.name}$required") + val typeInfo = getTypeInfo(arg.type) + println(" ${arg.name}$typeInfo$required") arg.description?.let { desc -> println(" $desc") } @@ -107,4 +111,18 @@ abstract class Subcommand( return errors } + + /** + * Type info helper method + */ + private fun getTypeInfo(type: ArgType<*>): String { + return when (type) { + 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("|")}>" + } + } }