package commando import scala.annotation.meta.param object Command { case class ParseError(message: String) extends RuntimeException(message) } class Command(val name: String) { import Command._ import collection.mutable private case class NamedParameter( var name: String, var short: Option[Char], var argName: String, var acceptsArg: Boolean, var requiresArg: Boolean, var action: Option[String] => Unit ) private case class PositionalParameter( var name: String, var optional: Boolean, var repeated: Boolean, var action: String => Unit ) private val namedParams = mutable.ListBuffer.empty[NamedParameter] private val posParams = mutable.ListBuffer.empty[PositionalParameter] class NamedBuilder(param: NamedParameter) { 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: NamedParameter) { def action(fct: String => Unit) = { param.action = opt => fct(opt.get); this } } class NamedOptArgBuilder(param: NamedParameter) { def action(fct: Option[String] => Unit) = { param.action = fct; this } } class PositionalBuilder(param: PositionalParameter) { def optional() = { param.optional = true; this } def repeat() = { param.repeated = true; this } def action(fct: String => Unit) = { param.action = fct; this } } def named(name: String, short: Char = 0): NamedBuilder = { val shortName = if (short == 0) None else Some(short) val param = NamedParameter(name, shortName, "", false, false, _ => ()) namedParams += param new NamedBuilder(param) } def positional(name: String): PositionalBuilder = { val param = PositionalParameter(name, false, false, _ => ()) posParams += param new PositionalBuilder(param) } /** Raise a fatal parse error. This will cause parsing to fail with * the given message. */ def error(message: String): Nothing = throw new ParseError(message) private val checks = mutable.ListBuffer.empty[() => Unit] /** Add a post-codition check. * * This function will get run after parsing * is complete, but before any parameter actions are invoked. * * Use the error() function in a check to signal that it failed. */ def check(checkFct: => Unit) = { checks += (() => checkFct) } /** Parse this command against the given arguments. * * Returns 'None' if parsing was successful, or an error message otherwise. */ def parse(args: Iterable[String]): Option[String] = try { val named = namedParams.result() var positional = posParams.result() val namedQueue = mutable.ListBuffer.empty[(NamedParameter, Option[String])] val posQueue = mutable.ListBuffer.empty[(PositionalParameter, String)] 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 readPositional(arg: String) = if (positional.isEmpty) { error("too many arguments") } else { posQueue += positional.head -> arg //seen += positional.head if (!positional.head.repeated) { positional = positional.tail } next() } def getLong(name: String): NamedParameter = named.find(_.name == name) match { case None => error(s"unknown parameter: --$name") case Some(param) => param } def getShort(name: Char): NamedParameter = named.find(_.short == Some(name)) match { case None => error(s"unknown parameter: -$name") case Some(param) => param } def readNamed(param: NamedParameter, given: String) = { next() val nextIsArg = !done && (!arg.startsWith("-") || arg == "--") if (param.requiresArg && nextIsArg) { namedQueue += param -> Some(arg) next() } else if (param.requiresArg && !nextIsArg) { error(s"parameter '$given' requires an argument") } else if (param.acceptsArg && nextIsArg) { namedQueue += param -> Some(arg) next() } else { namedQueue += param -> None } } // parse arguments while (!done) { if (escaping == true) { readPositional(arg) } else if (arg == "--") { escaping = true next() } else if (arg.startsWith("--")) { arg.drop(2).split("=", 2) match { case Array(name, embeddedValue) => val param = getLong(name) if (param.acceptsArg) { namedQueue += param -> Some(embeddedValue) next() } else { error(s"parameter '$arg' does not accept an argument") } case Array(name) => readNamed(getLong(name), arg) } } else if (arg.startsWith("-") && arg != "-") { val chars = arg.drop(1) val params = chars.map(c => getShort(c)) if (params.length > 1) { if (!params.forall(!_.acceptsArg)) { error( s"only flags are allowed when multiple short parameters are given: $chars" ) } else { params.foreach(p => namedQueue += p -> None) next() } } else { readNamed(params.head, s"-${chars.head}") } } else { readPositional(arg) } } // there should only be optional positional parameters left at this point for (p <- positional) { if (!p.optional && !p.repeated) error(s"missing parameter: '${p.name}'") } for (check <- checks) { check() } // process arguments for ((p, v) <- namedQueue) { p.action(v) } for ((p, v) <- posQueue) { p.action(v) } None } catch { case ParseError(message) => Some(message) } def completion(): String = { val named = namedParams.result() val completions: List[String] = named.flatMap { param => param.short match { case Some(s) => List(s"-$s") case None => Nil } }.sorted ::: 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}") } }.sorted 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}) ) | return 0 | fi |} |complete -F _${name}_complete -o default ${name} |""".stripMargin } }