All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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:
   * 
    *
  1. `None` => `Nil` *
  2. `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