fix(help-and-test): fixed help, and tests
This commit is contained in:
@@ -4,7 +4,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "org.kargs"
|
group = "org.kargs"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ sealed class ArgType<T>(val typeName: String) {
|
|||||||
/**
|
/**
|
||||||
* Create a constrained integer type with min/max bounds
|
* Create a constrained integer type with min/max bounds
|
||||||
*/
|
*/
|
||||||
class IntRange(private val min: kotlin.Int, private val max: kotlin.Int) : ArgType<kotlin.Int>("Int") {
|
class IntRange(val min: kotlin.Int, val max: kotlin.Int) : ArgType<kotlin.Int>("Int") {
|
||||||
override fun convert(value: String): kotlin.Int {
|
override fun convert(value: String): kotlin.Int {
|
||||||
val intValue = value.toIntOrNull() ?: throw ArgumentParseException("`$value` is not a valid integer")
|
val intValue = value.toIntOrNull() ?: throw ArgumentParseException("`$value` is not a valid integer")
|
||||||
if (intValue !in min..max) {
|
if (intValue !in min..max) {
|
||||||
@@ -64,7 +64,7 @@ sealed class ArgType<T>(val typeName: String) {
|
|||||||
/**
|
/**
|
||||||
* Create an enum type from list of valid choices
|
* Create an enum type from list of valid choices
|
||||||
*/
|
*/
|
||||||
class Choice(private val choices: List<kotlin.String>) : ArgType<kotlin.String>("Choice") {
|
class Choice(val choices: List<kotlin.String>) : ArgType<kotlin.String>("Choice") {
|
||||||
override fun convert(value: String): kotlin.String {
|
override fun convert(value: String): kotlin.String {
|
||||||
if (value !in choices) {
|
if (value !in choices) {
|
||||||
throw ArgumentParseException("`$value` is not a valid choice. Valid options: ${choices.joinToString(", ")}")
|
throw ArgumentParseException("`$value` is not a valid choice. Valid options: ${choices.joinToString(", ")}")
|
||||||
|
|||||||
@@ -32,12 +32,6 @@ class Parser(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for global help
|
|
||||||
if (args.contains("--help") || args.contains("-h")) {
|
|
||||||
printGlobalHelp()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val cmdName = args[0]
|
val cmdName = args[0]
|
||||||
val cmd = findCommand(cmdName)
|
val cmd = findCommand(cmdName)
|
||||||
|
|
||||||
@@ -49,7 +43,7 @@ class Parser(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for command specific help
|
// Check for help, global or command
|
||||||
if (args.contains("--help") || args.contains("-h")) {
|
if (args.contains("--help") || args.contains("-h")) {
|
||||||
cmd.printHelp()
|
cmd.printHelp()
|
||||||
return
|
return
|
||||||
@@ -60,8 +54,7 @@ class Parser(
|
|||||||
validateRequiredOptions(cmd)
|
validateRequiredOptions(cmd)
|
||||||
cmd.execute()
|
cmd.execute()
|
||||||
} catch (e: ArgumentParseException) {
|
} catch (e: ArgumentParseException) {
|
||||||
printError(e.message ?: "Parse error")
|
handleParseError(e, cmd)
|
||||||
cmd.printHelp()
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,9 +218,22 @@ class Parser(
|
|||||||
*/
|
*/
|
||||||
private fun validateRequiredOptions(cmd: Subcommand) {
|
private fun validateRequiredOptions(cmd: Subcommand) {
|
||||||
val missingRequired = cmd.options.filter { it.required && it.value == null }
|
val missingRequired = cmd.options.filter { it.required && it.value == null }
|
||||||
|
val missingArgs = cmd.arguments.filter { it.required && it.value == null }
|
||||||
|
|
||||||
|
val errors = mutableListOf<String>()
|
||||||
|
|
||||||
if (missingRequired.isNotEmpty()) {
|
if (missingRequired.isNotEmpty()) {
|
||||||
val missing = missingRequired.joinToString(", ") { "--${it.longName}" }
|
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"),
|
YELLOW("\u001B[33m"),
|
||||||
BOLD("\u001B[1m")
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ abstract class Subcommand(
|
|||||||
* Print help information for this subcommand
|
* Print help information for this subcommand
|
||||||
*/
|
*/
|
||||||
fun printHelp() {
|
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()) {
|
if (description.isNotEmpty()) {
|
||||||
println()
|
println()
|
||||||
println(description)
|
println(description)
|
||||||
@@ -60,7 +61,9 @@ abstract class Subcommand(
|
|||||||
val shortName = option.shortName?.let { "-$it, " } ?: " "
|
val shortName = option.shortName?.let { "-$it, " } ?: " "
|
||||||
val required = if (option.required) " (required)" else ""
|
val required = if (option.required) " (required)" else ""
|
||||||
val defaultVal = option.getValueOrDefault()?.let { " [default: $it]" } ?: ""
|
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 ->
|
option.description?.let { desc ->
|
||||||
println(" $desc$required$defaultVal")
|
println(" $desc$required$defaultVal")
|
||||||
}
|
}
|
||||||
@@ -84,7 +87,8 @@ abstract class Subcommand(
|
|||||||
println("Arguments:")
|
println("Arguments:")
|
||||||
arguments.forEach { arg ->
|
arguments.forEach { arg ->
|
||||||
val required = if (arg.required) " (required)" else " (optional)"
|
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 ->
|
arg.description?.let { desc ->
|
||||||
println(" $desc")
|
println(" $desc")
|
||||||
}
|
}
|
||||||
@@ -107,4 +111,18 @@ abstract class Subcommand(
|
|||||||
|
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type info helper method
|
||||||
|
*/
|
||||||
|
private fun getTypeInfo(type: ArgType<*>): String {
|
||||||
|
return when (type) {
|
||||||
|
is ArgType.StringType -> ""
|
||||||
|
is ArgType.IntType -> " <int>"
|
||||||
|
is ArgType.DoubleType -> " <double>"
|
||||||
|
is ArgType.BooleanType -> " <bool>"
|
||||||
|
is ArgType.IntRange -> " <${type.min}-${type.max}>"
|
||||||
|
is ArgType.Choice -> " <${type.choices.joinToString("|")}>"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user