Skip to content

Commit becce2e

Browse files
authored
Finalize eager options before any commands (#547)
1 parent b1f5b42 commit becce2e

File tree

5 files changed

+86
-25
lines changed

5 files changed

+86
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
- **Breaking Change:** `Context.obj` and `Context.terminal`, and `OptionTransformContext.terminal` are now extension functions rather than properties.
4141
- **Breaking Change:** The `RenderedSection` and `DefinitionRow` classes have moved to `AbstractHelpFormatter`.
4242
- Updated Kotlin to 2.0.0
43+
- Support calling `--help` on subcommands when parents have required parameters.
4344

4445
### Fixed
4546
- Fixed excess arguments not being reported when `allowMultipleSubcommands=true` and a subcommand has excess arguments followed by another subcommand.

clikt-mordant/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/SubcommandTest.kt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import io.kotest.matchers.string.shouldContain
2323
import kotlin.js.JsName
2424
import kotlin.test.Test
2525

26-
@Suppress("BooleanLiteralArgument")
26+
@Suppress("BooleanLiteralArgument", "unused")
2727
class SubcommandTest {
2828
@Test
2929
fun subcommand() = forAll(
@@ -267,6 +267,32 @@ class SubcommandTest {
267267
""".trimMargin()
268268
}
269269

270+
@Test
271+
@JsName("subcommand_help_with_required_parent")
272+
fun `subcommand help with required parent`() {
273+
class Parent : TestCommand() {
274+
val o by option().required()
275+
}
276+
class Child : TestCommand() {
277+
val o by option().required()
278+
}
279+
class Grandchild : TestCommand(called = false) {
280+
val foo by option()
281+
}
282+
283+
val p = Parent()
284+
shouldThrow<PrintHelpMessage> {
285+
p.subcommands(Child().subcommands(Grandchild()))
286+
.parse("child grandchild --help")
287+
}.let { p.getFormattedHelp(it) } shouldBe """
288+
|Usage: parent child grandchild [<options>]
289+
|
290+
|Options:
291+
| --foo=<text>
292+
| -h, --help Show this message and exit
293+
""".trimMargin()
294+
}
295+
270296
@Test
271297
@JsName("subcommandprintHelpOnEmptyArgs__true")
272298
fun `subcommand printHelpOnEmptyArgs = true`() {
@@ -342,10 +368,12 @@ class SubcommandTest {
342368
@Test
343369
@JsName("multiple_subcommands_optional_sub_arg")
344370
fun `multiple subcommands optional sub arg`() {
345-
class Sub: TestCommand(count = 2) {
371+
class Sub : TestCommand(count = 2) {
346372
val a by argument().optional()
347373
}
348-
class C: TestCommand(allowMultipleSubcommands = true)
374+
375+
class C : TestCommand(allowMultipleSubcommands = true)
376+
349377
val sub = Sub()
350378
C().subcommands(sub).parse("sub sub b")
351379
sub.a shouldBe "b"

clikt/api/clikt.api

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1349,7 +1349,8 @@ public final class com/github/ajalt/clikt/parsers/CommandLineParseResult {
13491349

13501350
public final class com/github/ajalt/clikt/parsers/CommandLineParser {
13511351
public static final field INSTANCE Lcom/github/ajalt/clikt/parsers/CommandLineParser;
1352-
public final fun finalize (Lcom/github/ajalt/clikt/parsers/CommandInvocation;)V
1352+
public final fun finalizeCommand (Lcom/github/ajalt/clikt/parsers/CommandInvocation;)V
1353+
public final fun finalizeEagerOptions (Lcom/github/ajalt/clikt/parsers/CommandInvocation;)V
13531354
public final fun main (Lcom/github/ajalt/clikt/core/BaseCliktCommand;Lkotlin/jvm/functions/Function1;)V
13541355
public final fun mainReturningValue (Lcom/github/ajalt/clikt/core/BaseCliktCommand;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
13551356
public final fun parse (Lcom/github/ajalt/clikt/core/BaseCliktCommand;Ljava/util/List;)Lcom/github/ajalt/clikt/parsers/CommandLineParseResult;

clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parsers/CommandLineParser.kt

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ object CommandLineParser {
7878
}
7979

8080
/**
81-
* [Finalize][finalize] and [run][runCommand] all invoked commands.
81+
* [Finalize][finalizeCommand] and [run][runCommand] all invoked commands.
8282
*
8383
* @throws CliktError if an error occurred while parsing or of any occur while finalizing or
8484
* running the commands.
@@ -100,38 +100,48 @@ object CommandLineParser {
100100
* This function does not throw exceptions. If parsing errors occur, they will be in the returned
101101
* result.
102102
*
103-
* This function does not [run] the command or [finalize] the invocations.
103+
* This function does not [run] the command or [finalizeCommand] the invocations.
104104
*/
105105
fun <T : BaseCliktCommand<T>> parse(command: T, argv: List<String>): CommandLineParseResult<T> {
106106
return parseArgv(command, argv)
107107
}
108-
109108
/**
110-
* Finalize a command invocation, converting and setting the values for all options and other
111-
* parameters. This function does not [run] the command.
109+
* Finalize eager options for a command invocation, running them if they were invoked.
112110
*
113-
* @throws CliktError If the [invocation] had any errors or if any parameters fail to finalize,
114-
* such as if a required option is missing or a value could not be converted.
111+
* This does not finalize any other parameters.
112+
*
113+
* @throws CliktError If any of the eager options were invoked and throw an error like
114+
* [PrintHelpMessage].
115115
*/
116-
fun finalize(invocation: CommandInvocation<*>) {
116+
fun finalizeEagerOptions(invocation: CommandInvocation<*>) {
117117
val command = invocation.command
118118
val context = command.currentContext
119-
val groups = command.registeredParameterGroups()
120-
val arguments = command.registeredArguments()
121-
122119
throwCompletionMessageIfRequested(context, command)
123120

124-
val (eagerOpts, nonEagerOpts) = command.registeredOptions()
125-
.partition { it.eager }
126-
127-
val (eagerInvs, nonEagerInvs) = invocation.optionInvocations.entries
128-
.partition { it.key.eager }
129-
.toList().map { it.associate { (k, v) -> k to v } }
121+
val (eagerOpts, _) = getOpts(command)
122+
val (eagerInvs, _) = getInvs(invocation)
130123

131124
// finalize and validate eager options first; unlike other options, eager options only get
132125
// validated if they're invoked
133126
finalizeOptions(context, eagerOpts, eagerInvs)
134127
validateParameters(context, eagerInvs.keys).throwErrors()
128+
}
129+
130+
/**
131+
* Finalize a command invocation, converting and setting the values for all options and other
132+
* parameters. This function does not [finalizeEagerOptions] or [run] the command.
133+
*
134+
* @throws CliktError If the [invocation] had any errors or if any parameters fail to finalize,
135+
* such as if a required option is missing or a value could not be converted.
136+
*/
137+
fun finalizeCommand(invocation: CommandInvocation<*>) {
138+
val command = invocation.command
139+
val context = command.currentContext
140+
val groups = command.registeredParameterGroups()
141+
val arguments = command.registeredArguments()
142+
143+
val (_, nonEagerOpts) = getOpts(command)
144+
val (_, nonEagerInvs) = getInvs(invocation)
135145

136146
// throw any parse errors after the eager options are finalized
137147
invocation.throwErrors()
@@ -154,6 +164,15 @@ object CommandLineParser {
154164

155165
context.invokedSubcommands += invocation.subcommandInvocations.map { it.command }
156166
}
167+
168+
private fun getInvs(invocation: CommandInvocation<*>) =
169+
invocation.optionInvocations.entries
170+
.partition { it.key.eager }
171+
.toList().map { it.associate { (k, v) -> k to v } }
172+
173+
private fun getOpts(command: BaseCliktCommand<*>) =
174+
command.registeredOptions()
175+
.partition { it.eager }
157176
}
158177

159178
private fun CommandInvocation<*>.throwErrors() {

clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parsers/Invocation.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ class CommandLineParseResult<T : BaseCliktCommand<T>>(
9595
* ```
9696
*
9797
* @param finalize If true (the default), finalize all commands as they are emitted in the sequence.
98-
* If false, you must call [CommandLineParser.finalize] on each invocation yourself before running
99-
* the command.
98+
* If false, you must call [CommandLineParser.finalizeEagerOptions] and
99+
* [CommandLineParser.finalizeCommand] on each invocation yourself before running the command.
100100
*/
101101
fun <T : BaseCliktCommand<T>> CommandInvocation<T>.flatten(finalize: Boolean = true): FlatInvocations<T> {
102102
return FlatInvocations(this, finalize)
@@ -116,9 +116,21 @@ class FlatInvocations<T : BaseCliktCommand<T>> internal constructor(
116116
closables.removeLast().close()
117117
}
118118
yieldSubs(root)
119-
}.onEach { if (finalize) CommandLineParser.finalize(it) }
119+
}
120+
120121

121-
override fun iterator(): Iterator<CommandInvocation<T>> = seq.iterator()
122+
override fun iterator(): Iterator<CommandInvocation<T>> {
123+
return when {
124+
finalize -> sequence {
125+
// Finalize eager options of all commands first so that you can call --help on
126+
// subcommands even if the parent has required parameters
127+
seq.iterator().forEach(CommandLineParser::finalizeEagerOptions)
128+
yieldAll(seq.onEach { CommandLineParser.finalizeCommand(it) })
129+
}
130+
131+
else -> seq
132+
}.iterator()
133+
}
122134

123135
/**
124136
* [Close][Context.close] all open contexts of invoked commands.

0 commit comments

Comments
 (0)