aboutsummaryrefslogtreecommitdiff
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
parent8a71d804a41ffbd80f881fae24c42637e246afc2 (diff)
downloadcommando-a5a2118e4e0f31a4b8ae9921fa634058af526cdc.tar.gz
commando-a5a2118e4e0f31a4b8ae9921fa634058af526cdc.tar.bz2
commando-a5a2118e4e0f31a4b8ae9921fa634058af526cdc.zip
Migrate build to mill; redesign command structure and parser
-rw-r--r--.gitignore2
-rw-r--r--build.sbt32
-rw-r--r--build.sc13
-rw-r--r--commando/src/Command.scala201
-rw-r--r--commando/src/Main.scala32
-rw-r--r--commando/test/src/CommandTest.scala221
-rw-r--r--project/build.properties1
-rw-r--r--project/plugins.sbt6
-rw-r--r--project/publish.sbt2
-rw-r--r--publish.sbt29
-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
15 files changed, 468 insertions, 649 deletions
diff --git a/.gitignore b/.gitignore
index 9f97022..89f9ac0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1 @@
-target/ \ No newline at end of file
+out/
diff --git a/build.sbt b/build.sbt
deleted file mode 100644
index 152be51..0000000
--- a/build.sbt
+++ /dev/null
@@ -1,32 +0,0 @@
-// shadow sbt-scalajs' crossProject and CrossType until Scala.js 1.0.0 is released
-import sbtcrossproject.{crossProject, CrossType}
-
-lazy val commando = crossProject(JSPlatform, JVMPlatform, NativePlatform)
- .withoutSuffixFor(JVMPlatform)
- .crossType(CrossType.Pure)
- .in(file("."))
- .settings(
- scalacOptions ++= Seq(
- "-deprecation",
- "-feature"
- ),
- libraryDependencies ++= Seq(
- "com.lihaoyi" %%% "utest" % "0.6.6" % "test"
- ),
- testFrameworks += new TestFramework("utest.runner.Framework"),
- scalaVersion := crossScalaVersions.value.head
- )
- .jsSettings(
- crossScalaVersions := "2.12.8" :: "2.11.12" :: Nil
- )
- .jvmSettings(
- crossScalaVersions := "2.12.8" :: "2.11.12" :: Nil
- )
- .nativeSettings(
- crossScalaVersions := "2.11.12" :: Nil,
- nativeLinkStubs := true
- )
-
-lazy val commandoJS = commando.js
-lazy val commandoJVM = commando.jvm
-lazy val commandoNative = commando.native
diff --git a/build.sc b/build.sc
new file mode 100644
index 0000000..e27039f
--- /dev/null
+++ b/build.sc
@@ -0,0 +1,13 @@
+import mill._, scalalib._, scalafmt._
+
+object commando extends ScalaModule with ScalafmtModule {
+ override def scalaVersion = "2.13.0"
+
+ object test extends Tests {
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest:0.7.1"
+ )
+ def testFrameworks = Seq("utest.runner.Framework")
+ }
+
+}
diff --git a/commando/src/Command.scala b/commando/src/Command.scala
new file mode 100644
index 0000000..e0a7a5a
--- /dev/null
+++ b/commando/src/Command.scala
@@ -0,0 +1,201 @@
+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)
+}
diff --git a/commando/src/Main.scala b/commando/src/Main.scala
new file mode 100644
index 0000000..1e1b786
--- /dev/null
+++ b/commando/src/Main.scala
@@ -0,0 +1,32 @@
+package example
+
+object Main extends App {
+
+ val cmd = new commando.Command("xorc") {
+ val version = named("version")
+ .action(() => println("version 1"))
+ .repeat()
+
+ named("verbose")
+ .optionalArg("level")
+ .action(level => println(s"level $level"))
+
+ positional("FILES")
+ .action { s =>
+ val f = new java.io.File(s)
+ if (!f.exists()) error(s"File $s does not exit")
+ println(f)
+ }
+ .repeat()
+
+ val comp = named("completion")
+ .action(() => println(completion()))
+ }
+ cmd.parse(args) match {
+ case None =>
+ case Some(error) =>
+ println(error)
+ sys.exit(1)
+ }
+
+}
diff --git a/commando/test/src/CommandTest.scala b/commando/test/src/CommandTest.scala
new file mode 100644
index 0000000..13897ee
--- /dev/null
+++ b/commando/test/src/CommandTest.scala
@@ -0,0 +1,221 @@
+package commando
+
+import utest._
+
+object CommandTests extends TestSuite {
+
+ def eval(cmd: Command, args: List[String]): Boolean = {
+ cmd.parse(args).isEmpty
+ }
+
+ val tests = Tests {
+ "empty" - {
+ val cmd = new Command("cmd")
+ assert(eval(cmd, Nil))
+ assert(!eval(cmd, "a" :: Nil))
+ }
+ "positional required" - {
+ val cmd = new Command("cmd") {
+ positional("POS")
+ }
+ assert(!eval(cmd, Nil)) // no param
+ assert(eval(cmd, "" :: Nil)) // empty param
+ assert(eval(cmd, "a" :: Nil)) // one param
+ assert(!eval(cmd, "a" :: "b" :: Nil)) // too many params
+ assert(!eval(cmd, "a" :: "b" :: "c" :: Nil)) // too many params
+ }
+ "positional two" - {
+ val cmd = new Command("cmd") {
+ positional("one")
+ positional("two")
+ }
+ assert(!eval(cmd, Nil)) // no param
+ assert(!eval(cmd, "" :: Nil)) // empty param
+ assert(!eval(cmd, "a" :: Nil)) // one param
+ assert(eval(cmd, "a" :: "b" :: Nil)) // too many params
+ assert(!eval(cmd, "a" :: "b" :: "c" :: Nil)) // too many params
+ }
+ "positional repeated" - {
+ val cmd = new Command("cmd") {
+ positional("one").repeat()
+ }
+ assert(eval(cmd, Nil))
+ assert(eval(cmd, "" :: Nil))
+ assert(eval(cmd, "a" :: Nil))
+ assert(eval(cmd, "a" :: "b" :: Nil))
+ assert(eval(cmd, "a" :: "b" :: "c" :: Nil))
+ }
+ "positional optional" - {
+ val cmd = new Command("cmd") {
+ positional("one").optional()
+ }
+ assert(eval(cmd, Nil))
+ assert(eval(cmd, "" :: Nil))
+ assert(eval(cmd, "a" :: Nil))
+ assert(!eval(cmd, "a" :: "b" :: Nil))
+ assert(!eval(cmd, "a" :: "b" :: "c" :: Nil))
+ }
+ "positional combination" - {
+ class Cmd extends Command("cmd") {
+ var one: Option[String] = None
+ var two: Option[String] = None
+ positional("one")
+ .optional()
+ .action(v => one = Some(v))
+ positional("two")
+ .repeat()
+ .action(v => two = Some(v))
+ }
+
+ val cmd1 = new Cmd
+ assert(eval(cmd1, Nil))
+ assert(cmd1.one == None && cmd1.two == None)
+
+ val cmd2 = new Cmd
+ assert(eval(cmd2, "" :: Nil))
+ assert(cmd2.one == Some("") && cmd2.two == None)
+
+ val cmd3 = new Cmd
+ assert(eval(cmd3, "a" :: Nil))
+ assert(cmd3.one == Some("a") && cmd3.two == None)
+
+ val cmd4 = new Cmd
+ assert(eval(cmd4, "a" :: "b" :: Nil))
+ assert(cmd4.one == Some("a") && cmd4.two == Some("b"))
+
+ val cmd5 = new Cmd
+ assert(eval(cmd5, "a" :: "b" :: "c" :: Nil))
+ assert(cmd5.one == Some("a") && cmd5.two == Some("c"))
+ }
+ "named flag optional" - {
+ val cmd = new Command("cmd") {
+ named("param")
+ }
+ assert(eval(cmd, Nil))
+ assert(!eval(cmd, "" :: Nil))
+ assert(eval(cmd, "--param" :: Nil))
+ assert(!eval(cmd, "--param" :: "" :: Nil))
+ assert(!eval(cmd, "--param" :: "--param" :: Nil))
+ assert(!eval(cmd, "--param" :: "--param" :: "a" :: Nil))
+ }
+ "named flag required" - {
+ val cmd = new Command("cmd") {
+ named("param").require()
+ }
+ assert(!eval(cmd, Nil))
+ assert(!eval(cmd, "" :: Nil))
+ assert(eval(cmd, "--param" :: Nil))
+ assert(!eval(cmd, "--param" :: "" :: Nil))
+ assert(!eval(cmd, "--param" :: "--param" :: Nil))
+ assert(!eval(cmd, "--param" :: "--param" :: "a" :: Nil))
+ }
+ "named flag repeated" - {
+ val cmd = new Command("cmd") {
+ named("param").repeat()
+ }
+ assert(eval(cmd, Nil))
+ assert(!eval(cmd, "" :: Nil))
+ assert(eval(cmd, "--param" :: Nil))
+ assert(!eval(cmd, "--param" :: "" :: Nil))
+ assert(eval(cmd, "--param" :: "--param" :: Nil))
+ assert(!eval(cmd, "--param" :: "--param" :: "a" :: Nil))
+ }
+ "named arg" - {
+ class Cmd extends Command("cmd") {
+ var one: Option[String] = None
+ var two: Option[String] = None
+ named("one")
+ .arg("value")
+ .action(v => one = Some(v))
+ positional("two")
+ .action(v => two = Some(v))
+ }
+
+ val cmd1 = new Cmd
+ assert(!eval(cmd1, Nil))
+ assert(cmd1.one == None && cmd1.two == None)
+
+ val cmd2 = new Cmd
+ assert(eval(cmd2, "--one=a" :: "b" :: Nil))
+ assert(cmd2.one == Some("a") && cmd2.two == Some("b"))
+
+ val cmd3 = new Cmd
+ assert(eval(cmd3, "b" :: "--one=a" :: Nil))
+ assert(cmd3.one == Some("a") && cmd3.two == Some("b"))
+
+ val cmd4 = new Cmd
+ assert(eval(cmd4, "--one" :: "a" :: "b" :: Nil))
+ assert(cmd4.one == Some("a") && cmd4.two == Some("b"))
+
+ val cmd5 = new Cmd
+ assert(eval(cmd5, "b" :: "--one" :: "a" :: Nil))
+ assert(cmd5.one == Some("a") && cmd5.two == Some("b"))
+
+ val cmd6 = new Cmd
+ assert(!eval(cmd6, "--one" :: "--a" :: "b" :: Nil))
+
+ val cmd7 = new Cmd
+ assert(!eval(cmd7, "b" :: "--one" :: Nil))
+
+ val cmd8 = new Cmd
+ assert(!eval(cmd8, "a" :: "--" :: "--one" :: Nil))
+
+ val cmd9 = new Cmd
+ assert(!eval(cmd9, "--one" :: "--" :: "--one" :: "b" :: Nil))
+
+ val cmd10 = new Cmd
+ assert(eval(cmd10, "--one=--ab" :: "b" :: Nil))
+ assert(cmd10.one == Some("--ab") && cmd10.two == Some("b"))
+ }
+ "named arg optional" - {
+ class Cmd extends Command("cmd") {
+ var one: Option[String] = None
+ var two: Option[String] = None
+ named("one")
+ .optionalArg("value")
+ .action(v => one = v)
+ positional("two")
+ .action(v => two = Some(v))
+ }
+
+ val cmd1 = new Cmd
+ assert(!eval(cmd1, Nil))
+ assert(cmd1.one == None && cmd1.two == None)
+
+ val cmd2 = new Cmd
+ assert(eval(cmd2, "--one=a" :: "b" :: Nil))
+ assert(cmd2.one == Some("a") && cmd2.two == Some("b"))
+
+ val cmd3 = new Cmd
+ assert(eval(cmd3, "b" :: "--one=a" :: Nil))
+ assert(cmd3.one == Some("a") && cmd3.two == Some("b"))
+
+ val cmd4 = new Cmd
+ assert(eval(cmd4, "--one" :: "a" :: "b" :: Nil))
+ assert(cmd4.one == Some("a") && cmd4.two == Some("b"))
+
+ val cmd5 = new Cmd
+ assert(eval(cmd5, "b" :: "--one" :: "a" :: Nil))
+ assert(cmd5.one == Some("a") && cmd5.two == Some("b"))
+
+ val cmd6 = new Cmd
+ assert(!eval(cmd6, "--one" :: "--a" :: "b" :: Nil))
+
+ val cmd7 = new Cmd
+ assert(eval(cmd7, "b" :: "--one" :: Nil))
+ assert(cmd7.one == None && cmd7.two == Some("b"))
+
+ val cmd8 = new Cmd
+ assert(eval(cmd8, "--" :: "--one" :: Nil))
+ assert(cmd8.one == None && cmd8.two == Some("--one"))
+
+ val cmd9 = new Cmd
+ assert(eval(cmd9, "--one" :: "a" :: "--" :: "--one" :: Nil))
+ assert(cmd9.one == Some("a") && cmd9.two == Some("--one"))
+
+ val cmd10 = new Cmd
+ assert(eval(cmd10, "--one=--ab" :: "b" :: Nil))
+ assert(cmd10.one == Some("--ab") && cmd10.two == Some("b"))
+ }
+ }
+}
diff --git a/project/build.properties b/project/build.properties
deleted file mode 100644
index c0bab04..0000000
--- a/project/build.properties
+++ /dev/null
@@ -1 +0,0 @@
-sbt.version=1.2.8
diff --git a/project/plugins.sbt b/project/plugins.sbt
deleted file mode 100644
index 233cf8d..0000000
--- a/project/plugins.sbt
+++ /dev/null
@@ -1,6 +0,0 @@
-addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1")
-
-addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")
-addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "0.6.0")
-addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.26")
-addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.3.8")
diff --git a/project/publish.sbt b/project/publish.sbt
deleted file mode 100644
index b39e15a..0000000
--- a/project/publish.sbt
+++ /dev/null
@@ -1,2 +0,0 @@
-addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3")
-addSbtPlugin("io.crashbox" % "sbt-gpg" % "0.2.0")
diff --git a/publish.sbt b/publish.sbt
deleted file mode 100644
index 9c0a42e..0000000
--- a/publish.sbt
+++ /dev/null
@@ -1,29 +0,0 @@
-organization in ThisBuild := "io.crashbox"
-licenses in ThisBuild := Seq(
- ("BSD-3-Clause", url("https://opensource.org/licenses/BSD-3-Clause")))
-homepage in ThisBuild := Some(url("https://github.com/jodersky/commando"))
-publishMavenStyle in ThisBuild := true
-publishTo in ThisBuild := Some(
- if (isSnapshot.value)
- Opts.resolver.sonatypeSnapshots
- else
- Opts.resolver.sonatypeStaging
-)
-scmInfo in ThisBuild := Some(
- ScmInfo(
- url("https://github.com/jodersky/commando"),
- "scm:git@github.com:jodersky/commando.git"
- )
-)
-developers in ThisBuild := List(
- Developer(
- id = "jodersky",
- name = "Jakob Odersky",
- email = "jakob@odersky.com",
- url = url("https://crashbox.io")
- )
-)
-version in ThisBuild := {
- import sys.process._
- ("git describe --always --dirty=-SNAPSHOT --match v[0-9].*" !!).tail.trim
-}
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
- }
- }
- }
-}