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) }