org.sellmerfud.optparse

OptionParser

class OptionParser[C] extends AnyRef

Overview

OptionParser is a class that handles the parsing of switches and arguments on the command line. It is based on the Ruby OptionParser class that is part of the standard Ruby library. It supports POSIX style short option switches as well as GNU style long option switches. By using closures when defining command line switches your code becomes much easier to write and maintain.

Features

Dependencies

This code requires Scala 2.10 as it relies on scala.reflect.ClassTag.

Configuration Class

The OptionParser class has a single type parameter. This type parameter specifies the configuration class that your code will use to aggregate the command line switches and arguments. Each time a command line switch or argument is processed a function is called with your configuration class. The function should modify the configuration class or preferably construct a new immutable instance of the class and return it. The command line parser will return the configuration class upon successfully parsing the command line.

Defining Switches

You define a switch by supplying its name(s), description, and a function that will be called each time the switch is detected in the list of command line arguments. Your function has the type { (value: T, cfg: C) => C }. You supply the type for T and the framework will select the appropriate parser and call your function with the value converted to your expected type. The type C is your configuration class that was specified when the OptionParser was instantiated.

case class Config(revision: Int = 0, args: Vector[String] = Vector.empty)
val cli = new OptionParser[Config] {
  reqd[Int]("-r", "--revision NUM", "Choose revision") { (value, cfg) => cfg.copy(revision = value) }
  arg[String] { (arg, cfg) => cfg.copy(args = cfg.args :+ arg) }
}
val config = cli.parse(List("-r", "9"), Config())

The reqd() function defines a switch that takes a required argument. In this case we have specified that we expect the argument value to be an Int. If the user enters a value that is not a valid integer then an org.sellmerfud.optparse.OptionParserException is thrown with an appropriate error message. If the value is valid then our supplied function is called with the integer value. Here we return a copy of our configuration class with the revision field updated.

Non-Switch Arguments

Anything encountered on the command line that is not a switch or an argument to a switch is passed to the function supplied to the arg() method. Like switch arguments, the non-switch arguments can be of any type for which you have a defined argument parser. In the example above we have specified that non-switch arguments are of type String.

Switch Names

A switch may have a short name, a long name, or both.

Short names may be specified as a single character preceded by a single dash. You may optionally append an argument name separated by a space. The argument name is only for documentation purposes and is displayed with the help text.

-t           <== no argument
-t ARG       <== required argument
-t [VAL]     <== optional argument

Long names may be any number of characters and are preceded by two dashes. You may optionally append an argument name. The argument name may be separated by a space or by an equals sign. This will affect how the name is displayed in the help text.

--quiet
--revision REV
--revision=REV
--start-date [TODAY]
--start-date=[TODAY]
--start-date[=TODAY]

Notice that in the case of an optional parameter you may put the equal sign inside or outside the bracket. Again this only affects how it is dispalyed in the help message. If you specify an argument name with both the short name and the long name, the one specified with the long name is used.

There is a boolean switch that does not accept a command line argument but may be negated when using the long name by preceding the name with no-. For example:

cli.bool("-t", "--timestamp", "Generate a timestamp") { (v, c) => c.copy(genTimestamp = v) }

can be specified on the command line as:
  -t                <== function called with v == true
  --timestamp       <== function called with v == true
  --no-timestamp    <== function called with v == false
  --no-t            <== function called with v == false  (using partial name)

Notice that you only specify the positive form of the name when defining the switch. The help text for this switch looks like this:

-t, --[no-]timestamp            Generate a timestamp

Special Tokens

Switch Types

You can define switches that take no arguments, an optional argument, or a required argument.

- Flag
      cli.flag("-x", "--expert", "Description") { (cfg) => ... }

- Boolean
      cli.bool("-t", "--timestamp", "Description") { (v, cfg) => ... }

- Required Argument
      cli.reqd[String]("-n", "--name=NAME", "Description") { (v, cfg) => ... }

- Optional Argument
      cli.optl[String]("-d", "--start-date[=TODAY]", "Description") { (v, cfg) => ... }

- Comma Separated List
      cli.list[String]("-b", "--branches=B1,B2,B3", "Description") { (v, cfg) => ... }

Limiting Values

For switches that take arguments, either required or optional, you can specify a list of acceptable values.

case class Config(color: String = "red")
cli.reqd[String]("", "--color COLOR", List("red", "green", "blue")) { (v, c) => c.copy(color = v) }

Here if the user enters --color purple on the command line an org.sellmerfud.optparse.OptionParserException is thrown. The exception message will display the accepable values. Also the user can enter partial values.

coolapp --color r     // <==  Will be interpreted as red

If the value entered matches two or more of the acceptable values then an org.sellmerfud.optparse.OptionParserException is thrown with a message indicating that the value was ambiguous and displays the acceptable values. Also note that you can pass a Map instead of a List if you need to map string values to some other type.

class Color(rgb: String)
val red   = new Color("#FF0000")
val green = new Color("#00FF00")
val blue  = new Color("#0000FF")
case class Config(color: Color = red)
cli.optl[Color]("", "--color [ARG]", Map("red" -> red, "green" -> green, "blue" -> blue)) {
  (v, c) =>
  c.copy(color = v getOrElse red)
}

Since we are defining a switch with an optional argument, the type of v is Option[Color].

Banner, Separators and Help Text

You can specify a banner which will be the first line displayed in the help text. You can also define separators that display information between the switches in the help text.

case class Config(...)
val cli = new OptionParser[Config]
cli.banner = "coolapp [Options] file..."
cli.separator("")
cli.separator("Main Options:")
cli.flag("-f", "--force", "Force file creation") { (c) => ... }
cli.reqd[String]("-n NAME", "", "Specify a name") { (v, c) => ... }
cli.separator("")
cli.separator("Other Options:")
cli.optl[String]("", "--backup[=NAME]", "Make a backup", "--> NAME defaults to 'backup'") { (v, c) => ... }
cli.bool("-t", "--timestamp", "Create a timestamp") { (v, c) => ... }
println(cli)  // or println(cli.help)

Would print the following:
coolapp [Options] file...

Main Options:
    -f, --force                  Force file creation
    -n NAME                      Specify a name

Other Options:
        --backup[=FILE]          Make a backup
                                 --> FILE defaults to 'backup'
    -t, --[no-]timestamp         Create a timestamp
    -h, --help                   Show this message

Where did the -h, --help entry come from? By default the -h switch is added automatically. The function associated with it will print the help text to stdout and call java.lang.System.exit(0). You can define your own help switch by simply defining a switch with either or both of the names -h, --help. You can also turn off the auto help altogether.

How Short Switches Are Parsed

Short switches encountered on the command line are interpreted as follows:

Assume that the following switches have been defined:
   -t, --text   (Takes no argument)
   -v           (Takes no argument)
   -f FILE      (Requires an argument)
   -b [OPT]     (Takes an optional argument)

Switches that do not accept arguments may be specified separately or may be concatenated together:
   -tv  ==  -t -v

A switch that takes an argument may be concatenated to one or more switches that do not take
arguments as long as it is the last switch in the group:
    -tvf foo.tar  ==  -t -v -f foo.tar
    -tfv foo.tar  ==  -t -f v foo.tar  (v is the argument value for the -f switch)

The argument for a switch may be specified with or without intervening spaces:
    -ffoo.tar  == -f foo.tar

For arguments separated by space, switches with required arguments are greedy while those that take
optional arguments are not. They will ignore anything that looks like a another switch.
   -v -f -t       <-- The -f option is assigned the value "-t"
   -v -f -text    <-- The -f option is assigned the value "-text"
   -v -f --text   <-- The -f option is assigned the value "--text"

   -v -b t        <-- The -b option is assigned the value "t"
   -v -b -t       <-- The -b option is interpreted without an argument
   -v -b -text    <-- The -b option is interpreted without an argument
   -v -b --text   <-- The -b option is interpreted without an argument
   -v -b-text     <-- The -b option is assigned the value "-text" (no intervening space)

How Long Switches Are Parsed

Long swithes encountered on the command line are interpreted as follows:

Assume that the following switches have been defined:
   --timestamp       (Boolean - takes no argument)
   --file FILE       (Requires an argument)
   --backup[=BACKUP] (Takes an optional argument)

The argument for a switch may be joined by and equals sign or may be separated by space:
    --file=foo.tar == --file foo.tar
    --backup=data.bak == --backup data.bak

For arguments separated by space, switches with required arguments are greedy while those that take
optional arguments are not. They will ignore anything that looks like a another switch. See the
discussion of short switches above for an example.  The behavior for long switches is identical.

Boolean switches may be negated.
    --timestamp      <-- The option is assigned a true value
    --no-timestamp   <-- The option is assigned a false value

Full Example

import java.util.Date
import java.io.File
import java.text.{SimpleDateFormat, ParseException}
import org.sellmerfud.optparse._

object Sample {
  val dateFormat = new SimpleDateFormat("MM-dd-yyyy")

  def main(args: Array[String]) {
    case class Config(
      quiet:    Boolean        = false,
      expert:   Boolean        = false,
      name:     Option[String] = None,
      fileType: String         = "binary",
      base:     String         = "HEAD",
      date:     Date           = new Date(),
      libs:     List[String]   = Nil,
      fileArgs: Vector[String] = Vector.empty)

    val config = try {
      new OptionParser[Config] {
        // Add an argument parser to handle date values
        addArgumentParser[Date] { arg =>
          try   { dateFormat.parse(arg) }
          catch { case e: ParseException => throw new InvalidArgumentException("Expected date in mm-dd-yyyy format") }
        }
        banner = "coolapp [options] file..."
        separator("")
        separator("Options:")
        bool("-q", "--quiet", "Do not write to stdout.")
          { (v, c) => c.copy(quiet = v) }

        flag("-x", "", "Use expert mode")
          { (c) => c.copy(expert = true) }

        reqd[String]("-n ", "", "Enter you name.")
          { (v, c) => c.copy(name = Some(v)) }

        reqd[File]("-l", "--lib=", "Specify a library. Can be used mutiple times.")
          { (v, c) => c.copy(libs = c.libs :+ v) }

        reqd[Date]("-d", "--date ", "Enter date in mm-dd-yyyy format.")
          { (v, c) => c.copy(date = v) }

        reqd[String]("-t", "--type=", List("ascii", "binary"), "Set the data type. (ascii, binary)")
          { (v, c) => c.copy(fileType = v) }

        optl[String]("-b", "--base[=]", "Set the base commit. Default is HEAD.")
          { (v, c) => c.copy(base = v getOrElse "HEAD") }

        arg[String] { (v, c) => c.copy(fileArgs = c.fileArgs :+ v) }
      }.parse(args, Config())
    }
    catch { case e: OptionParserException => println(e.getMessage); java.lang.System.exit(1) }

    println("config: " + config)
  }
}

Command Line: -l /etc/foo --lib=/tmp/bar -x .profile -n Bob -d09-11-2001
-------------------------------------------------------------------------------
config: Config(false,true,Some(Bob),binary,HEAD,Tue Sep 11 00:00:00 CDT 2001,
               List(/etc/foo, /tmp/bar), Vector(.profile))

Command Line: --date=04/01/2011
-------------------------------------------------------------------------------
invalid argument: --date=04/01/2011   (Expected date in mm-dd-yyyy format)

Command Line: --ty=ebcdic
-------------------------------------------------------------------------------
invalid argument: --ty ebcdic    (ascii, binary)

Command Line: --ty=a
-------------------------------------------------------------------------------
config: Config(false,false,None,ascii,HEAD,Mon Feb 11 21:52:12 CST 2013,
               List(), Vector())
Linear Supertypes
AnyRef, Any
Ordering
  1. Alphabetic
  2. By inheritance
Inherited
  1. OptionParser
  2. AnyRef
  3. Any
  1. Hide All
  2. Show all
Learn more about member selection
Visibility
  1. Public
  2. All

Instance Constructors

  1. new OptionParser()

Value Members

  1. final def !=(arg0: Any): Boolean

    Definition Classes
    AnyRef → Any
  2. final def ##(): Int

    Definition Classes
    AnyRef → Any
  3. final def ==(arg0: Any): Boolean

    Definition Classes
    AnyRef → Any
  4. def addArgumentParser[T](f: (String) ⇒ T)(implicit m: ClassTag[T]): Unit

    Add an argument parser for a specific type.

    Add an argument parser for a specific type.

    Parsers for the these types are provided by default:

    • String
    • Int
    • Short
    • Long
    • Float
    • Double
    • Char
    • java.io.File

    A parser is simply a function that takes a single String argument and returns a value of the desired type. If your parser detects an invalid argument it should throw an org.sellmerfud.optparse.InvalidArgumentException. You can supply a message in the exception that indicates why the argument was not valid.

    If you add a parser for a type that already has a parser, the existing parser will be replaced.

  5. def arg[T](func: (T, C) ⇒ C)(implicit m: ClassTag[T]): Unit

    Provide a function that will be called with each non-switch argument as it is encountered on the command line.

  6. final def asInstanceOf[T0]: T0

    Definition Classes
    Any
  7. var auto_help: Boolean

    Set this to false to avoid the automatically added help switch.

    Set this to false to avoid the automatically added help switch.

    The action for the added help switch is to print the help text to stdout and then call java.lang.System.exit(0).

    You can also override the default help switch by adding your own switch with a short name of "-h" or a long name of "--help".

  8. var banner: String

    Set the banner that is displayed as the first line of the help text.

  9. def bool(short: String, long: String, info: String*)(func: (Boolean, C) ⇒ C): Unit

    Define a boolean switch.

    Define a boolean switch. This switch takes no arguments. The long form of the switch may be prefixed with no- to negate the switch. For example a switch with long name --expert could be specified as --no-expert on the command line.

  10. def clone(): AnyRef

    Attributes
    protected[java.lang]
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  11. final def eq(arg0: AnyRef): Boolean

    Definition Classes
    AnyRef
  12. def equals(arg0: Any): Boolean

    Definition Classes
    AnyRef → Any
  13. def finalize(): Unit

    Attributes
    protected[java.lang]
    Definition Classes
    AnyRef
    Annotations
    @throws( classOf[java.lang.Throwable] )
  14. def flag(short: String, long: String, info: String*)(func: (C) ⇒ C): Unit

    Define a switch that takes no arguments.

  15. final def getClass(): Class[_]

    Definition Classes
    AnyRef → Any
  16. def hashCode(): Int

    Definition Classes
    AnyRef → Any
  17. def help: String

    Returns the formatted help text as a String

  18. final def isInstanceOf[T0]: Boolean

    Definition Classes
    Any
  19. def list[T](short: String, long: String, info: String*)(func: (List[T], C) ⇒ C)(implicit m: ClassTag[T]): Unit

    Define a switch that takes a comma separated list of arguments.

  20. final def ne(arg0: AnyRef): Boolean

    Definition Classes
    AnyRef
  21. final def notify(): Unit

    Definition Classes
    AnyRef
  22. final def notifyAll(): Unit

    Definition Classes
    AnyRef
  23. def optl[T](short: String, long: String, vals: Map[String, T], info: String*)(func: (Option[T], C) ⇒ C)(implicit m: ClassTag[T]): Unit

    Define a switch that takes an optional argument where the valid values are given by a Map.

  24. def optl[T](short: String, long: String, vals: Seq[T], info: String*)(func: (Option[T], C) ⇒ C)(implicit m: ClassTag[T]): Unit

    Define a switch that takes an optional argument where the valid values are given by a Seq[].

  25. def optl[T](short: String, long: String, info: String*)(func: (Option[T], C) ⇒ C)(implicit m: ClassTag[T]): Unit

    Define a switch that takes an optional argument.

  26. def parse(args: Seq[String], config: C): C

    Parse the given command line.

    Parse the given command line. Each token from the command line should be in a separate entry in the given sequence such as the array of strings passed to def main(args: Array[String]) {}. The option switches are processed using the previously defined switches. All non-switch arguments are returned as a list of strings.

    If any problems are encountered an org.sellmerfud.optparse.OptionParserException is thrown.

  27. def reqd[T](short: String, long: String, vals: Map[String, T], info: String*)(func: (T, C) ⇒ C)(implicit m: ClassTag[T]): Unit

    Define a switch that takes a required argument where the valid values are given by a Map.

  28. def reqd[T](short: String, long: String, vals: Seq[T], info: String*)(func: (T, C) ⇒ C)(implicit m: ClassTag[T]): Unit

    Define a switch that takes a required argument where the valid values are given by a Seq[].

  29. def reqd[T](short: String, long: String, info: String*)(func: (T, C) ⇒ C)(implicit m: ClassTag[T]): Unit

    Define a switch that takes a required argument.

  30. def separator(text: String): Unit

    Add a message to the help text.

    Add a message to the help text.

    It will be displayed at the left margin after any previously defined switches/separators.

  31. final def synchronized[T0](arg0: ⇒ T0): T0

    Definition Classes
    AnyRef
  32. def toString(): String

    Same as calling help.

    Same as calling help.

    Definition Classes
    OptionParser → AnyRef → Any
  33. final def wait(): Unit

    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  34. final def wait(arg0: Long, arg1: Int): Unit

    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  35. final def wait(arg0: Long): Unit

    Definition Classes
    AnyRef
    Annotations
    @throws( ... )

Inherited from AnyRef

Inherited from Any

Ungrouped