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

com.concurrentthought.cla.Opt.scala Maven / Gradle / Ivy

package com.concurrentthought.cla
import scala.util.{Try, Success, Failure}

/**
 * A command-line option, required or not, with a corresponding value.
 * @param name serve as a lookup key for retrieving the value.
 * @param flags the arguments that invoke the option, e.g., `-h` and `--help`.
 * @param help a message displayed for command-line help.
 * @param default an optional default value, for when the user doesn't specify the option.
 * @param required the user must specify this option on the command line.
 *     This flag is effectively ignored if a `default` is provided.
 * @param parser An implementation feature for parsing arguments.
 */
trait Opt[V] {
  val name:     String
  val flags:    Seq[String]
  val default:  Option[V]
  val help:     String
  val required: Boolean

  require (name.length != 0, "The Opt name can't be empty.")

  protected val optEQRE = "^([^=]+)=(.*)$".r

  /** Is the required flag true _and_ the default is None? */
  def isRequired: Boolean = required && default == None

  val parser: Opt.Parser[V]

  /** The minimal step to parse a string into an object, wrapping in a Try. */
  protected def stringToObject: String => Try[V]

  /** Helper for concrete implementations of the parser method. */
  protected def parserHelper(flag: String, value: String, tail: Seq[String]) = stringToObject(value) match {
    case Success(v)  => ((name, Success(v)), tail)
    case Failure(ex) => ex match {
      case ivs: Opt.InvalidValueString => ((name, Failure(ivs)), tail)
      case ex: Any => ((name, Failure(Opt.InvalidValueString(flag, value, Some(ex)))), tail)
    }
  }
}


object Opt {

  val REMAINING_KEY = "remaining"

  /**
   * Each option attempts to parse one or more tokens in the argument list.
   * If successful, it returns the option's name and extracted value as a tuple,
   * along with the rest of the arguments.
   */
  type Parser[V] = PartialFunction[Seq[String], ((String, Try[V]), Seq[String])]

  /**
   * Lift `String => V` to `String => Try[V]`.
   */
  def toTry[V](to: String => V): String => Try[V] = s => Try(to(s))

  /**
   * Lift `String => V` to `String => Seq[V]`.
   */
  def toSeq[V](to: String => V): String => Seq[V] = s => Vector(to(s))

  /**
   * Exception raised when an invalid value string is given. Not all errors
   * are detected and reported this way. For example, calls to `s.toInt` for
   * an invalid string will result in `NumberFormatException`.
   */
  final case class InvalidValueString(
      flag: String,
      valueMessage: String,
      cause: Option[Throwable] = None)
    extends RuntimeException(s"$valueMessage for option $flag", cause.getOrElse(null)) {

    /** Override toString to show a nicer name for the exception than the FQN. */
    override def toString: String = {
      val causeStr = cause.fold("")(c => s" (cause $c)")
      "Invalid value string: " + getMessage + causeStr
    }
  }

  /**
   * Implementation of an an Opt that has a flag and value.
   * @param fromString convert the found value from a String to the correct type.
   */
  protected[cla] final case class OptImpl[V] (
    name:     String,
    flags:    Seq[String],
    default:  Option[V] = None,
    help:     String = "",
    required: Boolean = false)(
    fromString: String => Try[V]) extends Opt[V] {

    protected def stringToObject: String => Try[V] = fromString

    val parser: Opt.Parser[V] = {
      case optEQRE(flag, value) +: tail if flags.contains(flag) => parserHelper(flag, value, tail)
      case flag +: value +: tail if flags.contains(flag) => parserHelper(flag, value, tail)
    }
  }

  /**
   * An implementation class for flags (boolean) options.
   */
  protected[cla] final case class Flag(
    name:         String,
    flags:        Seq[String],
    defaultValue: Boolean = false,
    help:         String = "",
    required:     Boolean = false) extends Opt[Boolean] {

    val default = Some(defaultValue)

    protected def stringToObject: String => Try[Boolean] = s => Try(s.toBoolean)

    /** For flags, the parser doesn't consume the next token (if any). */
    val parser: Opt.Parser[Boolean] = {
      case flag +: tail if flags.contains(flag) => parserHelper(flag, (!defaultValue).toString, tail)
    }
  }

  /**
   * Implementation of an a special kind of Opt that represents "bare" command-line
   * tokens without flags.
   * @param fromStringtring convert the found value from a String to the correct type.
   */
  protected[cla] final case class BareTokenOpt (
    name:     String,
    help:     String,
    required: Boolean = false) extends Opt[String] {

    val flags: Seq[String] = Nil
    val default: Option[String] = None

    protected def stringToObject: String => Try[String] = s => Try(s)

    /** Now there are no flags expected as the first token. */
    val parser: Opt.Parser[String] = {
      case value +: tail => ((name, Success(value)), tail)
    }
  }

  // Helper methods to create options.

  /**
   * Create a "flag" (Boolean) option. Unlike all the other kinds of
   * options, it does not consume an argument that follows it.
   * Instead the inferred default value corresponding to the flag is false.
   * If a user specifies the flag on the command line, the corresponding value
   * is true.
   * @see Opt.notflag
   */
  def flag(
    name:     String,
    flags:    Seq[String],
    help:     String = "",
    required: Boolean = false): Flag =
      new Flag(name, flags, false, help, required)

  /**
   * Like `flag`, but the default value is true, not false.
   * @see Opt.flag
   */
  def notflag(
    name:     String,
    flags:    Seq[String],
    help:     String = "",
    required: Boolean = false): Flag =
      new Flag(name, flags, true, help, required)

  /** Create a String option */
  def string(
    name:     String,
    flags:    Seq[String],
    default:  Option[String] = None,
    help:     String = "",
    required: Boolean = false): Opt[String] =
      new OptImpl(name, flags, default, help, required)(toTry(identity))

  /** Create a Byte option. */
  def byte(
    name:     String,
    flags:    Seq[String],
    default:  Option[Byte] = None,
    help:     String = "",
    required: Boolean = false): Opt[Byte] =
      new OptImpl(name, flags, default, help, required)(toTry(_.toByte))

  /** Create a Char option. Just takes the first character in the value string. */
  def char(
    name:     String,
    flags:    Seq[String],
    default:  Option[Char] = None,
    help:     String = "",
    required: Boolean = false): Opt[Char] =
      new OptImpl(name, flags, default, help, required)(toTry(_(0)))

  /** Create an Int option. */
  def int(
    name:     String,
    flags:    Seq[String],
    default:  Option[Int] = None,
    help:     String = "",
    required: Boolean = false): Opt[Int] =
      new OptImpl(name, flags, default, help, required)(toTry(_.toInt))

  /** Create a Long option. */
  def long(
    name:     String,
    flags:    Seq[String],
    default:  Option[Long] = None,
    help:     String = "",
    required: Boolean = false): Opt[Long] =
      new OptImpl(name, flags, default, help, required)(toTry(_.toLong))

  /** Create a Float option. */
  def float(
    name:     String,
    flags:    Seq[String],
    default:  Option[Float] = None,
    help:     String = "",
    required: Boolean = false): Opt[Float] =
      new OptImpl(name, flags, default, help, required)(toTry(_.toFloat))

  /** Create a Double option. */
  def double(
    name:     String,
    flags:    Seq[String],
    default:  Option[Double] = None,
    help:     String = "",
    required: Boolean = false): Opt[Double] =
      new OptImpl(name, flags, default, help, required)(toTry(_.toDouble))

  /**
   * Create an option where the value string represents a sequence with a delimiter.
   * The delimiter string is treated as a regex. For matching on several possible
   * delimiter characters, use "[;-_]", for example. The resulting substrings won't
   * be trimmed of whitespace, in case you want it, but you can also remove any
   * internal whitespace (i.e., not at the beginning or end of the input string),
   * e.g., "\\s*[;-_]\\s*". The delimiter is given as a separate argument list so
   * that the list of common Opt arguments is consistent with the other helper
   * methods.
   */
  def seq[V](delimsRE: String)(
    name:     String,
    flags:    Seq[String],
    default:  Option[Seq[V]] = None,
    help:     String = "",
    required: Boolean = false)(
    fromString: String => Try[V]): Opt[Seq[V]] = {
      require (delimsRE.trim.length > 0, "The delimiters RE string can't be empty.")
      new OptImpl(name, flags, default, help, required)(s => seqSupport(s, delimsRE, fromString))
    }

  /**
   * A helper method when the substrings are returned without further processing required.
   */
  def seqString(delimsRE: String)(
    name:     String,
    flags:    Seq[String],
    default:  Option[Seq[String]] = None,
    help:     String = "",
    required: Boolean = false): Opt[Seq[String]] =
      seq[String](delimsRE)(name, flags, default, help, required)(toTry(_.toString))

  /**
   * A helper method for path-like structures, where the default delimiter
   * for the platform is used, e.g., ':' for *nix systems and ';' for Windows.
   */
  def path(
    name:     String,
    flags:    Seq[String],
    default:  Option[Seq[String]] = None,
    help:     String = "List of file system paths",
    required: Boolean = false): Opt[Seq[String]] =
      seqString(pathSeparator)(
        name, flags, default, help, required)

  def pathSeparator: String = sys.props.getOrElse("path.separator",":")

  private def seqSupport[V](
    str:        String,
    delimsRE:   String,
    fromString: String => Try[V]): Try[Seq[V]] = {
    @annotation.tailrec
    def f(strs: Seq[String], vect: Vector[V]): Try[Vector[V]] = strs match {
      case head +: tail => fromString(head) match {
        case Success(value) => f(tail, vect :+ value)
        case Failure(ex) => Failure(ex)
      }
      case Nil => Success(vect)
    }
    f(str.split(delimsRE).toIndexedSeq, Vector.empty[V])
  }

  /**
   * A helper method for a socket host and port.
   */
  def socket(
    name:     String = "socket",
    flags:    Seq[String] = Seq("-s", "--socket"),
    default:  Option[(String, Int)] = None,
    help:     String = "Socket host:port.",
    required: Boolean = false): Opt[(String,Int)] =
      new OptImpl(name, flags, default, help, required)(socketFromString)

  private def socketFromString(s: String): Try[(String,Int)] = {
    val array = s.split(":")
    if (array.length != 2) Failure(Opt.InvalidValueString("--socket", s))
    else {
      val host = array(0)
      Try(array(1).toInt) match {
        case Success(port) => Success(host -> port)
        case Failure(th)   => Failure(Opt.InvalidValueString("--socket", s"$s (not an int?)", Some(th)))
      }
    }
  }

  /**
   * A helper method for a special kind of "option", one for handling bare token
   * arguments that don't have flags.
   */
  def bareTokens(
    name: String = REMAINING_KEY,
    help: String = "All remaining arguments that aren't associated with flags.",
    required: Boolean = false): Opt[String] =
      new BareTokenOpt(name, help, required)
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy