From 7001b00f5f46e91011f24d7bdd9113f86dfa2a64 Mon Sep 17 00:00:00 2001 From: Infendro Date: Thu, 8 Jan 2026 21:09:04 +0100 Subject: [PATCH] improve help display type for fallback commands display type and range for arguments/options --- .../com/infendro/cli/command/Command.kt | 10 ++-- .../cli/command/help/DefaultHelpRenderer.kt | 47 ++++++++++++++----- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt index 5cfb1df..0faa25e 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt @@ -23,8 +23,8 @@ sealed class Command private constructor( if (!name.matches(Regex.COMMAND)) throw InvalidCommandName(name) } - fun run(args: Array) { - when (val result = ContextParser(this, args).parse()) { + fun run(vararg args: String) { + when (val result = ContextParser(this, arrayOf(*args)).parse()) { is ContextParser.Result.Success -> result.context.execute() is ContextParser.Result.Help -> { val command = result.path.last() @@ -144,7 +144,7 @@ sealed class Command private constructor( val last = commands.last() if (last is Fallback<*>) throw InvalidCommandOrder() } - if (commands.any { it.name == command.name }) throw DuplicateCommand(command.name) + if (commands.any { it.name == command.name }) throw DuplicateCommand(command) } private fun validateArgument(argument: Argument<*>) { @@ -155,12 +155,12 @@ sealed class Command private constructor( argument.required && last.optional -> throw InvalidArgumentOrder() } } - if (arguments.any { it.name == argument.name }) throw DuplicateArgument(argument.name) + if (arguments.any { it.name == argument.name }) throw DuplicateArgument(argument) } private fun validateOption(option: Option<*>) { for (name in option.names) { - if (options.any { name in it.names }) throw DuplicateOption(name) + if (options.any { name in it.names }) throw DuplicateOption(option) } } diff --git a/src/commonMain/kotlin/com/infendro/cli/command/help/DefaultHelpRenderer.kt b/src/commonMain/kotlin/com/infendro/cli/command/help/DefaultHelpRenderer.kt index 50a105a..d1027cb 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/help/DefaultHelpRenderer.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/help/DefaultHelpRenderer.kt @@ -3,6 +3,7 @@ package com.infendro.cli.command.help import com.infendro.cli.command.Command import com.infendro.cli.command.argument.Argument import com.infendro.cli.command.option.Option +import kotlin.reflect.KClass object DefaultHelpRenderer : HelpRenderer { override fun render(path: List) = buildString { @@ -40,11 +41,7 @@ object DefaultHelpRenderer : HelpRenderer { argument.min == 1 && argument.max == 1 -> append("<${argument.name}>") argument.min == 0 && argument.max == 1 -> append("[<${argument.name}>]") else -> { - val range = when { - argument.unbounded -> "{${argument.min},}" - argument.min == argument.max -> "{${argument.min}}" - else -> "{${argument.min},${argument.max}}" - } + val range = renderRange(argument.min, argument.max) append("<${argument.name}>$range") } } @@ -64,11 +61,7 @@ object DefaultHelpRenderer : HelpRenderer { option.min == 1 && option.max == 1 -> append("($text)") option.min == 0 && option.max == 1 -> append("[$text]") else -> { - val range = when { - option.unbounded -> "{${option.min},}" - option.min == option.max -> "{${option.min}}" - else -> "{${option.min},${option.max}}" - } + val range = renderRange(option.min, option.max) append("($text)$range") } } @@ -80,7 +73,11 @@ object DefaultHelpRenderer : HelpRenderer { appendLine("Commands:") for (command in commands) { val name = when { - command is Command.Fallback<*> -> "<${command.name}>" + command is Command.Fallback<*> -> { + val name = command.name + val type = command.parser.type.name + "<$name> ($type)" + } else -> command.name } appendLine(" * $name") @@ -90,7 +87,10 @@ object DefaultHelpRenderer : HelpRenderer { private fun StringBuilder.arguments(arguments: List>) { appendLine("Arguments:") for (argument in arguments) { - appendLine(" * ${argument.name}") + val name = argument.name + val type = argument.parser.type.name + val range = renderRangeFull(argument.min, argument.max) + appendLine(" * $name ($type, $range)") } } @@ -98,7 +98,9 @@ object DefaultHelpRenderer : HelpRenderer { appendLine("Options:") for (option in options) { val name = option.names.joinToString(" | ") { it.withPrefix() } - appendLine(" * $name") + val type = option.parser.type.name + val range = renderRangeFull(option.min, option.max) + appendLine(" * $name ($type, $range)") } } @@ -106,4 +108,23 @@ object DefaultHelpRenderer : HelpRenderer { length == 1 -> "-$this" else -> "--$this" } + + private val KClass<*>.name: String + get() = simpleName ?: "Unknown" + + private fun renderRange(min: Int, max: Int?) = when { + max == null -> "{$min,}" + min == max -> "{$min}" + else -> "{$min,$max}" + } + + private fun renderRangeFull(min: Int, max: Int?) = when { + min == 1 && max == 1 -> "required" + min == 0 && max == 1 -> "optional" + else -> when { + max == null -> "at least $min" + min == max -> "exactly $min" + else -> "between $min and $max" + } + } }