Command flag behavior improvements (#1432)

* Forbid overlap of built-in and command-defined flag names 
* Allow interleaving built-in and command-defined flags on the command
line
* List abbreviated flag names first, matching the behavior of built-in
flags
This commit is contained in:
Jen Basch
2026-02-20 12:00:18 -08:00
committed by GitHub
parent 08712e8b26
commit a5dc91f0a5
11 changed files with 146 additions and 31 deletions

View File

@@ -45,6 +45,8 @@ class CliCommandRunner
@JvmOverloads
constructor(
private val options: CliBaseOptions,
private val reservedFlagNames: Set<String>,
private val reservedFlagShortNames: Set<String>,
private val args: List<String>,
private val outputStream: OutputStream = System.out,
private val errStream: OutputStream = System.err,
@@ -65,7 +67,11 @@ constructor(
private fun evalCmd(builder: EvaluatorBuilder) {
val evaluator = builder.build()
evaluator.use {
evaluator.evaluateCommand(uri(normalizedSourceModule)) { spec ->
evaluator.evaluateCommand(
uri(normalizedSourceModule),
reservedFlagNames,
reservedFlagShortNames,
) { spec ->
try {
val root = SynthesizedRunCommand(spec, this, options.sourceModules.first().toString())
root.installCommonOptions(includeVersion = false)
@@ -233,7 +239,7 @@ constructor(
.mapNotNull {
val opt = it as? OptionWithValues<*, *, *> ?: return@mapNotNull null
return@mapNotNull if (it.names.contains("--help")) null
else it.names.first().trimStart('-') to opt.value
else it.names.last().trimStart('-') to opt.value
}
.toMap() +
registeredArguments()

View File

@@ -16,11 +16,16 @@
package org.pkl.cli.commands
import com.github.ajalt.clikt.completion.CompletionCandidates
import com.github.ajalt.clikt.core.MissingArgument
import com.github.ajalt.clikt.core.PrintHelpMessage
import com.github.ajalt.clikt.core.context
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.convert
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import java.net.URI
import org.pkl.cli.CliCommandRunner
import org.pkl.commons.cli.commands.BaseCommand
@@ -32,19 +37,46 @@ class RunCommand : BaseCommand(name = "run", helpLink = helpLink) {
override val treatUnknownOptionsAsArgs = true
init {
context { allowInterspersedArgs = false }
context {
// override clikt's built-in help behavior
// the built-in --help is eager so any --help/-h would force printing of pkl run's help
// which is not desired when a command module (or any of its subcommands) are present
// since that would mean command-defined help is gated behind a non-obvious `-- --help`
helpOptionNames = emptySet()
}
}
val module: URI by
argument(name = "module", completionCandidates = CompletionCandidates.Path).convert {
BaseOptions.parseModuleName(it)
}
private val showHelp by option("-h", "--help", help = "Show this message and exit").flag()
val module: URI? by
argument(name = "module", completionCandidates = CompletionCandidates.Path)
.convert { BaseOptions.parseModuleName(it) }
.optional()
val args: List<String> by argument(name = "args").multiple()
private val projectOptions by ProjectOptions()
override fun run() {
CliCommandRunner(baseOptions.baseOptions(listOf(module), projectOptions), args).run()
// if no module is specified but --help is show help, otherwise error becuase module is missing
if (module == null)
if (showHelp) throw PrintHelpMessage(currentContext)
else throw MissingArgument(registeredArguments().find { it.name == "module" }!!)
val reservedFlagNames = mutableSetOf("help")
val reservedFlagShortNames = mutableSetOf("h")
registeredOptions().forEach { opt ->
(opt.names + opt.secondaryNames).forEach {
if (it.startsWith("--")) reservedFlagNames.add(it.trimStart('-'))
else reservedFlagShortNames.add(it.trimStart('-'))
}
}
CliCommandRunner(
baseOptions.baseOptions(listOf(module!!), projectOptions),
reservedFlagNames,
reservedFlagShortNames,
if (showHelp) args + listOf("--help") else args,
)
.run()
}
}

View File

@@ -85,7 +85,15 @@ class CliCommandRunnerTest {
private fun runToStdout(options: CliBaseOptions, args: List<String>): String {
val outWriter = ByteArrayOutputStream()
CliCommandRunner(options, args, outWriter, ByteArrayOutputStream()).run()
CliCommandRunner(
options,
setOf("root-dir"),
emptySet(),
args,
outWriter,
ByteArrayOutputStream(),
)
.run()
return outWriter.toString(StandardCharsets.UTF_8)
}
@@ -1071,9 +1079,9 @@ class CliCommandRunnerTest {
int16: Int16
@CountedFlag { shortName = "d" }
int32: Int32
@CountedFlag { shortName = "e" }
@CountedFlag { shortName = "x" }
uint: UInt
@CountedFlag { shortName = "f" }
@CountedFlag { shortName = "y" }
uint8: UInt8
@CountedFlag { shortName = "g" }
uint16: UInt16
@@ -1087,7 +1095,7 @@ class CliCommandRunnerTest {
val output =
runToStdout(
CliBaseOptions(sourceModules = listOf(moduleUri)),
listOf("-abbcccddddeeeeeffffffgggggggiiiiiiii"),
listOf("-abbcccddddxxxxxyyyyyygggggggiiiiiiii"),
)
assertThat(output)
.isEqualTo(