mill icon indicating copy to clipboard operation
mill copied to clipboard

Commands parameter parser should support delegate classes

Open lefou opened this issue 3 years ago • 1 comments

Because the way commands are currently implemented, there is not well defined way to overload a command with different options. This makes gradual evolving a modules API hard.

mainargs already supports parsing case classes. If Mill would support parsing of command parameter by delegating to parsing a case class first, we could gradually evolve and deprecate some parameters without breaking (binary) compatibility.

Example:


case class MyArgs(verbose: Flag = Flag, prefix: Option[String] = None)
def myCommand(args: MyArgs) = T.command {
  if(args.verbose.value) ...
}

$ mill myCommand --verbose --prefix abc

lefou avatar May 16 '22 14:05 lefou

@lihaoyi Could you comment whether this is already possible or how much work this might take in either mainargs or Mill?

lefou avatar Jul 12 '22 13:07 lefou

To really avoid binary compatibility issue, case classes are a bad choice. We rather want to use ordinary classes, so we probably need to add support for these in mainargs.

lefou avatar Sep 11 '22 22:09 lefou

Haha, this is actually working, when not defined in a build.sc. It fails to compile in a build.sc, probably because the code wrapping happening for Ammonite scripts. But it works, if we define the module trait in a Scala class and compile it in a regular project.

trait MyModule extends Module {
  def argTest(args: MyModule.MyArgs) = T.command {
    println(s"args: ${args}")
  }
}

object MyModule {
  case class MyArgs(a: mainargs.Flag = mainargs.Flag(false), b: Seq[String] = Seq.empty[String])
  implicit val argsReader: mainargs.ParserForClass[MyArgs] = mainargs.ParserForClass[MyArgs]
}

It's even working for non-case classes, as long as we provide a companion object with an apply method.

trait MyModule extends Module {
  def argTest(args: MyModule.MyArgs) = T.command {
    println(s"args: ${args}")
  }
}

object MyModule {
  class MyArgs private (val a: mainargs.Flag, val b: Seq[String]) {
    override def toString: String = getClass.getSimpleName + ("a=" + a + ",b=" + b + ")")
  }

  object MyArgs {
    @deprecated("test deprecation")
    def apply(a: mainargs.Flag): MyArgs = apply(a, Seq.empty)

    def apply(a: mainargs.Flag = mainargs.Flag(false), b: Seq[String] = Seq.empty): MyArgs =
      new MyArgs(a, b)
  }

  implicit val argsReader: mainargs.ParserForClass[MyArgs] = mainargs.ParserForClass[MyArgs]
}

lefou avatar Sep 12 '22 08:09 lefou