Skip to content

deanwampler/command-line-arguments

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Command Line Arguments

Dean Wampler, Ph.D. @deanwampler

This is a Scala library for handling command-line arguments. It has no dependencies on other libraries (other than ScalaTest), so its footprint is small.

Usage

This library is built for Scala 2.11.4. Artifacts are published to Sonatype's OSS service. You'll need the following settings.

resolvers ++= Seq(
  Resolver.sonatypeRepo("releases"),
  Resolver.sonatypeRepo("snapshots")
)
...

scalaVersion := "2.11.4"

libraryDependencies ++= Seq(
  "com.concurrentthought.cla" %% "command-line-arguments" % "0.2.0"
)

API

The included com.concurrentthought.cla.CLASampleMain shows two different idiomatic ways to set up and use the API.

The simplest approach parses a multi-line string to specify the command-line arguments com.concurrentthought.cla.Args:

import com.concurrentthought.cla._

object CLASampleMain {

  def main(argstrings: Array[String]) = {
    val args: Args = """
      |run-main CLASampleMain [options]
      |Demonstrates the CLA API.
      |  -i | --in  | --input      string              Path to input file.
      |  -o | --out | --output     string=/dev/null    Path to output file.
      |  -l | --log | --log-level  int=3               Log level to use.
      |  -p | --path               path                Path elements separated by ':' (*nix) or ';' (Windows).
      |       --things             seq([-|])           String elements separated by '-' or '|'.
      |  -q | --quiet              flag                Suppress some verbose output.
      |                            others              Other arguments.
      |""".stripMargin.toArgs

    process(args, argstrings)
  }
  ...

The Scaladocs comments for the cla package explain the format and its limitations, but hopefully most of the format is reasonable intuitive from the example.

The first lines of the string that don't have leading whitespace are interpreted as lines to show as part of the corresponding help message, including an example of how to invoke the program and zero or more additional descriptions.

Next come the command-line options, one per line. Each must start with whitespace, followed by zero or more flags separated by |. There can be at most one option that has no flags. It is used to provide a help message for how command-line tokens that aren't associated with flags will be interpreted. (Note that the library will still handle these tokens whether or not you specify a line like this.)

The center "column" specifies the type of the option and an optional default value, which is indicated with an equals = sign. The following "types" are supported:

String Interpretation Corresponding Helper Method Default Values Supported?
flag Boolean value Flag (case class) false (note 1)
~flag Boolean value Flag (case class) true (note 1)
string String value Opt.string yes
byte Byte value Opt.byte yes
char Char value Opt.char yes
int Int value Opt.int yes
long Long value Opt.long yes
float Float value Opt.float yes
double Double value Opt.double yes
path "path-like" Seq[String] (note 2) Opt.path no (note 3)
seq Seq[String] (note 2) Opt.seqString no (note 3)
other Only allowed for the single, no-flags case Args.remainingOpt no (note 3)
  • Note 1: Both flag and ~flag represent Boolean flags where no value is supplied (e.g., --help). While flag defaults to false if not specified, ~flag ("tilde" or "not" flag) defaults to true.
  • Note 2: Both path and seq split an argument using the delimiter regex. For path, this is the platform-specific path separator, given by sys.props.getOrElse("path.separator", ":"). For seq, you must provide the delimiter regex using a suffix of the form (delimRE), as shown in the example.
  • Note 3: It's an implementation limitation that default values can't be specified using this approach. You can do this if you build the Args with the API, as shown below.

So, when an option expects something other than a String, the token given on the command line (or as a default here) will be parsed into the correct type, with error handling captured in the Args.failures field.

Finally, the rest of the text on a line is the help message for the option.

Before discussing the process method shown, let's see two alternative, programmatic ways to declare Args using the API:

  ...
  def main2(argstrings: Array[String]) = {
    val input  = Opt.string(
      name     = "input",
      flags    = Seq("-i", "--in", "--input"),
      help     = "Path to input file.")
    val output = Opt.string(
      name     = "output",
      flags    = Seq("-o", "--out", "--output"),
      default  = Some("/dev/null"),
      help     = "Path to output file.")
    val logLevel = Opt.int(
      name     = "log-level",
      flags    = Seq("-l", "--log", "--log-level"),
      default  = Some(3),
      help     = "Log level to use.")
    val path = Opt.seqString(delimsRE = "[:;]")(
      name     = "path",
      flags    = Seq("-p", "--path"),
      help     = "Path elements separated by ':' (*nix) or ';' (Windows).")
    val others = Args.makeRemainingOpt(
      name     = "others",
      help     = "Other arguments")

    val args = Args("run-main CLASampleMain [options]", "Demonstrates the CLA API.",
      Seq(input, output, logLevel, path, Args.quietFlag, others)).parse(argstrings)

    process(args, argstrings)
  }
  ...
}

Each option is defined using a com.concurrentthought.cla.Opt value. In this case, there are helper methods in the Opt companion object for constructing options where the values are strings or numbers. The string and int helpers are used here for String and Int arguments, respectively).

The arguments to each of these helpers (and also for Opt[V].apply() that they invoke) is the option name, used to retrieve the value later, a Seq of flags for command line invocation, an optional default value if the command-line argument isn't used, and a help string for the option.

There are also two helpers for command-line arguments that are strings that contain sequences of elements. We use one of them here, seqString, for a classpath-style argument, where the elements will be split into a Seq[String], using : and ; as delimiters; the first argument is a regular expression for the delimiter. If you want to support a path-like option, e.g., a CLASSPATH, there is another, even more specific helper, Opt.path, that handles the platform-specific value for the path-element separator.

There is also a more general seq[V] helper, where the string is first split, then parsed into V instances. See Opt.seq[V] for more details.

The first two arguments to the Args.apply() method provide help strings. The first shows how to run the application, e.g., run-main CLASampleMain as shown, or perhaps java -cp ... foo.bar.Main, etc. The string is arbitrary. The second string is an optional description of the program. Finally, a Seq[Opt[V]] specifies the actual options supported. Note that we didn't define a Flag for quiet, as in the first example, instead we used a built-in flag Args.quietFlag.

Here is a slightly more concise way to write the content in main2:

  ...
  def main3(argstrings: Array[String]) = {
    import Opt._
    import Args._
    val args = Args("run-main CLASampleMain [options]", "Demonstrates the CLA API.",
      Seq(
        string("input",     Seq("-i", "--in", "--input"),      None,              "Path to input file."),
        string("output",    Seq("-o", "--out", "--output"),    Some("/dev/null"), "Path to output file."),
        int(   "log-level", Seq("-l", "--log", "--log-level"), Some(3),           "Log level to use."),
        seqString("[:;]")(
               "path",      Seq("-p", "--path"),               None,              "Path elements separated by ':' (*nix) or ';' (Windows)."),
        Args.quietFlag,
        makeRemainingOpt(
               "others",                                                          "Other arguments")))

    process(args, argstrings)
  }
  ...

This is more concise, but harder to follow.

The process method uses the Args. It first parses the user-specified arguments, returning a new Args instance with updated values for each argument.

  protected def process(args: Args, argstrings: Array[String]): Unit = {
    val parsedArgs = args.parse(argstrings)
    ...

If errors occurred or help was requested, print the appropriate messages and exit.

    ...
    if (parsedArgs.handleErrors()) sys.exit(1)
    if (parsedArgs.handleHelp())   sys.exit(0)
    ...

Otherwise, if --quiet wasn't specified, then start printing information.

First, print all the options and the current values for them, either the defaults or the user-specified values.

    ...
    if (parsedArgs.getOrElse("quiet", false)) {
      println("(... I'm being very quiet...)")
    } else {
      // Print all the default values or those specified by the user.
      parsedArgs.printValues()

      // Print all the values including repeats.
      parsedArgs.printAllValues()

      // Repeat the "other" arguments (not associated with flags).
      println("\nYou gave the following \"other\" arguments: " +
        parsedArgs.remaining.mkString(", "))
      ...

What's the difference between printValues and printAllValues. They address the case where the user should be able to repeat some options, for example, multiple sources of input, while other examples should only be used once. To simplify handling, the API remembers all occurrences of an option on the command line. The method printAllValues and the corresponding getAll and getAllOrElse methods print or return all occurrences seen, respectively. So, if you want an option to be repeatable, retrieve the results with getAll or getAllOrElse. Otherwise, use get and getOrElse, which return the last occurrence of an option (or the default, if any). This supports the common practice in POSIX systems of allowing subsequent option occurrences to override previous occurrences on a command line.

Finally, we extract some other values and "use" them.

    ...
      showPathElements(parsedArgs.get[Seq[String]]("path"))
      showLogLevel(parsedArgs.getOrElse("log-level", 0))
      println
    }
  }

  protected def showPathElements(path: Option[Seq[String]]) = path match {
    case None => println("No path elements to show!")
    case Some(seq) => println(s"Setting path elements to $seq")
  }

  protected def showLogLevel(level: Int) =
    println(s"New log level: $level")
}

The get[V] method returns values of the expected type. It uses asInstanceOf[] internally, but it should never fail because the parsing process already converted the value to the correct type (and then put it in a Map[String,Any] used by get[V]).

Note that an advantage of getOrElse[V] is that its type parameter can be inferred due to the second argument.

Try running the following examples within SBT (run and run-main com.concurrentthought.cla.CLASampleMain do the same thing):

 run-main com.concurrentthought.cla.CLASampleMain -h
 run -h
 run --help
 run -i /in -o /out -l 4 -p a:b --things x-y|z foo bar baz
 run -i /in -o /out -l 4 -p a:b --things x-y|z foo bar baz --quiet
 run --in /in --out=/out -l=4 --path "a:b" --things=x-y|z foo bar baz

The last example mixes flag value and flag=value syntax, which of are both supported.

Try a few runs with unknown flags and other errors. Note the error handling that's done, such as when you omit a value expected by a flag, or you provide an invalid value, such as --log-level foo.

About

A simple Scala library for processing command-line arguments

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages