Skip to content

Commit

Permalink
Merge pull request #297 from alexarchambault/develop
Browse files Browse the repository at this point in the history
Tweak help / sub-commands stuff
  • Loading branch information
alexarchambault authored Jun 7, 2021
2 parents 0fa920f + 41a8c6b commit ba47d45
Show file tree
Hide file tree
Showing 18 changed files with 287 additions and 71 deletions.
7 changes: 0 additions & 7 deletions cats/shared/src/test/scala/caseapp/cats/CatsTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,6 @@ object CatsTests extends TestSuite {
}
}
test("IOCommandApp") {
test("output usage") {
testCommandStdout(List("--usage"),
"""Usage: none.type [options] [command] [command-options]
|Available commands: first, second, third
|
|Type none.type command --usage for usage of an individual command""".stripMargin)
}
test("parse error") {
testCommandStderr(List("--invalid"), "Unrecognized argument: --invalid")
}
Expand Down
17 changes: 17 additions & 0 deletions core/js/src/main/scala/caseapp/core/app/PlatformUtil.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package caseapp.core.app

import scala.scalajs.js
import js.Dynamic.{global => g}

object PlatformUtil {
private lazy val process = g.require("process")
def exit(code: Int): Nothing = {
process.exit(code)
sys.error(s"Attempt to exit with code $code failed")
}
def arguments(args: Array[String]): Array[String] =
if (args.isEmpty)
process.argv.asInstanceOf[js.Array[String]].toArray
else
args
}
8 changes: 8 additions & 0 deletions core/jvm/src/main/scala/caseapp/core/app/PlatformUtil.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package caseapp.core.app

object PlatformUtil {
def exit(code: Int): Nothing =
sys.exit(code)
def arguments(args: Array[String]): Array[String] =
args
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package caseapp.core.app

object PlatformUtil {
def exit(code: Int): Nothing =
sys.exit(code)
def arguments(args: Array[String]): Array[String] =
args
}
58 changes: 39 additions & 19 deletions core/shared/src/main/scala/caseapp/core/app/CaseApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package caseapp.core.app
import caseapp.Name
import caseapp.core.{Error, RemainingArgs}
import caseapp.core.complete.{Completer, CompletionItem, HelpCompleter}
import caseapp.core.help.{Help, WithHelp}
import caseapp.core.help.{Help, HelpFormat, WithHelp}
import caseapp.core.parser.Parser
import caseapp.core.util.Formatter

abstract class CaseApp[T](implicit val parser0: Parser[T], val messages: Help[T]) {

def hasHelp: Boolean = true

def parser: Parser[T] = {
val p = parser0.nameFormatter(nameFormatter)
if (ignoreUnrecognized)
Expand All @@ -23,18 +25,27 @@ abstract class CaseApp[T](implicit val parser0: Parser[T], val messages: Help[T]
new HelpCompleter[T](messages)

def complete(args: Seq[String], index: Int): List[CompletionItem] =
parser.withHelp.complete(
args,
index,
completer.withHelp,
stopAtFirstUnrecognized,
ignoreUnrecognized
)
if (hasHelp)
parser.withHelp.complete(
args,
index,
completer.withHelp,
stopAtFirstUnrecognized,
ignoreUnrecognized
)
else
parser.complete(
args,
index,
completer,
stopAtFirstUnrecognized,
ignoreUnrecognized
)

def run(options: T, remainingArgs: RemainingArgs): Unit

def exit(code: Int): Nothing =
sys.exit(code)
PlatformUtil.exit(code)

def error(message: Error): Nothing = {
Console.err.println(message.message)
Expand All @@ -51,18 +62,21 @@ abstract class CaseApp[T](implicit val parser0: Parser[T], val messages: Help[T]
helpAsked("")
def helpAsked(progName: String): Nothing = {
val help = helpWithProgName(progName)
println(help.help)
println(help.help(helpFormat))
exit(0)
}

def usageAsked(): Nothing =
usageAsked("")
def usageAsked(progName: String): Nothing = {
val help = helpWithProgName(progName)
println(help.usage)
println(help.usage(helpFormat))
exit(0)
}

def helpFormat: HelpFormat =
HelpFormat.default()

def ensureNoDuplicates(): Unit =
messages.ensureNoDuplicates()

Expand Down Expand Up @@ -109,16 +123,22 @@ abstract class CaseApp[T](implicit val parser0: Parser[T], val messages: Help[T]
Formatter.DefaultNameFormatter

def main(args: Array[String]): Unit =
main(messages.progName, args)
main(messages.progName, PlatformUtil.arguments(args))

def main(progName: String, args: Array[String]): Unit =
parser.withHelp.detailedParse(expandArgs(args.toList), stopAtFirstUnrecognized, ignoreUnrecognized) match {
case Left(err) => error(err)
case Right((WithHelp(_, true, _), _)) => helpAsked(progName)
case Right((WithHelp(true, _, _), _)) => usageAsked(progName)
case Right((WithHelp(_, _, Left(err)), _)) => error(err)
case Right((WithHelp(_, _, Right(t)), remainingArgs)) => run(t, remainingArgs)
}
if (hasHelp)
parser.withHelp.detailedParse(expandArgs(args.toList), stopAtFirstUnrecognized, ignoreUnrecognized) match {
case Left(err) => error(err)
case Right((WithHelp(_, true, _), _)) => helpAsked(progName)
case Right((WithHelp(true, _, _), _)) => usageAsked(progName)
case Right((WithHelp(_, _, Left(err)), _)) => error(err)
case Right((WithHelp(_, _, Right(t)), remainingArgs)) => run(t, remainingArgs)
}
else
parser.detailedParse(expandArgs(args.toList), stopAtFirstUnrecognized, ignoreUnrecognized) match {
case Left(err) => error(err)
case Right((t, remainingArgs)) => run(t, remainingArgs)
}
}

object CaseApp {
Expand Down
2 changes: 2 additions & 0 deletions core/shared/src/main/scala/caseapp/core/app/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ abstract class Command[T](implicit parser: Parser[T], help: Help[T]) extends Cas
List(List(name))
def name: String =
help.progName
def group: String = ""
def hidden: Boolean = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ abstract class CommandApp[T](implicit
def beforeCommand(options: None.type, remainingArgs: Seq[String]): Unit =
if (remainingArgs.nonEmpty) {
Console.err.println(s"Found extra arguments: ${remainingArgs.mkString(" ")}")
sys.exit(255)
PlatformUtil.exit(255)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ abstract class CommandAppWithPreCommand[D, T](implicit
def run(options: T, remainingArgs: RemainingArgs): Unit

def exit(code: Int): Nothing =
sys.exit(code)
PlatformUtil.exit(code)

def error(message: Error): Nothing = {
Console.err.println(message.message)
Expand Down Expand Up @@ -65,7 +65,7 @@ abstract class CommandAppWithPreCommand[D, T](implicit
def progName: String = Help[D].progName

def main(args: Array[String]): Unit =
commandParser.withHelp.detailedParse(args.toVector)(beforeCommandParser.withHelp) match {
commandParser.withHelp.detailedParse(PlatformUtil.arguments(args).toVector)(beforeCommandParser.withHelp) match {
case Left(err) =>
error(err)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package caseapp.core.app
import caseapp.core.commandparser.RuntimeCommandParser
import caseapp.core.complete.{Bash, CompletionItem, Zsh}
import caseapp.core.help.{Help, HelpFormat, RuntimeCommandsHelp}
import caseapp.core.help.RuntimeCommandHelp

abstract class CommandsEntryPoint extends PlatformCommandsMethods {

Expand All @@ -17,7 +18,7 @@ abstract class CommandsEntryPoint extends PlatformCommandsMethods {
progName,
Some(description).filter(_.nonEmpty),
defaultCommand.map(_.messages.withHelp: Help[_]).getOrElse(Help[Unit]()),
commands.map(cmd => (cmd.names, cmd.messages.withHelp))
commands.map(cmd => RuntimeCommandHelp(cmd.names, cmd.messages.withHelp, cmd.group, cmd.hidden))
)

def helpFormat: HelpFormat =
Expand All @@ -40,7 +41,7 @@ abstract class CommandsEntryPoint extends PlatformCommandsMethods {
case Zsh.id => Zsh.script(progName)
case _ =>
System.err.println(s"Unrecognized completion format '$format'")
sys.exit(1)
PlatformUtil.exit(1)
}
args match {
case Array(format, dest) =>
Expand All @@ -51,7 +52,7 @@ abstract class CommandsEntryPoint extends PlatformCommandsMethods {
println(script0)
case _ =>
System.err.println(s"Usage: $progName $completionsCommandName format [dest]")
sys.exit(1)
PlatformUtil.exit(1)
}
}

Expand Down Expand Up @@ -81,32 +82,34 @@ abstract class CommandsEntryPoint extends PlatformCommandsMethods {
println(Zsh.print(items))
case _ =>
System.err.println(s"Unrecognized completion format '$format'")
sys.exit(1)
PlatformUtil.exit(1)
}
case _ =>
System.err.println(s"Usage: $progName $completeCommandName format index ...args...")
sys.exit(1)
PlatformUtil.exit(1)
}

def main(args: Array[String]): Unit =
if (enableCompleteCommand && args.startsWith(completeCommandName.toArray[String]))
completeMain(args.drop(completeCommandName.length))
else if (enableCompletionsCommand && args.startsWith(completionsCommandName.toArray[String]))
completionsMain(args.drop(completionsCommandName.length))
def main(args: Array[String]): Unit = {
val actualArgs = PlatformUtil.arguments(args)
if (enableCompleteCommand && actualArgs.startsWith(completeCommandName.toArray[String]))
completeMain(actualArgs.drop(completeCommandName.length))
else if (enableCompletionsCommand && actualArgs.startsWith(completionsCommandName.toArray[String]))
completionsMain(actualArgs.drop(completionsCommandName.length))
else
defaultCommand match {
case None =>
RuntimeCommandParser.parse(commands, args.toList) match {
RuntimeCommandParser.parse(commands, actualArgs.toList) match {
case None =>
val usage = help.help(helpFormat)
println(usage)
sys.exit(0)
PlatformUtil.exit(0)
case Some((commandName, command, commandArgs)) =>
command.main(commandProgName(commandName), commandArgs.toArray)
}
case Some(defaultCommand0) =>
val (commandName, command, commandArgs) =
RuntimeCommandParser.parse(defaultCommand0, commands, args.toList)
RuntimeCommandParser.parse(defaultCommand0, commands, actualArgs.toList)
command.main(commandProgName(commandName), commandArgs.toArray)
}
}
}
12 changes: 6 additions & 6 deletions core/shared/src/main/scala/caseapp/core/help/Help.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ import caseapp.HelpMessage

/** One-line usage message for `T` */
def usage: String =
Seq(
"Usage:",
progName,
optionsDesc,
argsNameOption.fold("")("<" + _ + ">")
).filter(_.nonEmpty).mkString(" ")
usage(HelpFormat.default())
def usage(format: HelpFormat): String = {
val b = new StringBuilder
printUsage(b, format)
b.result()
}

/** Options description for `T` */
def options: String = Help.optionsMessage(args, nameFormatter)
Expand Down
16 changes: 14 additions & 2 deletions core/shared/src/main/scala/caseapp/core/help/HelpFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ import dataclass._
option: fansi.Attrs = fansi.Attrs.Empty,
newLine: String = System.lineSeparator(),
terminalWidth: Int = 80,
@since("2.1.0")
sortGroups: Option[Seq[String] => Seq[String]] = None,
sortedGroups: Option[Seq[String]] = None
sortedGroups: Option[Seq[String]] = None,
@since("2.1.0")
sortCommandGroups: Option[Seq[String] => Seq[String]] = None,
sortedCommandGroups: Option[Seq[String]] = None
) {
def sortGroupValues[T](elems: Seq[(String, T)]): Seq[(String, T)] =
private def sortValues[T](
sortGroups: Option[Seq[String] => Seq[String]],
sortedGroups: Option[Seq[String]],
elems: Seq[(String, T)]
): Seq[(String, T)] =
sortGroups match {
case None =>
sortedGroups match {
Expand All @@ -26,6 +34,10 @@ import dataclass._
val sorted = sort(elems.map(_._1)).zipWithIndex.toMap
elems.sortBy { case (group, _) => sorted.getOrElse(group, Int.MaxValue) }
}
def sortGroupValues[T](elems: Seq[(String, T)]): Seq[(String, T)] =
sortValues(sortGroups, sortedGroups, elems)
def sortCommandGroupValues[T](elems: Seq[(String, T)]): Seq[(String, T)] =
sortValues(sortCommandGroups, sortedCommandGroups, elems)
}

object HelpFormat {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package caseapp.core.help

import dataclass.data

@data class RuntimeCommandHelp[T](
names: List[List[String]],
help: Help[T],
group: String,
hidden: Boolean
)
Loading

0 comments on commit ba47d45

Please sign in to comment.