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.
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"
)
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
representBoolean
flags where no value is supplied (e.g.,--help
). Whileflag
defaults tofalse
if not specified,~flag
("tilde" or "not" flag) defaults totrue
. - Note 2: Both
path
andseq
split an argument using the delimiter regex. Forpath
, this is the platform-specific path separator, given bysys.props.getOrElse("path.separator", ":")
. Forseq
, 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
.