Command line parsing with Scala dynamic and macros

In our developer meeting this week, we talked about using the Scala features of “dynamic” and macros to simplify code for command-line parsing.

The problem we were trying to solve is a recent project in which we have lots of separate command-line apps, with a lot of overlap between their arguments, but with a need for app-specific arguments and to apply different default parameters to each app.

Our initial implementation was one big “all the command line arguments” class, shared between everything, listing all of the arguments. It works, and it’s simple, but it’s a single, large shared dependency. We’re using Scallop as a command line parsing library.

The first improved version was to use the Builder syntax for the Scallop command line parser – leading to code for accessing arguments like:

opts[String](“beanstalkHost”),
opts[Int](“beanstalkPort”)

This let groups of arguments be put into traits, so you could build up a command line app from a set of traits, one for accessing the beanstalk queue with the arguments that requires, one for accessing the REST datastore with the arguments that requires, and so on.

Then, using scala.Dynamic, we tried a new version that allows us to have a nicer syntax for accessing arguments like:

opt.beanstalkPort, opt.beanstalkPort

Where this is backed by a class implementing Dynamic with an appropriate selectDynamic implementation:

def selectDynamic[T](fieldName: String)(implicit ev: TypeTag[T]): T = {
scallop[T](name)
}

But this allows argument names to be mistyped, and the errors are only displayed at runtime. To get compile-time type checking for arguments, we need to use a compile-time Scala macro, something like:

def selectDynamic(fieldName: String): String = macro ScallopMacro.checkSafety
def checkSafety(c: blackbox.Context)(fieldName: c.Expr[String]): c.Expr[String] = {
import c.universe._
val evalFieldName= c.eval(c.Expr[String](c.untypecheck(fieldName.tree.duplicate)))
println(“Compile time output – Eval field name is “+evalFieldName)
if (! isValid(evalFieldName)) {
c.error(c.enclosingPosition, s”Illegal field name $evalFieldName!”)
}

val tree = q”opts.doSomething( $fieldName )”
c.Expr(tree)
}

This works – and gives us compile-time checks for argument names.

However, our conclusion after doing all of this is that the added code complexity created by the use of dynamic and macros outweighed the benefits. We’ve stuck with the simple approach that we had originally – it does mean a shared dependency, and a bit of code duplication, but the code is much simpler.