aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJakob Odersky <jakob@odersky.com>2019-09-21 00:38:30 -0400
committerJakob Odersky <jakob@odersky.com>2019-09-22 14:58:51 -0400
commita5a2118e4e0f31a4b8ae9921fa634058af526cdc (patch)
tree3a8912601637c78c8995fb8403302e2f3971d323 /src
parent8a71d804a41ffbd80f881fae24c42637e246afc2 (diff)
downloadcommando-a5a2118e4e0f31a4b8ae9921fa634058af526cdc.tar.gz
commando-a5a2118e4e0f31a4b8ae9921fa634058af526cdc.tar.bz2
commando-a5a2118e4e0f31a4b8ae9921fa634058af526cdc.zip
Migrate build to mill; redesign command structure and parser
Diffstat (limited to 'src')
-rw-r--r--src/main/scala/completion/Bash.scala107
-rw-r--r--src/main/scala/definitions.scala66
-rw-r--r--src/main/scala/package.scala50
-rw-r--r--src/main/scala/parsing.scala212
-rw-r--r--src/test/scala/ParserTest.scala143
5 files changed, 0 insertions, 578 deletions
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 // -<n>
- private case object LONG extends TokenKind // --<n>
- private case object POSITIONAL extends TokenKind // <n>
- 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)
- }
-
-}
diff --git a/src/test/scala/ParserTest.scala b/src/test/scala/ParserTest.scala
deleted file mode 100644
index 579ba95..0000000
--- a/src/test/scala/ParserTest.scala
+++ /dev/null
@@ -1,143 +0,0 @@
-package commando
-
-import utest._
-
-object ParserTest extends TestSuite {
-
- implicit class EliteCommando(line: String) {
- def parse(command: Command): Unit = {
- val args = line.split(" ").tail
- commando.parse(args, command, (c, err) => throw new ParseException(err))
- }
- def fail(command: Command, message: String): Unit = {
- val args = line.split(" ").tail
- try {
- commando.parse(args, command, (c, err) => throw new ParseException(err))
- sys.error("parsing succeeded but was expected to fail")
- } catch {
- case err: ParseException if err.getMessage.contains(message) =>
- case err: ParseException =>
- sys.error(s"parsing failed for the wrong reason: ${err.getMessage}")
- }
- }
- }
-
- def cbx(asserts: Command.Arguments => Unit) =
- cmd("cbx")(
- opt("server", 's', "name" -> true)
- ).sub(
- cmd("version")(
- opt("verbose", 'v', "k=v" -> false)
- ).run(asserts),
- cmd("login")(
- pos("server_url"),
- pos("username", false),
- pos("password", false)
- ).run(asserts),
- cmd("run")(
- opt("file", 'f', "file_name" -> true),
- opt("force"),
- pos("pipeline", false)
- ).run(asserts),
- cmd("level1")().sub(
- cmd("level2-1")(
- pos("p2")
- ).sub(
- cmd("level3")(pos("p3")).run(asserts)
- ),
- cmd("level2-2")().run(asserts)
- )
- )
-
- val tests = Tests {
- "print usage" - {
- cbx(_ => ()).usage
- }
- "simple" - {
- "cbx version" parse cbx { args =>
- args ==> Map.empty
- }
- }
- "empty allowed optional" - {
- "cbx version -v" parse cbx { args =>
- args ==> Map("verbose" -> Seq(""))
- }
- "cbx version --verbose" parse cbx { args =>
- args ==> Map("verbose" -> Seq(""))
- }
- }
- "set allowed optional" - {
- "cbx version -v x" parse cbx { args =>
- args ==> Map("verbose" -> Seq("x"))
- }
- "cbx version --verbose x" parse cbx { args =>
- args ==> Map("verbose" -> Seq("x"))
- }
- "cbx version --verbose=x" parse cbx { args =>
- args ==> Map("verbose" -> Seq("x"))
- }
- "cbx version --verbose=x=y" parse cbx { args =>
- args ==> Map("verbose" -> Seq("x=y"))
- }
- "cbx version --verbose=x=y,z=w" parse cbx { args =>
- args ==> Map("verbose" -> Seq("x=y,z=w"))
- }
- "cbx version --verbose x=y" parse cbx { args =>
- args ==> Map("verbose" -> Seq("x=y"))
- }
- "cbx version --verbose x=y z=w".fail(cbx(_ => ()), "too many arguments")
- }
- "required argument optional" - {
- "cbx run" parse cbx { _ ==> Map.empty } // make sure it works first
- "cbx run -f x" parse cbx { _ ==> Map("file" -> Seq("x")) }
- "cbx run --file x" parse cbx { _ ==> Map("file" -> Seq("x")) }
- "cbx run --file=x" parse cbx { _ ==> Map("file" -> Seq("x")) }
- "cbx run --file=x=y,z=w" parse cbx { _ ==> Map("file" -> Seq("x=y,z=w")) }
- "cbx run --file".fail(cbx(_ => ()), "requires")
- "cbx run --file --".fail(cbx(_ => ()), "requires")
- }
- "no argument optional" - {
- "cbx run --force=x".fail(cbx(_ => ()), "no argument allowed")
- "cbx run --force x" parse cbx { _ ==> Map("force" -> Seq(""), "pipeline" -> Seq("x")) }
- }
- "global optional" - {
- "cbx --server run run" parse cbx {_ ==> Map("server" -> Seq("run"))}
- "cbx -s run run" parse cbx {_ ==> Map("server" -> Seq("run"))}
- "cbx --server=run run" parse cbx {_ ==> Map("server" -> Seq("run"))}
- "cbx -x run".fail(cbx(_ => ()), "unknown option")
- "cbx --x run".fail(cbx(_ => ()), "unknown option")
- }
- "positional" - {
- "cbx login x" parse cbx { _ ==> Map("server_url" -> Seq("x"))}
- "cbx login x y" parse cbx { _ ==> Map("server_url" -> Seq("x"), "username" -> Seq("y"))}
- "cbx login x y z" parse cbx { _ ==> Map("server_url" -> Seq("x"), "username" -> Seq("y"), "password" -> Seq("z"))}
- "cbx login - x y z".fail(cbx(_ => ()), "too many")
- "cbx login - y" parse cbx { _ ==> Map("server_url" -> Seq("-"), "username" -> Seq("y"))}
- }
- "out of order options" - {
- "cbx run --force pipelinename -f x" parse cbx {
- _ ==> Map("force" -> Seq(""), "pipeline" -> Seq("pipelinename"), "file" -> Seq("x"))
- }
- "cbx run --force -- -f" parse cbx {
- _ ==> Map("force" -> Seq(""), "pipeline" -> Seq("-f"))
- }
- "cbx run --force -- --file" parse cbx {
- _ ==> Map("force" -> Seq(""), "pipeline" -> Seq("--file"))
- }
- "cbx run --force -- --" parse cbx {
- _ ==> Map("force" -> Seq(""), "pipeline" -> Seq("--"))
- }
- "cbx run --force -- -f x".fail(cbx(_ => ()), "too many")
- }
- "nested1" - {
- "cbx level1 level2-1 x=y level3 z" parse cbx {
- _ ==> Map("p2" -> Seq("x=y"), "p3" -> Seq("z"))
- }
- }
- "nested2" - {
- "cbx level1 level2-2 --" parse cbx {
- _ ==> Map.empty
- }
- }
- }
-}