views:

1562

answers:

5

What's the best way to parse command-line parameters in Scala? I personally prefer something lightweight that does not require external jar.

Related:

+17  A: 

Mailing list thread Scala CLI Library? lists

paulp/optional

By using reflection it automatically calls the main with optional argument:

object MyAwesomeCommandLineTool extends optional.Application {
  // for instance...
  def main(count: Option[Int], file: Option[java.io.File], arg1: String) {
    [...]
  }
}

In terms of the usage code, this is the most elegant, but it looks like it has a dependency on paranamer-1.3.jar. Also it requires inheriting from optional.Application.

jstrachan/scopt

val parser = new OptionParser {
  intOpt("f", "foo", "foo is an integer property", {v: Int => config.foo = v})
  opt("b", "bar", "bar is a string property", {v: String => config.bar = v})
  booleanOpt("x", "xyz", "xyz is a boolean property", {v: Boolean => config.xyz = v})
  arg("whatnot", "some argument", {v: String => config.whatnot = v})
}
if (parser.parse(args)) {
   // do stuff
}
else {
  // arguments are bad, usage message will have been displayed
}

This one looks the most promising to me. Clean usage without too much baggage. (Disclaimer: I now contribute to this project)

And there's also

parse-cmd's AScalaParserClass

def main(args: Array[String]) = {
  val helpString = " -p1 out.txt -p2 22 [ -p3 100 -p4 1200 ] "
  val pp = new ParseParms( helpString )
  pp.parm("-p1", "output.txt").rex("^.*\\.txt$").req(true)    // required
    .parm("-p2", "22","^\\d{2}$",true)        // alternate form, required
    .parm("-p3","100").rex("^\\d{3}$")                        // optional
    .parm("-p4","1200").rex("^\\d{4}$").req(false)            // optional

  val result = pp.validate( args.toList )
  println(  if( result._1 ) result._3  else result._2 )
  // result is a tuple (Boolean, String, Map)
  // ._1 Boolean; false: error String contained in ._2, Map in ._3 is empty
  //              true:  successful, Map of parsed & merged parms in ._3

  System.exit(0)
}

Single file. Very lightweight, but I don't like the whole builder pattern DSL thing compared to scopt.

Apache Felix Karaf's console support

I think this is for OSGi shells, but jstrachan told me about cool attribute notations by Apache Felix Karaf.

import org.apache.felix.gogo.commands.{Action, Option => option, Argument => argument, Command => command}
import org.osgi.service.command.CommandSession

@command(scope = "scalate", name = "jsp2ssp", description = "Converts JSP files to SSP files")
class JspConvert extends Runnable with Action {
  @argument(index = 0, name = "dir", description = "Root of the directory containing the JSP files.")
  var dir: File = new File(".")

  @option(name = "--extension", description = "Extension for output files")
  var outputExtension = ".ssp"
  @option(name = "--recursion", description = "The number of directroy levels to recusively scan file input files.")
  var recursionDepth = -1
}
eed3si9n
I like the builder pattern DSL much better, because it enables delegation of parameter construction to modules.
Daniel
+1  A: 

For most cases you do not need an external parser. Scala's pattern matching allows consuming args in a functional style. For example:

object MmlAlnApp {
  val usage = """
    Usage: mmlaln [--min-size num] [--max-size num] filename
  """
  def main(args: Array[String]) {
    if (args.length == 0) println(usage)
    val arglist = args.toList
    type OptionMap = Map[Symbol, Any]

    def nextOption(map : OptionMap, list: List[String]) : OptionMap = {
      def isSwitch(s : String) = (s(0) == '-')
      list match {
        case Nil => map
        case "--max-size" :: value :: tail =>
                               nextOption(map ++ Map('maxsize -> value.toInt), tail)
        case "--min-size" :: value :: tail =>
                               nextOption(map ++ Map('minsize -> value.toInt), tail)
        case string :: opt2 :: tail if isSwitch(opt2) => 
                               nextOption(map ++ Map('infile -> string), list.tail)
        case string :: Nil =>  nextOption(map ++ Map('infile -> string), list.tail)
        case option :: tail => println("Unknown option "+option) 
                               exit(1) 
      }
    }
    val options = nextOption(Map(),arglist)
    println(options)
  }
}

will print, for example:

Map('infile -> test/data/paml-aln1.phy, 'maxsize -> 4, 'minsize -> 2)

This version only takes one infile. Easy to improve on (by using a List).

Note also that this approach allows for concatenation of multiple command line arguments - even more than two!

pjotrp
A: 

This is largely a shameless clone of my answer to the Java question of the same topic. It turns out that JewelCLI is Scala-friendly in that it doesn't require JavaBean style methods to get automatic argument naming.

JewelCLI is a Scala-friendly Java library for command-line parsing that yields clean code. It uses Proxied Interfaces Configured with Annotations to dynamically build a type-safe API for your command-line parameters.

An example parameter interface Person.scala:

import uk.co.flamingpenguin.jewel.cli.Option

trait Person {
  @Option def name: String
  @Option def times: Int
}

An example usage of the parameter interface Hello.scala:

import uk.co.flamingpenguin.jewel.cli.CliFactory.parseArguments
import uk.co.flamingpenguin.jewel.cli.ArgumentValidationException

object Hello {
  def main(args: Array[String]) {
    try {
      val person = parseArguments(classOf[Person], (args : _*) )
      for (i <- 1 to (person times))
        println("Hello " + (person name))
    } catch {
      case e: ArgumentValidationException => println(e getMessage)
    }
  }
}

Save copies of the files above to a single directory and download the JewelCLI 0.6 JAR to that directory as well.

Compile and run the example in Bash on Linux/Mac OS X/etc.:

scalac -cp jewelcli-0.6.jar:. Person.scala Hello.scala
scala -cp jewelcli-0.6.jar:. Hello --name="John Doe" --times=3

Compile and run the example in the Windows Command Prompt:

scalac -cp jewelcli-0.6.jar;. Person.scala Hello.scala
scala -cp jewelcli-0.6.jar;. Hello --name="John Doe" --times=3

Running the example should yield the following output:

Hello John Doe
Hello John Doe
Hello John Doe
Alain O'Dea
One piece of fun in this you may notice is the (args : _*). Calling Java varargs methods from Scala requires this. This is a solution I learned from http://daily-scala.blogspot.com/2009/11/varargs.html on Jesse Eichar's excellent Daily Scala blog. I highly recommend Daily Scala :)
Alain O'Dea
+1  A: 

I've just found an extensive command line parsing library in scalac's scala.tools.cmd package.

See http://www.assembla.com/code/scala-eclipse-toolchain/git/nodes/src/compiler/scala/tools/cmd?rev=f59940622e32384b1e08939effd24e924a8ba8db

Pablo Lalloni
A: 

Shameless, shameless plug: Argot

Brian Clapper