From eef44a276ace54ef0b215c1c4b045afde40daf5b Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Sun, 29 Apr 2018 17:59:47 -0700 Subject: Specify action inline with command --- src/main/scala/completion/Bash.scala | 10 +- src/main/scala/definitions.scala | 87 +++++++------- src/main/scala/package.scala | 52 +++++++-- src/main/scala/parsing.scala | 145 +++++++++++++----------- src/test/scala/CmdTest.scala | 207 ---------------------------------- src/test/scala/CmdTest.scala.disabled | 207 ++++++++++++++++++++++++++++++++++ src/test/scala/ParserTest.scala | 27 +++++ 7 files changed, 400 insertions(+), 335 deletions(-) delete mode 100644 src/test/scala/CmdTest.scala create mode 100644 src/test/scala/CmdTest.scala.disabled create mode 100644 src/test/scala/ParserTest.scala diff --git a/src/main/scala/completion/Bash.scala b/src/main/scala/completion/Bash.scala index e049fe1..db7b40a 100644 --- a/src/main/scala/completion/Bash.scala +++ b/src/main/scala/completion/Bash.scala @@ -8,9 +8,9 @@ object Bash { } private def addFlags(command: Command): String = { - command.options.map { opt => + command.optionals.map { opt => val extra = if (opt.argumentRequired) "=" else "" - val short = opt.short.map(c => s"""flags+=("-${c}")""").getOrElse("") + val short = opt.short.map(c => s"""flags+=("-$c")""").getOrElse("") s"""|flags+=("--${opt.long}$extra") |$short |""".stripMargin @@ -37,7 +37,7 @@ object Bash { } } - def completion(command: Command) = { + def completion(command: Command): String = { val name = command.name s"""__${name}_contains_word() { @@ -89,7 +89,7 @@ object Bash { | local c=0 | COMPREPLY=() | - | local last_command="_${name}" + | local last_command="_$name" | local commands=() | local flags=() | @@ -100,7 +100,7 @@ object Bash { | | return 0 |} - |complete -o default -F __${name}_start ${name} + |complete -o default -F __${name}_start $name |""".stripMargin } diff --git a/src/main/scala/definitions.scala b/src/main/scala/definitions.scala index b009e41..15c8e2c 100644 --- a/src/main/scala/definitions.scala +++ b/src/main/scala/definitions.scala @@ -1,71 +1,66 @@ package commando -sealed trait Definition +import commando.completion.Bash + +sealed trait Parameter { + def usage: String +} case class Optional(long: String, short: Option[Char] = None, - parameter: Option[Positional] = None) - extends Definition { - def argumentAllowed: Boolean = parameter.isDefined - def argumentRequired: Boolean = parameter.map(_.required).getOrElse(false) - override def toString = { + argumentAllowed: Boolean = false, + argumentRequired: Boolean = false, + parameterName: String = "param") + extends Parameter { + + def usage: String = { val shortString = short.map(c => s"-$c|").getOrElse("") - val argString = parameter match { - case None => "" - case Some(Positional(argName, false)) => s"[=<$argName>]" - case Some(Positional(argName, true)) => s"=<$argName>" + val paramString = if (argumentRequired) { + s"=<$parameterName>" + } else if (argumentAllowed) { + s"[=<$parameterName>]" + } else { + "" } - s"[$shortString--$long$argString]" + s"[$shortString--$long$paramString]" } } -case class Positional( - name: String, - required: Boolean = true -) extends Definition { - override def toString = if (required) s"<$name>" else s"[<$name>]" +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, - options: Set[Optional] = Set.empty, - parameters: Seq[Positional] = Seq.empty, - commands: Set[Command] = Set.empty -) extends Definition { - override def toString = name + name: String, + optionals: Set[Optional], + positionals: Seq[Positional], + commands: Set[Command] = Set.empty, + action: Option[Command.Arguments => Unit] = None +) { - def subusage(level: Int): String = { - val optionStrings = options.map { opt => - opt.toString + private def subusage(level: Int): String = { + val optStrings = optionals.map { opt => + opt.usage } - val parameterStrings = parameters map { param => - param.toString + val posStrings = positionals map { pos => + pos.usage } - val commandStrings = Seq(commands.map(cmd => cmd.name).mkString("|")) + val cmdStrings = Seq(commands.map(cmd => cmd.name).mkString("|")) val headline = - (Seq(name) ++ optionStrings ++ parameterStrings ++ commandStrings) - .mkString(" ") - val sublines = commands + (Seq(name) ++ optStrings ++ posStrings ++ cmdStrings).mkString(" ") + val lines = commands .map(_.subusage(level + 1)) .map(line => " " * (level + 1) + line) - headline + sublines.mkString("\n", "", "") + headline + lines.mkString("\n", "", "") } - def usage: String = "Usage: " + subusage(0) - - def completion: String = commando.completion.Bash.completion(this) + def usage: String = subusage(0) + def completion: String = Bash.completion(this) } -object Command { - - def apply(name: String, defs: Definition*): Command = { - Command( - name, - options = defs.collect { case opt: Optional => opt }.toSet, - parameters = defs.collect { case param: Positional => param }.toSeq, - commands = defs.collect { case cmd: Command => cmd }.toSet - ) - } -} +object Command { + type Arguments = Map[String, Seq[String]] +} \ No newline at end of file diff --git a/src/main/scala/package.scala b/src/main/scala/package.scala index b35740c..f813971 100644 --- a/src/main/scala/package.scala +++ b/src/main/scala/package.scala @@ -1,14 +1,48 @@ 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` { - def parse(command: Command, - arguments: Seq[String]): Either[ParseException, CommandLine] = - Parser.parse(command, arguments) - def parseOrExit(command: Command, arguments: Seq[String])( - action: CommandLine => Any): Unit = parse(command, arguments) match { - case Left(ex) => - System.err.println(ex.getMessage) - System.exit(1) - case Right(res) => action(res) + + 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 index 31880fa..95edc05 100644 --- a/src/main/scala/parsing.scala +++ b/src/main/scala/parsing.scala @@ -1,34 +1,27 @@ package commando import scala.collection.mutable -import scala.{Option => Maybe} - -case class CommandLine( - command: String, - arguments: Map[String, String], - subcommand: Maybe[CommandLine] -) 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 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]) = new Iterator[Token] { + private def lex(input: Seq[String]): Iterator[Token] = new Iterator[Token] { val args = input.iterator val shortOptions = new mutable.Queue[Token] var escaping = false - def hasNext = args.hasNext || !shortOptions.isEmpty + override def hasNext: Boolean = args.hasNext || shortOptions.nonEmpty - def next(): Token = - if (!shortOptions.isEmpty) { + override def next(): Token = + if (shortOptions.nonEmpty) { shortOptions.dequeue() } else { val arg = args.next @@ -51,8 +44,9 @@ object Parser { } } - def parse(command: Command, - args: Seq[String]): Either[ParseException, CommandLine] = + 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) @@ -70,25 +64,40 @@ object Parser { tok } - def line(command: Command): CommandLine = { - val longs: Map[String, Optional] = command.options.map { - case opt => opt.long -> opt + 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.options.collect { - case opt if opt.short.isDefined => opt.short.get.toString -> opt + val shorts: Map[String, Optional] = command.optionals.collect { + case opt: Optional if opt.short.isDefined => + opt.short.get.toString -> opt }.toMap - val subcommands: Map[String, Command] = command.commands.map { - case cmd => cmd.name -> cmd + 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(s"${command.name}: $message") + def fatal(message: String) = throw new ParseException(message) - def option(): (String, String) = { + def optional(): Unit = { val tok = accept() val parts = tok.value.split("=", 2) val name = parts(0) - val embedded: Maybe[String] = + val embedded: Option[String] = if (parts.size > 1) Some(parts(1)) else None val opt = (tok.kind: @unchecked) match { case LONG => @@ -99,76 +108,75 @@ object Parser { if (opt.argumentRequired) { embedded match { - case Some(value) => opt.long -> value + case Some(value) => + addArgument(opt.long, value) case None if token.kind == POSITIONAL => - opt.long -> accept().value + addArgument(opt.long, accept().value) case None => fatal( - s"option ${opt} requires an argument but ${token.value} found") + s"option ${opt.usage} requires an argument but ${token.value} found") } } else if (opt.argumentAllowed) { embedded match { - case Some(value) => opt.long -> value + case Some(value) => + addArgument(opt.long, value) case None => if (token.kind == POSITIONAL) { - opt.long -> accept.value + addArgument(opt.long, accept().value) } else { - opt.long -> "" + addArgument(opt.long, "") } } } else { // no argument allowed embedded match { case Some(value) => fatal( - s"no argument allowed for option $opt (it is set to $value)") - case None => opt.long -> "" + s"no argument allowed for option ${opt.usage} (it is set to $value)") + case None => addArgument(opt.long, "") } } } - val remainingParameters = command.parameters.iterator - def parameter(): (String, String) = { - if (remainingParameters.hasNext) { - remainingParameters.next.name -> accept().value + def positional(): Unit = { + if (remainingPositionals.hasNext) { + addArgument( + remainingPositionals.next.name, + accept().value + ) } else { fatal(s"too many parameters: '${token.value}'") } } - val parsedOptions = new mutable.HashMap[String, String] - val parsedParameters = new mutable.HashMap[String, String] - - var escaping = false - - def check(subline: Maybe[CommandLine]): CommandLine = { - val remaining = remainingParameters.toList + // make sure all required positional parameters have been parsed + def checkPositionals(): Unit = { + val remaining = remainingPositionals.toList if (!remaining.forall(_.required == false)) { - val missing = remaining.toList.map(p => s"'${p.name}'") + val missing = remaining.map(p => s"'${p.name}'") fatal(s"missing parameter(s) ${missing.mkString(", ")}") - } else if (!subcommands.isEmpty && subline.isEmpty) { - val missing = command.commands.map(c => s"'${c.name}'") - fatal( - s"command not specified (must be one of ${missing.mkString(", ")})") - } else { - CommandLine(command.name, - parsedOptions.toMap ++ parsedParameters.toMap, - subline) } } + var escaping = false @annotation.tailrec - def innerLine(): CommandLine = { + def innerLine(): Unit = { if (token.kind == EOL) { - check(None) + 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) { - parsedParameters += parameter() + positional() innerLine() } else if (token.kind == DOUBLE_DASH) { escaping = true readToken() innerLine() - } else if (token.kind == POSITIONAL && !remainingParameters.isEmpty) { - parsedParameters += parameter() + } else if (token.kind == POSITIONAL && remainingPositionals.nonEmpty) { + positional() innerLine() } else if (token.kind == POSITIONAL) { if (subcommands.isEmpty) { @@ -178,15 +186,16 @@ object Parser { case None => val cmds = command.commands.map(c => s"'${c.name}'") fatal( - s"subcommand '${token.value}' not found (must be one of ${cmds + s"command '${token.value}' not found (must be one of ${cmds .mkString(", ")})") - case Some(_) => - val subline = line(subcommands(accept().value)) - check(Some(subline)) + case Some(cmd) => + checkPositionals() + readToken() + line(cmd) } } } else if (token.kind == LONG || token.kind == SHORT) { - parsedOptions += option() + optional() innerLine() } else { fatal(s"unknown token $token") @@ -195,9 +204,9 @@ object Parser { innerLine() } readToken() - Right(line(command)) + line(command) } catch { - case ex: ParseException => Left(ex) + case ex: ParseException => onError(command, ex.getMessage) } } diff --git a/src/test/scala/CmdTest.scala b/src/test/scala/CmdTest.scala deleted file mode 100644 index f78611d..0000000 --- a/src/test/scala/CmdTest.scala +++ /dev/null @@ -1,207 +0,0 @@ -package commando - -import utest._ - -object CmdTests extends TestSuite { - - val cbx = commando.Command( - "cbx", - commando.Optional("server", Some('s'), Some(commando.Positional("name"))), - commando.Command( - "version", - commando.Optional("verbose", Some('v'), Some(commando.Positional("k=v", false)))), - commando.Command("login", - commando.Positional("server_url"), - commando.Positional("username", false), - commando.Positional("password", false)), - commando.Command("run", - commando.Optional("file", Some('f'), Some(commando.Positional("file_name"))), - commando.Optional("force", None), - commando.Positional("pipeline", false)), - commando.Command("level1", - commando.Command("level2-1", - commando.Positional("p2"), - commando.Command("level3", commando.Positional("p3"))), - commando.Command("level2-2")) - ) - - def parse(in: String): CommandLine = commando.parse(cbx, in.split(" ").tail) match { - case Left(ex) => throw ex - case Right(res) => res - } - - def shouldFail(in: String) = - assert(commando.parse(cbx, in.split(" ").tail).isLeft) - - val tests = Tests { - "printUsage" - { - cbx.usage - } - "simple" - { - assert( - parse("cbx version").subcommand.get == CommandLine("version", - Map.empty, - None)) - } - "emptyAllowedOption" - { - assert( - parse("cbx version -v").subcommand.get == CommandLine( - "version", - Map("verbose" -> ""), - None)) - assert( - parse("cbx version --verbose").subcommand.get == CommandLine( - "version", - Map("verbose" -> ""), - None)) - } - "setAllowedOption" - { - assert( - parse("cbx version -v x").subcommand.get == CommandLine( - "version", - Map("verbose" -> "x"), - None)) - assert( - parse("cbx version --verbose x").subcommand.get == CommandLine( - "version", - Map("verbose" -> "x"), - None)) - assert( - parse("cbx version --verbose=x").subcommand.get == CommandLine( - "version", - Map("verbose" -> "x"), - None)) - assert( - parse("cbx version --verbose=x=y").subcommand.get == CommandLine( - "version", - Map("verbose" -> "x=y"), - None)) - assert( - parse("cbx version --verbose=x=y,z=w").subcommand.get == CommandLine( - "version", - Map("verbose" -> "x=y,z=w"), - None)) - assert( - parse("cbx version --verbose x=y,z=w").subcommand.get == CommandLine( - "version", - Map("verbose" -> "x=y,z=w"), - None)) - shouldFail("cbx version --verbose x=y z=w") - } - "requiredArgOption" - { - assert(parse("cbx run").subcommand.get == CommandLine("run", Map(), None)) // make sure it works first - assert( - parse("cbx run -f x").subcommand.get == CommandLine("run", - Map("file" -> "x"), - None)) - assert( - parse("cbx run --file x").subcommand.get == CommandLine( - "run", - Map("file" -> "x"), - None)) - assert( - parse("cbx run --file=x").subcommand.get == CommandLine( - "run", - Map("file" -> "x"), - None)) - assert( - parse("cbx run --file=x=y,z=w").subcommand.get == CommandLine( - "run", - Map("file" -> "x=y,z=w"), - None)) - shouldFail("cbx run --file") - shouldFail("cbx run --file --") - } - "noArgOption" - { - shouldFail("cbx run --force=x") - assert( - parse("cbx run --force x").subcommand.get == CommandLine( - "run", - Map("force" -> "", "pipeline" -> "x"), - None)) - } - "globalOption" - { - assert(parse("cbx --server run run").arguments == Map("server" -> "run")) - assert( - parse("cbx --server run run").subcommand.get == CommandLine("run", - Map.empty, - None)) - assert(parse("cbx -s run run").arguments == Map("server" -> "run")) - assert( - parse("cbx -s run run").subcommand.get == CommandLine("run", - Map.empty, - None)) - assert(parse("cbx --server=run run").arguments == Map("server" -> "run")) - assert( - parse("cbx --server=run run").subcommand.get == CommandLine("run", - Map.empty, - None)) - shouldFail("cbx -x run") - shouldFail("cbx --x run") - } - "parameter" - { - assert( - parse("cbx login x").subcommand.get == CommandLine( - "login", - Map("server_url" -> "x"), - None)) - assert( - parse("cbx login x y").subcommand.get == CommandLine( - "login", - Map("server_url" -> "x", "username" -> "y"), - None)) - assert( - parse("cbx login x y z").subcommand.get == CommandLine( - "login", - Map("server_url" -> "x", "username" -> "y", "password" -> "z"), - None)) - shouldFail("cbx login - y z w") - assert( - parse("cbx login - y").subcommand.get == CommandLine( - "login", - Map("server_url" -> "-", "username" -> "y"), - None)) - } - "outOfOrderOptions" - { - assert( - parse("cbx run --force pipelinename -f x").subcommand.get == CommandLine( - "run", - Map("force" -> "", "pipeline" -> "pipelinename", "file" -> "x"), - None)) - assert( - parse("cbx run --force -- -f").subcommand.get == CommandLine( - "run", - Map("force" -> "", "pipeline" -> "-f"), - None)) - assert( - parse("cbx run --force -- --file").subcommand.get == CommandLine( - "run", - Map("force" -> "", "pipeline" -> "--file"), - None)) - assert( - parse("cbx run --force -- --").subcommand.get == CommandLine( - "run", - Map("force" -> "", "pipeline" -> "--"), - None)) - shouldFail("cbx run --force -- -f x") // too many parameters - } - "nested1" - { - val line = parse("cbx level1 level2-1 x=y level3 z").subcommand.get - val expected = CommandLine( - "level1", - Map.empty, - Some( - CommandLine("level2-1", - Map("p2" -> "x=y"), - Some(CommandLine("level3", Map("p3" -> "z"), None))))) - assert(line == expected) - } - "nested2" - { - val line = parse("cbx level1 level2-2 --").subcommand.get - val expected = CommandLine("level1", - Map.empty, - Some(CommandLine("level2-2", Map.empty, None))) - assert(line == expected) - } - } -} diff --git a/src/test/scala/CmdTest.scala.disabled b/src/test/scala/CmdTest.scala.disabled new file mode 100644 index 0000000..d943a79 --- /dev/null +++ b/src/test/scala/CmdTest.scala.disabled @@ -0,0 +1,207 @@ +package commando + +import utest._ + +object CmdTests extends TestSuite { + + val cbx = commando.Command( + "cbx", + commando.Optional("server", Some('s'), Optional.ArgRequired("name")), + commando.Command( + "version", + commando.Optional("verbose", Some('v'), Optional.ArgAllowed("k=v"))), + commando.Command("login", + commando.Positional("server_url"), + commando.Positional("username", false), + commando.Positional("password", false)), + commando.Command("run", + commando.Optional("file", Some('f'), Optional.ArgRequired("file_name")), + commando.Optional("force", None), + commando.Positional("pipeline", false)), + commando.Command("level1", + commando.Command("level2-1", + commando.Positional("p2"), + commando.Command("level3", commando.Positional("p3"))), + commando.Command("level2-2")) + ) + + def parse(in: String): CommandLine = commando.parse(cbx, in.split(" ").tail) match { + case Left(ex) => throw ex + case Right(res) => res + } + + def shouldFail(in: String) = + assert(commando.parse(cbx, in.split(" ").tail).isLeft) + + val tests = Tests { + "printUsage" - { + cbx.usage + } + "simple" - { + assert( + parse("cbx version").subcommand.get == CommandLine("version", + Map.empty, + None)) + } + "emptyAllowedOption" - { + assert( + parse("cbx version -v").subcommand.get == CommandLine( + "version", + Map("verbose" -> ""), + None)) + assert( + parse("cbx version --verbose").subcommand.get == CommandLine( + "version", + Map("verbose" -> ""), + None)) + } + "setAllowedOption" - { + assert( + parse("cbx version -v x").subcommand.get == CommandLine( + "version", + Map("verbose" -> "x"), + None)) + assert( + parse("cbx version --verbose x").subcommand.get == CommandLine( + "version", + Map("verbose" -> "x"), + None)) + assert( + parse("cbx version --verbose=x").subcommand.get == CommandLine( + "version", + Map("verbose" -> "x"), + None)) + assert( + parse("cbx version --verbose=x=y").subcommand.get == CommandLine( + "version", + Map("verbose" -> "x=y"), + None)) + assert( + parse("cbx version --verbose=x=y,z=w").subcommand.get == CommandLine( + "version", + Map("verbose" -> "x=y,z=w"), + None)) + assert( + parse("cbx version --verbose x=y,z=w").subcommand.get == CommandLine( + "version", + Map("verbose" -> "x=y,z=w"), + None)) + shouldFail("cbx version --verbose x=y z=w") + } + "requiredArgOption" - { + assert(parse("cbx run").subcommand.get == CommandLine("run", Map(), None)) // make sure it works first + assert( + parse("cbx run -f x").subcommand.get == CommandLine("run", + Map("file" -> "x"), + None)) + assert( + parse("cbx run --file x").subcommand.get == CommandLine( + "run", + Map("file" -> "x"), + None)) + assert( + parse("cbx run --file=x").subcommand.get == CommandLine( + "run", + Map("file" -> "x"), + None)) + assert( + parse("cbx run --file=x=y,z=w").subcommand.get == CommandLine( + "run", + Map("file" -> "x=y,z=w"), + None)) + shouldFail("cbx run --file") + shouldFail("cbx run --file --") + } + "noArgOption" - { + shouldFail("cbx run --force=x") + assert( + parse("cbx run --force x").subcommand.get == CommandLine( + "run", + Map("force" -> "", "pipeline" -> "x"), + None)) + } + "globalOption" - { + assert(parse("cbx --server run run").arguments == Map("server" -> "run")) + assert( + parse("cbx --server run run").subcommand.get == CommandLine("run", + Map.empty, + None)) + assert(parse("cbx -s run run").arguments == Map("server" -> "run")) + assert( + parse("cbx -s run run").subcommand.get == CommandLine("run", + Map.empty, + None)) + assert(parse("cbx --server=run run").arguments == Map("server" -> "run")) + assert( + parse("cbx --server=run run").subcommand.get == CommandLine("run", + Map.empty, + None)) + shouldFail("cbx -x run") + shouldFail("cbx --x run") + } + "parameter" - { + assert( + parse("cbx login x").subcommand.get == CommandLine( + "login", + Map("server_url" -> "x"), + None)) + assert( + parse("cbx login x y").subcommand.get == CommandLine( + "login", + Map("server_url" -> "x", "username" -> "y"), + None)) + assert( + parse("cbx login x y z").subcommand.get == CommandLine( + "login", + Map("server_url" -> "x", "username" -> "y", "password" -> "z"), + None)) + shouldFail("cbx login - y z w") + assert( + parse("cbx login - y").subcommand.get == CommandLine( + "login", + Map("server_url" -> "-", "username" -> "y"), + None)) + } + "outOfOrderOptions" - { + assert( + parse("cbx run --force pipelinename -f x").subcommand.get == CommandLine( + "run", + Map("force" -> "", "pipeline" -> "pipelinename", "file" -> "x"), + None)) + assert( + parse("cbx run --force -- -f").subcommand.get == CommandLine( + "run", + Map("force" -> "", "pipeline" -> "-f"), + None)) + assert( + parse("cbx run --force -- --file").subcommand.get == CommandLine( + "run", + Map("force" -> "", "pipeline" -> "--file"), + None)) + assert( + parse("cbx run --force -- --").subcommand.get == CommandLine( + "run", + Map("force" -> "", "pipeline" -> "--"), + None)) + shouldFail("cbx run --force -- -f x") // too many parameters + } + "nested1" - { + val line = parse("cbx level1 level2-1 x=y level3 z").subcommand.get + val expected = CommandLine( + "level1", + Map.empty, + Some( + CommandLine("level2-1", + Map("p2" -> "x=y"), + Some(CommandLine("level3", Map("p3" -> "z"), None))))) + assert(line == expected) + } + "nested2" - { + val line = parse("cbx level1 level2-2 --").subcommand.get + val expected = CommandLine("level1", + Map.empty, + Some(CommandLine("level2-2", Map.empty, None))) + assert(line == expected) + } + } +} diff --git a/src/test/scala/ParserTest.scala b/src/test/scala/ParserTest.scala new file mode 100644 index 0000000..11694c4 --- /dev/null +++ b/src/test/scala/ParserTest.scala @@ -0,0 +1,27 @@ +package commando + +import utest._ + +object ParserTest extends TestSuite { + + implicit class EliteCommando(line: String) { + def parse(command: Command): Unit = { + val args = line.split(" ") + commando.parse(args, command)(err => throw new ParseException(err)) + } + } + + val tests = Tests { + "foo" - { + val command = cmd("cbx")( + opt("server", 'S', param = "url" -> false), + pos("number") + ).run( + ctx => println("yoyo, my context was: " + ctx) + ) + "--server x 3 -S 5 --server=2 --server 2".parse(command) + println(command.usage) + } + } + +} -- cgit v1.2.3