Skip to content

premature evaluation of mutually-exclusive flag converters #629

@liblit

Description

@liblit

Summary

When mutuallyExclusiveOptions includes flags with convert functions, those converters are called prematurely for flags that were not used. I expected conversion to only happen for the mutually exclusive option that was actually used, if any.

Detailed Scenario

I'm using Clikt 5.0.3 with Kotlin 2.3.0. My CLI tool fetches a piece of data and does one of three things with it:

Flag Action
--save=<path> Save the data to the given file <path>
--show Show the data
--print Print the data

Notice that one of these actions requires an option with a <path> argument, while the other two are zero-argument flags.

I've modeled the actions as subclasses of a sealed Receiver class. Assume that the actual receive implementations are more complex than what's shown here.

sealed class Receiver {

  abstract fun receive()

  init {
    println("Initializing $this.")
  }

  data class SaveAs(var path: Path) : Receiver() {
    override fun receive() = println("Save as $path.")
  }

  class Show : Receiver() {
    override fun receive() = println("Show.")
  }

  class Print : Receiver() {
    override fun receive() = println("Print.")
  }
}

Since exactly one receiver is needed, I've modeled the command line using mutuallyExclusiveOptions, with convert lambdas to create the appropriate Receiver subclass instances:

class Main : CliktCommand() {

  val receiver: Receiver by
      mutuallyExclusiveOptions(
              option("--save").convert { Receiver.SaveAs(Path.of(it)) },
              option("--show").flag().convert { Receiver.Show() },
              option("--print").flag().convert { Receiver.Print() },
          )
          .single()
          .required()

  override fun run() {
    print("Using $receiver.")
  }
}

Observed Behavior Versus Expectation

Given the above, I find that Clikt calls the convert lambdas for each of the two flags, passing false as the value to be converted, even when the actual receiver should be SaveAs. In fact, even if the CLI has no options at all, the flag converters are still called before printing a usage message:

Initializing Receiver$Show@97e1986.
Initializing Receiver$Print@69d9c55.

Usage: main [<options>]

Error: must provide one of --save, --show, --print

My expectation was that a Show instance would only be constructed if --show were used, and likewise that a Print instance would only be constructed if --print were used. Prematurely creating these extra instances is a problem for my actual application, since the actual constructors for these Receiver subclasses do nontrivial work. I really want to create only the one Receiver subclass instance that is actually needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions