com.concurrentthought.cla.Args.scala Maven / Gradle / Ivy
package com.concurrentthought.cla
import scala.util.control.NonFatal
import scala.util.{Try, Success, Failure}
import java.io.PrintStream
/**
* Contains the options defined, the current values, which are the defaults
* before parsing and afterwards are the defaults overridden by the actual
* invocation options, and contains any parsing errors that were found, which
* is empty before parse is called. In order to properly construct the default
* values, this constructor is protected. Instead, use the companion Args.apply
* to construct initial instances correctly. Subsequent calls to `Args.parse` return
* new, updated instances.
*/
final case class Args protected (
programInvocation: String,
leadingComments: String,
trailingComments: String,
opts: Seq[Opt[_]],
defaults: Map[String,Any],
values: Map[String,Any],
allValues: Map[String,Seq[Any]],
remaining: Seq[String],
failures: Seq[(String,Any)]) {
def help: String = Help(this)
val requiredOptions = opts.filter(o => o.isRequired)
protected val remainingOpt = opts.find(_.flags.size == 0).getOrElse(
throw new IllegalStateException(s"BUG. There should be at least one 'remaining opts' Opt. opts = ${opts.mkString(", ")}"))
protected val remainingOptName = remainingOpt.name
protected val opts2: Seq[Opt[_]] = opts.filter(_.name != remainingOptName)
// We want the unknownOptionMatch to be just before the "remaining" option.
lazy val parserChain: Opt.Parser[_] =
opts2.map(_.parser)
.reduceLeft(_ orElse _)
.orElse(unknownOptionMatch)
.orElse(remainingOpt.parser)
/**
* Convenience method to parse the argument list and handle errors or
* help requests.
* If a parsing error occurs or help is requested, an appropriate message
* is printed to the `out` argument and the program exits with a call to
* `sys.exit(n)` with the integer exit returned by the sister `Args#process`
* method. Normally, the default values for the `out` and `exit` arguments
* are only overridden for testing.
* For more customized handling, {@see #parse}.
* @return Args
*/
def process(
args: Seq[String],
out: PrintStream = Console.out,
exit: Int => Unit = n => sys.exit(n)): Args = {
val newArgs = parse(args)
if (newArgs.handleHelp(out)) exit(0)
else if (newArgs.handleErrors(out)) exit(1)
newArgs
}
/**
* Parse the user-specified arguments, using `parserChain`. Note that if
* an unrecognized flag is found, i.e., a string that starts with one or two
* '-', it is an error. Otherwise, all unrecognized options are added to the
* resulting `values` in a `Seq[String]` with the key, "remaining".
* @return Args
*/
def parse(args: Seq[String]): Args = {
def callParserChain(seq: Seq[String]): ((String, Try[Any]), Seq[String]) = try {
parserChain(seq)
} catch {
case e @ Args.UnrecognizedArgument(head, tail) => ((head, Failure(e)), tail)
// Otherwise, assume that attempting to parse the value failed, but
// perhaps it's the next option?
case NonFatal(nf) => ((seq.head, Failure(nf)), seq.tail)
}
@annotation.tailrec
def p(args2: Seq[String], vect: Vector[(String, Any)] = Vector.empty): Vector[(String, Any)] = args2 match {
case Nil => vect
case seq => callParserChain(seq) match {
case ((flag, Success(value)), tail) => p(tail, vect :+ (flag -> value))
case ((flag, Failure(failure)), tail) => p(tail, vect :+ (flag -> failure))
}
}
val (failures, successes) = p(args) partition {
case (_, NonFatal(nf@_)) => true
case _ => false
}
// The "remaining" values aren't included in the values and allValues maps,
// but handled separately.
val newAllValues = allValues ++ successes.foldLeft(Map.empty[String,Vector[Any]]){
case (map, (key, value)) =>
val newVect = map.get(key) match {
case None => Vector(value)
case Some(v) => v :+ value
}
map + (key -> newVect)
} - remainingOptName
val newValues = values ++ successes.toMap - remainingOptName
// The remaining defaults are replaced by the new tokens:
val newRemaining = successes.filter(_._1 == remainingOptName).map(_._2.toString).toVector
val failures2 = resolveFailures(successes.map(_._1), failures)
copy(values = newValues, allValues = newAllValues, remaining = newRemaining, failures = failures2)
}
/** Ignore all parse errors if help was requested */
protected def resolveFailures(keys: Seq[String], failures: Seq[(String,Any)]): Seq[(String,Any)] = {
if (keys.contains(Args.HELP_KEY)) Nil
else {
val missing = requiredOptions.filter(o => !keys.contains(o.name))
if (missing.size == 0) failures
else {
val missingOpts = requiredOptions.filter(o => missing.contains(o))
failures ++ missingOpts.map(o => (o.name, Args.MissingRequiredArgument(o)))
}
}
}
import scala.reflect.ClassTag
/**
* Return the value for the option. This will be either the default specified,
* if the user did not invoke the option, or the _last_ invocation of the
* command line option. In other words, if the argument list contains
* `--foo bar1 --foo bar2`, then `Some("bar2")` is returned.
* Note: Use `remaining` to get the tokens not associated with a flag.
* @see getAll
*/
def get[V : ClassTag](flag: String): Option[V] =
values.get(flag).map(_.asInstanceOf[V])
/**
* Like `get`, but an alternative is specified, if no value for the option
* exists, so the return value is of type `V`, rather than `Option[V]`.
* Note: Use `remaining` to get the tokens not associated with a flag.
*/
def getOrElse[V : ClassTag](flag: String, orElse: V): V =
values.getOrElse(flag, orElse).asInstanceOf[V]
/**
* Return a `Seq` with all values specified for the option. This supports
* the case where an option can be repeated on the command line.
* If the user did not specify the option, then default is mapped to a
* return value as follows:
*
* - `None` => `Nil`
*
- `Some(x)` => `Seq(x)`
*
* If the user specified one or more invocations, then all of the values
* are returned in `Seq`. For example, for `--foo bar1 --foo bar2`, then
* this method returns `Seq("bar1", "bar2")`.
* Note: Use `remaining` to get the tokens not associated with a flag.
* @see get
*/
def getAll[V : ClassTag](flag: String): Seq[V] =
allValues.getOrElse(flag, Nil).map(_.asInstanceOf[V])
/**
* Like `getAll`, but an alternative is specified, if no value for exists.
* Note: Use `remaining` to get the tokens not associated with a flag.
*/
def getAllOrElse[V : ClassTag](flag: String, orElse: Seq[V]): Seq[V] =
allValues.getOrElse(flag, orElse).map(_.asInstanceOf[V])
/**
* Print the current values. Before any parsing is done, the values are
* the defaults. After parsing, they are the defaults overridden by any
* user-supplied options. If an option is specified multiple times, then
* the _last_ invocation is shown.
* Note that the "remaining" arguments are the same in this output and in
* `printAllValues`.
* @see printAllValues
*/
def printValues(out: PrintStream = Console.out): Unit =
doPrintValues(out, "")(
key => values.getOrElse(key, ""))
/**
* Print all the current values. Before any parsing is done, the values are
* the defaults. After parsing, they are the defaults overridden by all the
* user-supplied options. If an option is specified multiple times, then
* all values are shown.
* @see printValues
*/
def printAllValues(out: PrintStream = Console.out): Unit =
doPrintValues(out, " (all values given)")(
key => allValues.getOrElse(key, Vector.empty[String]))
private def doPrintValues[V](out: PrintStream, suffix: String)(get: String => V): Unit = {
out.println(s"\nCommand line arguments$suffix:")
val keys = opts.map(_.name)
val max = keys.maxBy(_.size).size
val fmt = s" %${max}s: %s"
keys.filter(_ != remainingOptName).foreach(key => out.println(fmt.format(key, get(key))))
out.println(fmt.format(remainingOptName, remaining))
out.println()
}
/**
* Was the help option invoked?
* If so, print the help message to the output `PrintStream` and return true.
* Otherwise, return false. Callers may wish to exit if true is returned.
*/
def handleHelp(out: PrintStream = Console.out): Boolean =
get[Boolean](Args.HELP_KEY) match {
case Some(true) => out.println(help); true
case _ => false
}
/**
* Were errors found in the argument list?
* If so, print the error messages, followed by the help message and return true.
* Otherwise, return false. Callers may wish to exit if true is returned.
*/
def handleErrors(out: PrintStream = Console.err): Boolean =
if (failures.size > 0) {
out.println(help)
true
}
else false
protected val unknownOptionRE = "(--?.+)".r
/** Unknown option that starts with one or two '-' matches! */
protected val unknownOptionMatch: Opt.Parser[Any] = {
case unknownOptionRE(flag) +: tail => throw Args.UnrecognizedArgument(flag, tail)
}
override def toString: String = s"""Args:
| program invocation: $programInvocation
| leading comments: $leadingComments
| trailing comments: $trailingComments
| opts: $opts
| defaults: $defaults
| values: $values
| allValues: $allValues
| remaining: $remaining
| failures: $failures
|""".stripMargin
}
object Args {
val HELP_KEY = "help"
val defaultProgramInvocation: String = "java -cp ..."
val defaultComments: String = ""
def empty: Args = {
apply(Args.defaultProgramInvocation, Args.defaultComments, Args.defaultComments, Nil)
}
def apply(opts: Seq[Opt[_]]): Args = {
apply(Args.defaultProgramInvocation, Args.defaultComments, Args.defaultComments, opts)
}
def apply(programInvocation: String, opts: Seq[Opt[_]]): Args = {
apply(programInvocation, Args.defaultComments, Args.defaultComments, opts)
}
def apply(
programInvocation: String,
leadingComments: String,
trailingComments: String,
opts: Seq[Opt[_]]): Args = {
def defs = defaults(opts)
apply(programInvocation, leadingComments, trailingComments, opts, defs, defs)
}
def apply(
programInvocation: String,
leadingComments: String,
trailingComments: String,
opts: Seq[Opt[_]],
defaults: Map[String,Any]): Args =
apply(programInvocation, leadingComments, trailingComments, opts, defaults, defaults)
def apply(
programInvocation: String,
leadingComments: String,
trailingComments: String,
opts: Seq[Opt[_]],
defaults: Map[String,Any],
values: Map[String,Any]): Args = {
val noFlagOpts = opts.filter(_.flags.size == 0)
require(noFlagOpts.size <= 1, "At most one option can have no flags, used for all command-line tokens not associated with flags.")
// Add opts or help at the beginning and "remaining" (no flag) tokens at
// the end, if necessary. Also, add defaults and values for the extra
// options, if needed.
val (opts1, defaults1, values1) =
if (opts.exists(_.name == HELP_KEY) == false) { // scalastyle:ignore
val hf = (HELP_KEY -> false)
(helpFlag +: opts.toVector, defaults + hf, values + hf)
} else (opts.toVector, defaults, values)
val (opts2, defaults2, values2, remaining2) =
if (noFlagOpts.size == 0) {
(opts1 :+ remainingOpt, defaults1, values1, Vector.empty[String])
} else {
// Make sure the remaining values aren't in "defaults1" or "values1", but update "remaining"
val noFlagName = noFlagOpts.head.name
val defs2 = defaults1 - noFlagName
val vals2 = values1 - noFlagName
val rem2 = noFlagOpts.head.default match {
case Some(s) => s match {
case s: Seq[_] => s.map(_.toString).toVector // _ should already be String, but erasure...
case x: Any => Vector(x.toString)
}
case None => Vector.empty[String]
}
(opts1, defs2, vals2, rem2)
}
val allValues = values2.map{ case (k,v) => (k,Vector(v)) }
val failures = Seq.empty[(String,Any)]
new Args(programInvocation, leadingComments, trailingComments,
opts2, defaults2, values2, allValues, remaining2, failures)
}
// Common options.
/** Show Help. Normally the program will exit afterwards. */
val helpFlag = Opt.flag(
name = HELP_KEY,
flags = Seq("-h", "--h", "--help"),
help = "Show this help message.")
/** Minimize logging and other output. */
val quietFlag = Opt.flag(
name = "quiet",
flags = Seq("-q", "--quiet"),
help = "Suppress some verbose output.")
/**
* A special option for "remaining" or "bare" tokens that aren't associated with a flag.
* Note that it has no flags; only one such option is allowed in an `Args`.
*/
def makeRemainingOpt(
name: String = Opt.REMAINING_KEY,
help: String = "All remaining arguments that aren't associated with flags.",
required: Boolean = false): Opt[String] = Opt.bareTokens(name, help, required)
val remainingOpt = makeRemainingOpt()
final case class MissingRequiredArgument[T](o: Opt[T])
extends RuntimeException("") {
override def toString: String =
s"""Missing required argument: "${o.name}"${flagsString} ${o.help}"""
protected def flagsString =
if (o.flags.size == 0) ""
else s""" with flags ${o.flags.mkString(" | ")},"""
}
final case class UnrecognizedArgument(arg: String, rest: Seq[String])
extends RuntimeException("") {
override def toString: String =
s"Unrecognized argument (or missing value): $arg ${restOfArgs(rest)}"
private def restOfArgs(rest: Seq[String]) =
if (rest.size == 0) "(end of arguments)" else s"""(rest of arguments: ${rest.mkString(" ")})"""
}
def defaults(opts: Seq[Opt[_]]): Map[String,Any] =
opts.foldLeft(Map.empty[String,Any]) { (map, opt) =>
opt.default match {
case None => map
case Some(obj) => map + (opt.name -> obj)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy