From a5a2118e4e0f31a4b8ae9921fa634058af526cdc Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Sat, 21 Sep 2019 00:38:30 -0400 Subject: Migrate build to mill; redesign command structure and parser --- src/main/scala/completion/Bash.scala | 107 ------------------ src/main/scala/definitions.scala | 66 ----------- src/main/scala/package.scala | 50 --------- src/main/scala/parsing.scala | 212 ----------------------------------- 4 files changed, 435 deletions(-) delete mode 100644 src/main/scala/completion/Bash.scala delete mode 100644 src/main/scala/definitions.scala delete mode 100644 src/main/scala/package.scala delete mode 100644 src/main/scala/parsing.scala (limited to 'src/main/scala') diff --git a/src/main/scala/completion/Bash.scala b/src/main/scala/completion/Bash.scala deleted file mode 100644 index b22e054..0000000 --- a/src/main/scala/completion/Bash.scala +++ /dev/null @@ -1,107 +0,0 @@ -package commando -package completion - -object Bash { - - private def addCommands(command: Command): String = { - command.commands.map(c => s"""commands+=("${c.name}")\n""").mkString - } - - private def addFlags(command: Command): String = { - command.optionals.map { opt => - val extra = if (opt.argumentRequired) "=" else "" - val short = opt.short.map(c => s"""flags+=("-$c")""").getOrElse("") - s"""|flags+=("--${opt.long}$extra") - |$short - |""".stripMargin - }.mkString - } - - private def commandBlocks(previous: String, - commands: Set[Command]): String = { - def block(previous: String, command: Command): String = { - s"""_${previous}_${command.name}() { - | ${addCommands(command)} - | ${addFlags(command)} - | true - |} - |""".stripMargin - } - - if (commands.isEmpty) { - "" - } else { - commands.map { cmd => - block(previous, cmd) + commandBlocks(cmd.name, cmd.commands) - }.mkString - } - } - - def completion(command: Command): String = { - val name = command.name - - s"""__${name}_contains_word() { - | local word="$$1"; shift - | for w in "$$@"; do - | [[ $$w = "$$word" ]] && return 0 - | done - | return 1 - |} - | - |__${name}_handle_reply() { - | case "$$cur" in - | -*) - | COMPREPLY=( $$(compgen -W "$${flags[*]}" -- "$$cur") ) - | if [[ $${#COMPREPLY[@]} -eq 1 ]] && [[ $${COMPREPLY[0]} == *= ]]; then - | compopt -o nospace - | else - | compopt +o nospace - | fi - | ;; - | *) - | COMPREPLY=( $$(compgen -W "$${commands[*]}" -- "$$cur") ) - | ;; - | esac - |} - | - |__${name}_handle_word() { - | if [[ $$c -ge $$cword ]]; then - | __${name}_handle_reply - | return - | fi - | if __${name}_contains_word "$${words[c]}" "$${commands[@]}"; then - | local next_command="$${last_command}_$${words[c]}" - | last_command="$$next_command" - | commands=() - | flags=() - | $$next_command - | fi - | c=$$((c+1)) - | __${name}_handle_word - |} - | - |${commandBlocks(name, command.commands)} - | - |__${name}_start() { - | local words=("$${COMP_WORDS[@]}") - | local cword="$$COMP_CWORD" - | local cur="$${COMP_WORDS[COMP_CWORD]}" - | local c=0 - | COMPREPLY=() - | - | local last_command="_$name" - | local commands=() - | local flags=() - | - | ${addCommands(command)} - | ${addFlags(command)} - | - | __${name}_handle_word - | - | return 0 - |} - |complete -o default -F __${name}_start $name - |""".stripMargin - } - -} diff --git a/src/main/scala/definitions.scala b/src/main/scala/definitions.scala deleted file mode 100644 index 8de9e3a..0000000 --- a/src/main/scala/definitions.scala +++ /dev/null @@ -1,66 +0,0 @@ -package commando - -import commando.completion.Bash - -sealed trait Parameter { - def usage: String -} - -case class Optional(long: String, - short: Option[Char] = None, - argumentAllowed: Boolean = false, - argumentRequired: Boolean = false, - parameterName: String = "param") - extends Parameter { - - def usage: String = { - val shortString = short.map(c => s"-$c|").getOrElse("") - val paramString = if (argumentRequired) { - s"=<$parameterName>" - } else if (argumentAllowed) { - s"[=<$parameterName>]" - } else { - "" - } - s"[$shortString--$long$paramString]" - } -} - -case class Positional(name: String, required: Boolean = true) - extends Parameter { - def usage: String = if (required) s"<$name>" else s"[<$name>]" -} - -case class Command( - name: String, - optionals: Set[Optional], - positionals: Seq[Positional], - commands: Set[Command] = Set.empty, - action: Option[Command.Arguments => Unit] = None -) { - - private def subusage(level: Int): String = { - val optStrings = optionals.map { opt => - opt.usage - } - val posStrings = positionals map { pos => - pos.usage - } - val cmdStrings = Seq(commands.map(cmd => cmd.name).mkString("|")) - - val headline = - (Seq(name) ++ optStrings ++ posStrings ++ cmdStrings).mkString(" ") - val lines = commands - .map(_.subusage(level + 1)) - .map(line => " " * (level + 1) + line) - headline + lines.mkString("\n", "", "") - } - - def usage: String = subusage(0) - - def completion: String = Bash.completion(this) -} - -object Command { - type Arguments = Map[String, Seq[String]] -} diff --git a/src/main/scala/package.scala b/src/main/scala/package.scala deleted file mode 100644 index 68936cd..0000000 --- a/src/main/scala/package.scala +++ /dev/null @@ -1,50 +0,0 @@ -package commando - -class CommandBuilder(name: String, params: Seq[Parameter]) { - - private def optionals = - params.collect { - case opt: Optional => opt - }.toSet - private def positionals = params.collect { - case pos: Positional => pos - } - - def run(action: Command.Arguments => Unit): Command = - Command(name, optionals, positionals, Set.empty, Some(action)) - - def sub(commands: Command*): Command = - Command(name, optionals, positionals, commands.toSet, None) - -} - -object `package` { - - val DefaultErrorHandler: (Command, String) => Unit = - (command: Command, err: String) => { - System.err.println(s"${command.name}: $err") - System.exit(2) - } - - def parse(arguments: Seq[String], - command: Command, - onError: (Command, String) => Unit = DefaultErrorHandler): Unit = - Parser.parse(arguments, command, onError) - - def cmd(name: String)(params: Parameter*): CommandBuilder = - new CommandBuilder(name, params) - def opt(name: String, - short: Char = '\u0000', - param: (String, Boolean) = ("", false)): Optional = - Optional( - name, - if (short == '\u0000') None else Some(short), - argumentAllowed = (param != ("", false)), - argumentRequired = (param != ("", false)) && param._2, - parameterName = if (param._1 == "") "param" else param._1 - ) - - def pos(name: String, required: Boolean = true): Positional = - Positional(name, required) - -} diff --git a/src/main/scala/parsing.scala b/src/main/scala/parsing.scala deleted file mode 100644 index 2526d39..0000000 --- a/src/main/scala/parsing.scala +++ /dev/null @@ -1,212 +0,0 @@ -package commando -import scala.collection.mutable - -class ParseException(message: String) extends RuntimeException(message) - -object Parser { - private sealed trait TokenKind - private case object SHORT extends TokenKind // - - private case object LONG extends TokenKind // -- - private case object POSITIONAL extends TokenKind // - private case object DOUBLE_DASH extends TokenKind // -- - private case object EOL extends TokenKind - - private case class Token(value: String, kind: TokenKind) - - private def lex(input: Seq[String]): Iterator[Token] = new Iterator[Token] { - val args = input.iterator - val shortOptions = new mutable.Queue[Token] - - var escaping = false - override def hasNext: Boolean = args.hasNext || shortOptions.nonEmpty - - override def next(): Token = - if (shortOptions.nonEmpty) { - shortOptions.dequeue() - } else { - val arg = args.next - - if (escaping) { - Token(arg, POSITIONAL) - } else if (arg == "--") { - escaping = true - Token("--", DOUBLE_DASH) - } else if (arg.startsWith("--")) { - Token(arg.drop(2), LONG) - } else if (arg.startsWith("-") && arg != "-") { - arg.drop(1).foreach { char => - shortOptions.enqueue(Token(char.toString, SHORT)) - } - next() - } else { - Token(arg, POSITIONAL) - } - } - } - - def parse(args: Seq[String], - command: Command, - onError: (Command, String) => Unit): Unit = - try { - val tokens: Iterator[Token] = lex(args) - var token: Token = Token("end-of-line", EOL) - def readToken(): Unit = { - if (tokens.hasNext) { - token = tokens.next - } else { - token = Token("end-of-line", EOL) - } - } - - def accept(): Token = { - val tok = token - readToken() - tok - } - - val _parsedArguments = new mutable.HashMap[String, List[String]] - def addArgument(name: String, value: String): Unit = - _parsedArguments.get(name) match { - case None => _parsedArguments(name) = value :: Nil - case Some(values) => _parsedArguments(name) = value :: values - } - def parsedArguments = - _parsedArguments.map { - case (key, values) => - key -> values.reverse.toSeq - }.toMap - - def line(command: Command): Unit = { - val longs: Map[String, Optional] = command.optionals.map { - case opt: Optional => opt.long -> opt - }.toMap - val shorts: Map[String, Optional] = command.optionals.collect { - case opt: Optional if opt.short.isDefined => - opt.short.get.toString -> opt - }.toMap - val remainingPositionals = command.positionals.collect { - case pos: Positional => pos - }.iterator - val subcommands: Map[String, Command] = command.commands.map { cmd => - cmd.name -> cmd - }.toMap - - def fatal(message: String) = throw new ParseException(message) - - def optional(): Unit = { - val tok = accept() - val parts = tok.value.split("=", 2) - val name = parts(0) - val embedded: Option[String] = - if (parts.size > 1) Some(parts(1)) else None - val opt = (tok.kind: @unchecked) match { - case LONG => - longs.getOrElse(name, fatal(s"unknown option '--$name'")) - case SHORT => - shorts.getOrElse(name, fatal(s"unknown option '-$name'")) - } - - if (opt.argumentRequired) { - embedded match { - case Some(value) => - addArgument(opt.long, value) - case None if token.kind == POSITIONAL => - addArgument(opt.long, accept().value) - case None => - fatal( - s"option ${opt.usage} requires an argument but ${token.value} found") - } - } else if (opt.argumentAllowed) { - embedded match { - case Some(value) => - addArgument(opt.long, value) - case None => - if (token.kind == POSITIONAL) { - addArgument(opt.long, accept().value) - } else { - addArgument(opt.long, "") - } - } - } else { // no argument allowed - embedded match { - case Some(value) => - fatal( - s"no argument allowed for option ${opt.usage} (it is set to $value)") - case None => addArgument(opt.long, "") - } - } - } - - def positional(): Unit = { - if (remainingPositionals.hasNext) { - addArgument( - remainingPositionals.next.name, - accept().value - ) - } else { - fatal(s"too many arguments: '${token.value}'") - } - } - - // make sure all required positional parameters have been parsed - def checkPositionals(): Unit = { - val remaining = remainingPositionals.toList - if (!remaining.forall(_.required == false)) { - val missing = remaining.map(p => s"'${p.name}'") - fatal(s"missing parameter(s) ${missing.mkString(", ")}") - } - } - - var escaping = false - @annotation.tailrec - def innerLine(): Unit = { - if (token.kind == EOL) { - checkPositionals() - if (subcommands.nonEmpty) { - val missing = command.commands.map(c => s"'${c.name}'") - fatal( - s"command not specified (must be one of ${missing.mkString(", ")})") - } - command.action.get(parsedArguments) - } else if (escaping) { - positional() - innerLine() - } else if (token.kind == DOUBLE_DASH) { - escaping = true - readToken() - innerLine() - } else if (token.kind == POSITIONAL && remainingPositionals.nonEmpty) { - positional() - innerLine() - } else if (token.kind == POSITIONAL) { - if (subcommands.isEmpty) { - fatal(s"too many arguments: '${token.value}'") - } else { - subcommands.get(token.value) match { - case None => - val cmds = command.commands.map(c => s"'${c.name}'") - fatal( - s"command '${token.value}' not found (must be one of ${cmds - .mkString(", ")})") - case Some(cmd) => - checkPositionals() - readToken() - line(cmd) - } - } - } else if (token.kind == LONG || token.kind == SHORT) { - optional() - innerLine() - } else { - fatal(s"unknown token $token") - } - } - innerLine() - } - readToken() - line(command) - } catch { - case ex: ParseException => onError(command, ex.getMessage) - } - -} -- cgit v1.2.3