aboutsummaryrefslogblamecommitdiff
path: root/commando/src/Command.scala
blob: e0a7a5aae8e3c1e0fc7ebace4cd022aa9def0466 (plain) (tree)








































































































































































































                                                                                         
package commando

class Command(val name: String) {
  import Command._
  import collection.mutable

  private case class Parameter(
      var named: Boolean,
      var name: String,
      var aliases: List[String],
      var argName: String,
      var acceptsArg: Boolean,
      var requiresArg: Boolean,
      var required: Boolean,
      var repeated: Boolean,
      var action: Option[String] => Unit
  )

  class NamedBuilder(param: Parameter) {
    def alias(aliases: String*) = { param.aliases ++= aliases; this }
    def require() = { param.required = true; this }
    def repeat() = { param.repeated = true; this }
    def action(fct: () => Unit) = { param.action = opt => fct(); this }

    def arg(name: String) = {
      param.argName = name; param.acceptsArg = true; param.requiresArg = true;
      new NamedArgBuilder(param)
    }
    def optionalArg(name: String) = {
      param.argName = name; param.acceptsArg = true; param.requiresArg = false;
      new NamedOptArgBuilder(param)
    }
  }

  class NamedArgBuilder(param: Parameter) {
    def alias(aliases: String*) = { param.aliases ++= aliases; this }
    def require() = { param.required = true; this }
    def repeat() = { param.repeated = true; this }

    def action(fct: String => Unit) = {
      param.action = opt => fct(opt.get); this
    }
  }
  class NamedOptArgBuilder(param: Parameter) {
    def alias(aliases: String*) = { param.aliases ++= aliases; this }
    def require() = { param.required = true; this }
    def repeat() = { param.repeated = true; this }

    def action(fct: Option[String] => Unit) = { param.action = fct; this }
  }

  class PositionalBuilder(param: Parameter) {
    def optional() = { param.required = false; this }
    def repeat() = { param.repeated = true; param.required = false; this }

    def action(fct: String => Unit) = {
      param.action = opt => fct(opt.get); this
    }
  }

  private val params = mutable.ListBuffer.empty[Parameter]

  def named(name: String): NamedBuilder = {
    val param =
      Parameter(true, name, Nil, "", false, false, false, false, _ => ())
    params += param
    new NamedBuilder(param)
  }

  def positional(name: String): PositionalBuilder = {
    val param =
      Parameter(false, name, Nil, "", false, false, true, false, _ => ())
    params += param
    new PositionalBuilder(param)
  }

  def error(message: String): Nothing = throw new ParseError(message)

  def parse(args: Iterable[String]): Option[String] =
    try {
      var (named, positional) = params.toList.partition(_.named)

      // keeps track of which parameters have already been set
      val seen: mutable.Set[Parameter] = mutable.Set.empty[Parameter]

      val it = args.iterator
      var arg = ""
      var done = false
      def next() = if (it.hasNext) arg = it.next() else done = true
      next()

      var escaping = false

      def process(param: Parameter, value: Option[String]) = {
        param.action(value)
      }

      def readPositional(arg: String) =
        if (positional.isEmpty) {
          error("too many arguments")
        } else {
          process(positional.head, Some(arg))
          seen += positional.head
          if (!positional.head.repeated) {
            positional = positional.tail
          }
        }

      def getNamed(name: String): Parameter = {
        named.find(p => p.name == name || p.aliases.contains(name)) match {
          case None => error(s"unknown parameter: '$name'")
          case Some(param) if (!param.repeated && seen.contains(param)) =>
            error(
              s"parameter '$name' has already been given and repetitions are not allowed"
            )
          case Some(param) =>
            seen += param
            param
        }
      }

      while (!done) {
        if (escaping == true) {
          readPositional(arg)
          next()
        } else if (arg == "--") {
          escaping = true
          next()
        } else if (arg.startsWith("--")) {
          arg.drop(2).split("=", 2) match {
            case Array(name, embeddedValue) =>
              val param = getNamed(name)
              if (param.acceptsArg) {
                process(param, Some(embeddedValue))
                next()
              } else {
                error(s"parameter '$name' does not accept an argument")
              }
            case Array(name) =>
              val param = getNamed(name)
              next()
              val nextIsArg = !done && (!arg.startsWith("-") || arg == "--")

              if (param.requiresArg && nextIsArg) {
                process(param, Some(arg))
                next()
              } else if (param.requiresArg && !nextIsArg) {
                error(s"parameter '$name' requires an argument")
              } else if (param.acceptsArg && nextIsArg) {
                process(param, Some(arg))
                next()
              } else {
                process(param, None)
              }
          }
        } else {
          readPositional(arg)
          next()
        }
      }

      for (param <- params) {
        if (param.required && !seen.contains(param))
          error(s"missing parameter: '${param.name}'")
      }
      None
    } catch {
      case ParseError(message) => Some(message)
    }

  def completion(): String = {
    val completions: List[String] = params.toList.filter(_.named).flatMap {
      param =>
        if (param.requiresArg) {
          List(s"--${param.name}=")
        } else if (param.acceptsArg) {
          List(s"--${param.name}", s"--${param.name}=")
        } else {
          List(s"--${param.name}")
        }
    }

    s"""|_${name}_complete() {
        |  local cur_word param_list
        |  cur_word="$${COMP_WORDS[COMP_CWORD]}"
        |  param_list="${completions.mkString(" ")}"
        |  if [[ $${cur_word} == -* ]]; then
        |    COMPREPLY=( $$(compgen -W "$$param_list" -- $${cur_word}) )
        |  else
        |    COMPREPLY=()
        |  fi
        |  return 0
        |}
        |complete -F _${name}_complete ${name}
        |""".stripMargin
  }

}
object Command {
  case class ParseError(message: String) extends RuntimeException(message)
}