Skip to content

Instantly share code, notes, and snippets.

@lefou
Last active November 2, 2025 15:19
Show Gist options
  • Save lefou/4ab9e44c119559ff5e31e1e761d552a4 to your computer and use it in GitHub Desktop.
Save lefou/4ab9e44c119559ff5e31e1e761d552a4 to your computer and use it in GitHub Desktop.
`Args` API to model a CLI args data structure that supports mapping of path roots
//| mill-version: 1.0.6
package build
import mill.*
import mill.api.*
import scala.annotation.targetName
case class Args (value: Seq[ArgGroup]) derives upickle.ReadWriter {
def toStringSeq: Seq[String] = value.map(_.toString)
override def toString(): String = value.mkString("Args(", ", ", ")")
}
object Args {
@targetName("applyUnion")
def apply(value: (Arg|ArgGroup|Seq[Arg])*): Args = Args(value.flatMap {
case a: Arg => Seq(ArgGroup(a))
case a: ArgGroup => Seq(a)
case s: Seq[Arg] => s.map(ArgGroup(_))
})
}
/**
* A set of args, which are use as one
* @param value
*/
case class ArgGroup private (value: Seq[Arg]) derives upickle.ReadWriter {
override def toString(): String = value.mkString("(", ", ", ")")
}
type ArgTypes = (String|os.Path)
object ArgGroup {
@targetName("applyUnion")
def apply(value: (Arg | Seq[Arg])*): ArgGroup = ArgGroup(value.flatMap {
case a: Arg => Seq(a)
case s: Seq[Arg] => s
})
def when(cond: Boolean)(value: Arg*): ArgGroup = if(cond) ArgGroup(value*) else ArgGroup()
given argsToArgGroup: Conversion[(ArgTypes,ArgTypes), ArgGroup] =
(tuple: (ArgTypes, ArgTypes)) => ArgGroup(Arg(tuple._1), Arg(tuple._2))
}
case class Arg (value: (ArgTypes)*) {
override def toString: String = value.mkString("")
}
object Arg {
implicit def jsonReadWriter: upickle.ReadWriter[Arg] = upickle.readwriter[Seq[(Option[String], Option[os.Path])]].bimap(
_.value.map {
case path: os.Path => (None, Some(path))
case str: String => (Some(str), None)
},
seq => Arg(seq.map {
case (Some(str), _) => str
case (_, Some(path)) => path
}*)
)
given stringToArg: Conversion[String, Arg] = (value: String) => Arg(value)
given osPathToArg: Conversion[os.Path, Arg] = (value: os.Path) => Arg(value)
}
implicit class ArgPartsSyntax(ctx: StringContext) extends AnyVal {
def arg(args: Any*): Arg = {
val vals = ctx.parts.take(args.length).zip(args).flatMap { case (p, a) => Seq(p, a) } ++
ctx.parts.drop(args.length)
val elems: Seq[(String|os.Path)] = vals.flatMap {
case path: os.Path => Seq(path)
case s => Seq(s.toString).filter(_.nonEmpty)
}
Arg(elems*)
}
}
object `package` extends Module {
def sources: T[Seq[PathRef]] = Task {
Seq(PathRef(moduleDir / "src/File1.java"), PathRef(moduleDir / "src/File2.java"))
}
def sources2: T[Seq[PathRef]] = Task {
Seq(PathRef(moduleDir / "src/File3.java"), PathRef(moduleDir / "src/File4.java"))
}
def plugin: T[PathRef] = Task {
PathRef(moduleDir / "lib/plugin.jar")
}
def javacOptions: Task.Simple[Args] = Args(
// single arg
Arg("-deprecation"),
// implicit single args
"-verbose",
// two args as group
ArgGroup("--release", "17"),
// two args as tuple
("--release", "17"),
// an option including a file via ArgParts
Arg("-Xplugin=", plugin().path),
// an option including a file via arg string interpolator
arg"-Xplugin:${plugin().path}",
// some files
sources().map(_.path),
// some files as ArgGroup
ArgGroup(sources2().map(_.path)*),
// Mixed ArgGroup
ArgGroup("--extra", arg"-Xplugin=${plugin().path}", sources().map(_.path))
)
def useOptions() = Task.Command {
val opts = javacOptions()
println(s"options: ${opts}")
pprint.log(upickle.write(opts))
}
}
@lefou
Copy link
Author

lefou commented Nov 2, 2025

Related to

This gits can be run with Mill 1.0.6 to show that it can be used as drop-in replacement for Seq[String] to encode options in Mill tasks:

> mill useOptions
[5/5] useOptions
[5] options: Args((-deprecation), (-verbose), (--release, 17), (--release, 17), (-Xplugin=/home/lefou/work/opensource/mill-experiment-options/lib/plugin.jar), (-Xplugin:/home/lefou/work/opensource/mill-experiment-options/lib/plugin.jar), (/home/lefou/work/opensource/mill-experiment-options/src/File1.java), (/home/lefou/work/opensource/mill-experiment-options/src/File2.java), (/home/lefou/work/opensource/mill-experiment-options/src/File3.java, /home/lefou/work/opensource/mill-experiment-options/src/File4.java), (--extra, -Xplugin=/home/lefou/work/opensource/mill-experiment-options/lib/plugin.jar, /home/lefou/work/opensource/mill-experiment-options/src/File1.java, /home/lefou/work/opensource/mill-experiment-options/src/File2.java))
[5] build.mill:121 upickle.write(opts): "{\"value\":[{\"value\":[[[\"-deprecation\",null]]]},{\"value\":[[[\"-verbose\",null]]]},{\"value\":[[[\"--release\",null]],[[\"17\",null]]]},{\"value\":[[[\"--release\",null]],[[\"17\",null]]]},{\"value\":[[[\"-Xplugin=\",null],[null,\"/home/lefou/work/opensource/mill-experiment-options/lib/plugin.jar\"]]]},{\"value\":[[[\"-Xplugin:\",null],[null,\"/home/lefou/work/opensource/mill-experiment-options/lib/plugin.jar\"]]]},{\"value\":[[[null,\"/home/lefou/work/opensource/mill-experiment-options/src/File1.java\"]]]},{\"value\":[[[null,\"/home/lefou/work/opensource/mill-experiment-options/src/File2.java\"]]]},{\"value\":[[[null,\"/home/lefou/work/opensource/mill-experiment-options/src/File3.java\"]],[[null,\"/home/lefou/work/opensource/mill-experiment-options/src/File4.java\"]]]},{\"value\":[[[\"--extra\",null]],[[\"-Xplugin=\",null],[null,\"/home/lefou/work/opensource/mill-experiment-options/lib/plugin.jar\"]],[[null,\"/home/lefou/work/opensource/mill-experiment-options/src/File1.java\"]],[[null,\"/home/lefou/work/opensource/mill-experiment-options/src/File2.java\"]]]}]}"
[5/5] ============================== useOptions ============================== 14s

When used with a local build based on PR com-lihaoyi/mill#6031, you can see, that all paths are properly represented with placeholders (here $WORKSPACE) to their mapped root paths.

> dev-mill useOptions
[5/5] useOptions
[5] options: Args((-deprecation), (-verbose), (--release, 17), (--release, 17), (-Xplugin=/home/lefou/work/opensource/mill-experiment-options/lib/plugin.jar), (-Xplugin:/home/lefou/work/opensource/mill-experiment-options/lib/plugin.jar), (/home/lefou/work/opensource/mill-experiment-options/src/File1.java), (/home/lefou/work/opensource/mill-experiment-options/src/File2.java), (/home/lefou/work/opensource/mill-experiment-options/src/File3.java, /home/lefou/work/opensource/mill-experiment-options/src/File4.java), (--extra, -Xplugin=/home/lefou/work/opensource/mill-experiment-options/lib/plugin.jar, /home/lefou/work/opensource/mill-experiment-options/src/File1.java, /home/lefou/work/opensource/mill-experiment-options/src/File2.java))
[5] build.mill:121 upickle.write(opts): "{\"value\":[{\"value\":[[[\"-deprecation\",null]]]},{\"value\":[[[\"-verbose\",null]]]},{\"value\":[[[\"--release\",null]],[[\"17\",null]]]},{\"value\":[[[\"--release\",null]],[[\"17\",null]]]},{\"value\":[[[\"-Xplugin=\",null],[null,\"$WORKSPACE/lib/plugin.jar\"]]]},{\"value\":[[[\"-Xplugin:\",null],[null,\"$WORKSPACE/lib/plugin.jar\"]]]},{\"value\":[[[null,\"$WORKSPACE/src/File1.java\"]]]},{\"value\":[[[null,\"$WORKSPACE/src/File2.java\"]]]},{\"value\":[[[null,\"$WORKSPACE/src/File3.java\"]],[[null,\"$WORKSPACE/src/File4.java\"]]]},{\"value\":[[[\"--extra\",null]],[[\"-Xplugin=\",null],[null,\"$WORKSPACE/lib/plugin.jar\"]],[[null,\"$WORKSPACE/src/File1.java\"]],[[null,\"$WORKSPACE/src/File2.java\"]]]}]}"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment