diff --git a/CHANGELOG.md b/CHANGELOG.md index eba466eaa..a9b96f875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Added +- `printHelpOnEmptyArgs` parameter to `CliktCommand` constructor. ([#41](https://github.com/ajalt/clikt/issues/41)) + ### Fixed - Usage errors now correctly print subcommand names. ([#47](https://github.com/ajalt/clikt/issues/47)) - Arguments with `multiple(required=true)` now report an error if no argument is given on the command line. ([#36](https://github.com/ajalt/clikt/issues/36)) diff --git a/clikt/src/main/kotlin/com/github/ajalt/clikt/core/CliktCommand.kt b/clikt/src/main/kotlin/com/github/ajalt/clikt/core/CliktCommand.kt index 2195c91c6..0a390ecb6 100755 --- a/clikt/src/main/kotlin/com/github/ajalt/clikt/core/CliktCommand.kt +++ b/clikt/src/main/kotlin/com/github/ajalt/clikt/core/CliktCommand.kt @@ -27,13 +27,16 @@ import kotlin.system.exitProcess * name. * @param invokeWithoutSubcommand Used when this command has subcommands, and this command is called * without a subcommand. If true, [run] will be called. By default, a [PrintHelpMessage] is thrown instead. + * @param printHelpOnEmptyArgs If this command is called with no values on the command line, print a + * help message (by throwing [PrintHelpMessage]) if this is true, otherwise run normally. */ @Suppress("PropertyName") abstract class CliktCommand( help: String = "", epilog: String = "", name: String? = null, - val invokeWithoutSubcommand: Boolean = false) { + val invokeWithoutSubcommand: Boolean = false, + val printHelpOnEmptyArgs: Boolean = false) { val commandName = name ?: javaClass.simpleName.split("$").last().toLowerCase() val commandHelp = help val commandHelpEpilog = epilog diff --git a/clikt/src/main/kotlin/com/github/ajalt/clikt/core/Context.kt b/clikt/src/main/kotlin/com/github/ajalt/clikt/core/Context.kt index 59d13e7b9..c83acc294 100644 --- a/clikt/src/main/kotlin/com/github/ajalt/clikt/core/Context.kt +++ b/clikt/src/main/kotlin/com/github/ajalt/clikt/core/Context.kt @@ -22,20 +22,23 @@ import kotlin.reflect.KProperty * options, the conflicting name will not be used for the help option. If the set is empty, or contains no * unique names, no help option will be added. * @property helpOptionMessage The description of the help option. - * @property helpFormatter The help formatter for this command + * @property helpFormatter The help formatter for this command. * @property tokenTransformer An optional transformation function that is called to transform command line * tokens (options and commands) before parsing. This can be used to implement e.g. case insensitive * behavior. + * @property console The console to use to print messages. */ -class Context(val parent: Context?, - val command: CliktCommand, - val allowInterspersedArgs: Boolean, - val autoEnvvarPrefix: String?, - val helpOptionNames: Set, - val helpOptionMessage: String, - val helpFormatter: HelpFormatter, - val tokenTransformer: Context.(String) -> String, - val console: CliktConsole) { +class Context( + val parent: Context?, + val command: CliktCommand, + val allowInterspersedArgs: Boolean, + val autoEnvvarPrefix: String?, + val helpOptionNames: Set, + val helpOptionMessage: String, + val helpFormatter: HelpFormatter, + val tokenTransformer: Context.(String) -> String, + val console: CliktConsole +) { var invokedSubcommand: CliktCommand? = null internal set var obj: Any? = null @@ -85,7 +88,7 @@ class Context(val parent: Context?, var helpOptionMessage: String = parent?.helpOptionMessage ?: "Show this message and exit" /** The help formatter for this command*/ var helpFormatter: HelpFormatter = parent?.helpFormatter ?: PlaintextHelpFormatter() - /** An optional transformation function that is called to transform command line*/ + /** An optional transformation function that is called to transform command line */ var tokenTransformer: Context.(String) -> String = parent?.tokenTransformer ?: { it } /** * The prefix to add to inferred envvar names. @@ -100,7 +103,7 @@ class Context(val parent: Context?, /** * The console that will handle reading and writing text. * - * The default uses [System.in] and [System.out]. + * The default uses [System. in] and [System.out]. */ var console: CliktConsole = parent?.console ?: defaultCliktConsole() } diff --git a/clikt/src/main/kotlin/com/github/ajalt/clikt/parsers/Parser.kt b/clikt/src/main/kotlin/com/github/ajalt/clikt/parsers/Parser.kt index 70d75a26d..fc4c65b2f 100644 --- a/clikt/src/main/kotlin/com/github/ajalt/clikt/parsers/Parser.kt +++ b/clikt/src/main/kotlin/com/github/ajalt/clikt/parsers/Parser.kt @@ -38,6 +38,10 @@ internal object Parser { } prefixes.remove("") + if (startingArgI > args.lastIndex && command.printHelpOnEmptyArgs) { + throw PrintHelpMessage(command) + } + val positionalArgs = ArrayList() var i = startingArgI var subcommand: CliktCommand? = null diff --git a/clikt/src/test/kotlin/com/github/ajalt/clikt/core/CliktCommandTest.kt b/clikt/src/test/kotlin/com/github/ajalt/clikt/core/CliktCommandTest.kt index 97f0f74f9..827ed457e 100644 --- a/clikt/src/test/kotlin/com/github/ajalt/clikt/core/CliktCommandTest.kt +++ b/clikt/src/test/kotlin/com/github/ajalt/clikt/core/CliktCommandTest.kt @@ -3,6 +3,7 @@ package com.github.ajalt.clikt.core import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.multiple import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.testing.NeverCalledCliktCommand import com.github.ajalt.clikt.testing.splitArgv import io.kotlintest.data.forall import io.kotlintest.shouldBe @@ -77,8 +78,15 @@ class CliktCommandTest { } } + @Test - fun `aliases`() = forall( + fun `printHelpOnEmptyArgs = true`() { + class C : NeverCalledCliktCommand(printHelpOnEmptyArgs = true) + shouldThrow { C().parse(splitArgv("")) } + } + + @Test + fun aliases() = forall( row("-xx", "x", emptyList()), row("a", "a", listOf("b")), row("a", "a", listOf("b")), @@ -105,4 +113,19 @@ class CliktCommandTest { C().parse(splitArgv(argv)) } + + @Test + fun `command usage`() { + class Parent : NeverCalledCliktCommand() { + val arg by argument() + } + + shouldThrow { + Parent().parse(splitArgv("")) + }.helpMessage() shouldBe """ + |Usage: parent [OPTIONS] ARG + | + |Error: Missing argument "ARG". + """.trimMargin() + } } diff --git a/clikt/src/test/kotlin/com/github/ajalt/clikt/output/TextExtensionsTest.kt b/clikt/src/test/kotlin/com/github/ajalt/clikt/output/TextExtensionsTest.kt index c265f93af..aae0404a2 100644 --- a/clikt/src/test/kotlin/com/github/ajalt/clikt/output/TextExtensionsTest.kt +++ b/clikt/src/test/kotlin/com/github/ajalt/clikt/output/TextExtensionsTest.kt @@ -7,7 +7,7 @@ import org.junit.Test class TextExtensionsTest { @Test - fun `wrapText`() = forall( + fun wrapText() = forall( row("abc".wrapText(), "abc"), row("abc\n".wrapText(), "abc"), row("abc\n".wrapText(width = 2), "abc"), @@ -28,7 +28,7 @@ class TextExtensionsTest { } @Test - fun `appendRepeat`() = forall( + fun appendRepeat() = forall( row("a", 0, ""), row("a", 1, "a"), row("a", 2, "aa"), diff --git a/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt b/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt index 053f8e5d2..4f44de9c4 100644 --- a/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt +++ b/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt @@ -21,7 +21,7 @@ import org.junit.Test @Suppress("unused") class OptionTest { @Test - fun `inferEnvvar`() = forall( + fun inferEnvvar() = forall( row(setOf("--foo"), null, null, null), row(setOf("--bar"), null, "FOO", "FOO_BAR"), row(setOf("/bar"), null, "FOO", "FOO_BAR"), diff --git a/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/SubcommandTest.kt b/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/SubcommandTest.kt index 818294859..9c6a97819 100644 --- a/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/SubcommandTest.kt +++ b/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/SubcommandTest.kt @@ -9,6 +9,7 @@ import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.multiple import com.github.ajalt.clikt.parameters.arguments.pair import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.testing.NeverCalledCliktCommand import com.github.ajalt.clikt.testing.splitArgv import io.kotlintest.data.forall import io.kotlintest.shouldBe @@ -19,7 +20,7 @@ import org.junit.Test class SubcommandTest { @Test - fun `subcommand`() = forall( + fun subcommand() = forall( row("--xx 2 sub --xx 3 --yy 4"), row("--xx 2 sub -x 3 --yy 4"), row("--xx 2 sub -x3 --yy 4"), @@ -242,26 +243,11 @@ class SubcommandTest { .parse(splitArgv(argv)) } - @Test - fun `command usage`() { - class Parent : NoRunCliktCommand() { - val arg by argument() - } - - shouldThrow { - Parent().parse(splitArgv("")) - }.helpMessage() shouldBe """ - |Usage: parent [OPTIONS] ARG - | - |Error: Missing argument "ARG". - """.trimMargin() - } - @Test fun `subcommand usage`() { class Parent : NoRunCliktCommand() class Child : NoRunCliktCommand() - class Grandchild : NoRunCliktCommand() { + class Grandchild : NeverCalledCliktCommand() { val arg by argument() } diff --git a/clikt/src/test/kotlin/com/github/ajalt/clikt/testing/NeverCalledCliktCommand.kt b/clikt/src/test/kotlin/com/github/ajalt/clikt/testing/NeverCalledCliktCommand.kt index 9d59d0364..c51c0fc55 100644 --- a/clikt/src/test/kotlin/com/github/ajalt/clikt/testing/NeverCalledCliktCommand.kt +++ b/clikt/src/test/kotlin/com/github/ajalt/clikt/testing/NeverCalledCliktCommand.kt @@ -3,10 +3,12 @@ package com.github.ajalt.clikt.testing import com.github.ajalt.clikt.core.CliktCommand import io.kotlintest.fail -open class NeverCalledCliktCommand(help: String = "", - epilog: String = "", - name: String? = null, - invokeWithoutSubcommand: Boolean = false) - : CliktCommand(help, epilog, name, invokeWithoutSubcommand) { +open class NeverCalledCliktCommand( + help: String = "", + epilog: String = "", + name: String? = null, + invokeWithoutSubcommand: Boolean = false, + printHelpOnEmptyArgs: Boolean = false +) : CliktCommand(help, epilog, name, invokeWithoutSubcommand, printHelpOnEmptyArgs) { override fun run() = fail("run should not be called") } diff --git a/docs/commands.md b/docs/commands.md index f47a1c2e5..cd87093ab 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -6,9 +6,9 @@ an `init` block, or on an existing instance. ## Executing Nested Commands -For commands with no children, [`run`](api/clikt/com.github.ajalt.clikt.core/-clikt-command/run.html) is called whenever the -command line is parsed (unless parsing is aborted from an error or an -option like `--help`). +For commands with no children, +[`run`](api/clikt/com.github.ajalt.clikt.core/-clikt-command/run.html) is called whenever the +command line is parsed (unless parsing is aborted from an error or an option like `--help`). If a command has children, this isn't the case. Instead, its `run` is called only if a child command is invoked, just before the subcommand's @@ -262,7 +262,7 @@ and class Cli : NoRunCliktCommand() fun main(args: Array) = Cli() .context { helpOptionMessage = "print the help" } - .main(splitArgv("")) + .main(args) ``` Any they work like: @@ -274,3 +274,31 @@ Usage: cli [OPTIONS] Options: -h, --help print the help ``` + +## Printing the help message when no arguments are given + +Normally, if a command is called with no values on the command line, a usage error is printed if +there are required parameters, or +[`run`](api/clikt/com.github.ajalt.clikt.core/-clikt-command/run.html) is called if there aren't +any. + +You can change this behavior by passing `printHelpOnEmptyArgs = true` to your's command's +constructor. This will cause a help message to be printed when to values are provided on the command +line, regardless of the parameters in your command. + +```kotlin +class Cli : CliktCommand() { + override fun run() { echo("Command ran") } +} +fun main(args: Array) = Cli().main(args) +``` + +Which will print the help message, even without `--help`: + +``` +$ ./cli +Usage: cli [OPTIONS] + +Options: + -h, --help print the help +```