1. Introduction

In this tutorial, we’ll show various ways to parse command-line arguments in Scala.

Although this topic might seem a bit trivial, someone might often wonder if he should implement the parsing himself or use an external library.

2. Pattern Matching

Pattern matching appears to be a convenient way of parsing arguments in Scala for simple scenarios:

val usage = """
  Usage: patternmatching [--arg1 num] [--arg2 num] filename
"""
def main(args: Array[String]) {
  if (args.length == 0) println(usage)

  def nextArg(map: Map[String, Any], list: List[String]): Map[String, Any] = {
    list match {
      case Nil => map
      case "--arg1" :: value :: tail =>
        nextArg(map ++ Map("arg1" -> value.toInt), tail)
      case "--arg2" :: value :: tail =>
        nextArg(map ++ Map("arg2" -> value.toInt), tail)
      case string :: Nil =>
        nextArg(map ++ Map("filename" -> string), list.tail)
      case unknown :: _ =>
        println("Unknown option " + unknown)
        exit(1)
    }
  }
  val options = nextArg(Map(), args.toList)
  println(options)
}

On the negative side, it’s important here to note that this solution isn’t flexible, and for every new argument, the implementation must change to parse it.

3. List Sliding

Another way to parse the arguments array is to use the sliding function. Essentially, this implementation splits the array into tuples and processes the tuples in the same way that the pattern matching solution does.

Let’s see the code:

val usage = """
  Usage: sliding [--arg1 num] [--arg2 num] [--filename filename]
"""

def main(args: Array[String]): Unit = {

  if (args.isEmpty || args.length % 2 != 0) {
    println(usage)
    exit(1)
  }

  val argMap = Map.newBuilder[String, Any]
  args.sliding(2, 2).toList.collect {
    case Array("--arg1", arg1: String) => argMap.+=("arg1" -> arg1)
    case Array("--arg2", arg2: String) => argMap.+=("arg2" -> arg2)
    case Array("--filename", filename: String) =>
      argMap.+=("filename" -> filename)
  }
  println(argMap.result())
}

Similar to the pattern matching solution, this solution is not general and every new argument has to be handled independently.

4. Scopt

Scopt is a command-line option parsing library that can be used for more complex and dynamic scenarios. Unlike custom solutions, Scopt offers options like default values, abbreviations, list parsing, help printing, option validation, and more. Notably, Scopt provides both functional and object-oriented DSLs.

To use Scopt, we first need to add the dependency:

libraryDependencies += "com.github.scopt" %% "scopt" % "4.0.1"

The next snippet demonstrates some of the features using the functional DSL:

case class Config(
  argA: Int = -1,
  argB: Int = -1,
  debug: Boolean = false,
  string: String = "",
  list: Seq[String] = Seq(),
  map: Map[String, String] = Map(),
  command1: String = "",
  cmdArg: Int = 0
)

val builder = OParser.builder[Config]
val argParser = {
  import builder._
  OParser.sequence(
    programName("myprog"),
    head("myprog", "0.1"),
    opt[Int]('a', "argA")
      .required()
      .action((a, c) => c.copy(argA = a))
      .text("required integer"),
    opt[Int]('b', "argB")
      .action((b, c) => c.copy(argB = b))
      .validate(b => {
        if (b >= 10) {
          success
        } else {
          failure("just cause")
        }
      })
      .text("optional integer"),
    opt[Boolean]('d', "debug")
      .action((d, c) => c.copy(debug = d))
      .text("optional boolean"),
    opt[String]('s', "string")
      .action((s, c) => c.copy(string = s))
      .text("optional string"),
    opt[Seq[String]]('l', "list")
      .valueName("<v1>,<v2>")
      .action((l, c) => c.copy(list = l))
      .text("string list"),
    opt[Map[String, String]]('m', "map")
      .valueName("<k1>=<v1>,<k2>=<v2>")
      .action((m, c) => c.copy(map = m))
      .text("optional map"),
    cmd("command1")
      .action((_, c) => c.copy(command1 = "command1"))
      .children(
        opt[Int]("cmdArg")
          .action((cmdArg, c) => c.copy(cmdArg = cmdArg))
          .text("command argument")
      ),
    checkConfig(c => {
      if (c.argA < c.argB) {
        success
      } else {
        failure("just cause")
      }
    })
  )

}

def main(args: Array[String]): Unit = {
  OParser.parse(argParser, args, Config()) match {
    case Some(config) =>
      println(OParser.usage(argParser))
      // do stuff with config
    case _ =>
      exit(1)
  }
}

Let’s see the output of the OParser.usage function:

myprog 0.1
Usage: myprog [command1] [options]

  -a, --argA <value>       required integer
  -b, --argB <value>       optional integer
  -d, --debug <value>      optional boolean
  -s, --string <value>     optional string
  -l, --list <v1>,<v2>     string list
  -m, --map <k1>=<v1>,<k2>=<v2>
                           optional map
Command: command1 [options]

  --cmdArg <value>         command argument

5. Scallop

Scallop is, yet again, another parsing library. While Scallop supports many features that Scopt supports, it doesn’t have a functional DSL.

The dependency needed for Scallop is:

libraryDependencies += "org.rogach" %% "scallop" % "5.0.0"

Let’s see a basic example of Scallop parsing:

class Conf(arguments: Seq[String]) extends ScallopConf(arguments) {
  val high = opt[Int](required = true)
  val low = opt[Int]()
  val name = trailArg[String]()
  verify()
}

def main(args: Array[String]): Unit = {
  val conf = new Conf(args)
  println("high is: " + conf.high())
  println("low is: " + conf.low())
  println("name is: " + conf.name())
}

Furthermore, Scallop supports powerful pattern matching for trailing arguments. For example:

object ScallopPatternMatching {

  class Conf(args: Seq[String])
    extends ScallopConf(args) {
    val propsMap = props[String]('P')
    val firstString = trailArg[String]()
    val firstList = trailArg[List[Int]]()
    val secondString = trailArg[String]()
    val secondList = trailArg[List[Double]]()
    verify()
  }

  def main(args: Array[String]): Unit = {
    val conf = new Conf(args)
    println("propsMap.key1 is: " + conf.propsMap("key1"))
    println("propsMap.key2 is: " + conf.propsMap("key2"))
    println("propsMap.key3 is: " + conf.propsMap("key3"))
    println("firstString is: " + conf.firstString())
    println("firstList is: " + conf.firstList())
    println("secondString is: " + conf.secondString())
    println("secondList is: " + conf.secondList())
  }
}

The above snippet parses the following arguments:

scala MyProg.scala -Pkey1=value1 key2=value2 key3=value3 first 1 2 3 second 4 5 6

6. Clist

Clist is yet another parsing library. The syntax and the features are very similar to what Scallop offers except for the powerful matching for trailing arguments.

Let’s add the Clist dependency to the build.sbt file:

libraryDependencies += "org.backuity.clist" %% "clist-core"   % "3.5.1"
libraryDependencies += "org.backuity.clist" %% "clist-macros" % "3.5.1" % "provided"

In the following example, we parse required, optional, and list arguments:

class Config extends Command("") {
  var apples = arg[Int](description = "apples count")
  var oranges = opt[Option[Int]](description = "oranges count")
  var debug = opt[Boolean](description = "debug flag", abbrev = "d")
  var list = opt[Option[Seq[String]]](description = "a list of strings")
}

object Clist {
  def main(args: Array[String]): Unit = {
    Cli.parse(args).withCommand(new Config) { config =>
      println(config.description)
    }
  }
}

Let’s look at the detailed description that is printed:

Usage

  [options] <apples>

Options

   -d, --debug : debug flag
   --list      : a list of strings
   --oranges   : oranges count

Arguments

   <apples> : apples count

7. Args4j

Unlike the previous libraries, Args4j uses annotations instead of a functional or object-oriented DSL. As the name implies Args4j is a Java library, hence the annotation use.

Let’s include the dependency for Args4j:

libraryDependencies += "args4j" % "args4j" % "2.33"

Below, we illustrate how to parse optional and required arguments:

object Args {
  @Option(name = "-bananas", required = true, usage = "bananas count")
  var bananas: Int = -1

  @Option(name = "-apples", usage = "apples count")
  var apples: Int = -1

  @Option(name = "-filename", usage = "file name")
  var filename: String = null

}

object Args4J {

  def main(args: Array[String]): Unit = {
    val parser = new CmdLineParser(Args)
    try {
      parser.parseArgument(JavaConverters.asJavaCollection(args))
    } catch {
      case e: CmdLineException =>
        print(s"Error:${e.getMessage}\n Usage:\n")
        parser.printUsage(System.out)
        exit(1)
    }
    println(Args.apples)
    println(Args.bananas)
    println(Args.filename)
  }

}

8. Conclusion

In this article, we demonstrated various ways to parse command-line arguments in Scala.

Of course, every solution comes with pros and cons. We hope that this article may help you the next time you have to parse command-line arguments.

As always, the code of the above examples is available over on GitHub.