Skip to content

Commit

Permalink
Merge pull request #460 from Gedochao/tweak-short-help
Browse files Browse the repository at this point in the history
Differentiate between `--help` and `--full-help` help format
  • Loading branch information
alexarchambault authored Feb 17, 2023
2 parents 2d51600 + bb834a7 commit c0f8c7f
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 53 deletions.
3 changes: 2 additions & 1 deletion annotations/shared/src/main/scala/caseapp/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ object ValueDescription {
* @messageMd
* not used by case-app itself, only there as a convenience for case-app users
*/
final case class HelpMessage(message: String, messageMd: String = "") extends StaticAnnotation
final case class HelpMessage(message: String, messageMd: String = "", detailedMessage: String = "")
extends StaticAnnotation

/** Name for the annotated case class of arguments E.g. MyApp
*/
Expand Down
6 changes: 6 additions & 0 deletions core/shared/src/main/scala-3/caseapp/core/Scala3Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ object Scala3Helpers {

def withFilterArgs(filterArgs: Option[Arg => Boolean]): HelpFormat =
helpFormat.copy(filterArgs = filterArgs)

def withFilterArgsWhenShowHidden(filterArgs: Option[Arg => Boolean]): HelpFormat =
helpFormat.copy(filterArgsWhenShowHidden = filterArgs)

def withHiddenGroupsWhenShowHidden(hiddenGroups: Option[Seq[String]]): HelpFormat =
helpFormat.copy(hiddenGroupsWhenShowHidden = hiddenGroups)
}

implicit class OptionParserWithOps[T](private val parser: OptionParser[T]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,14 @@ object HelpCompanion {
val helpMessage = sym.annotations
.find(_.tpe =:= TypeRepr.of[caseapp.HelpMessage])
.collect {
case Apply(_, List(arg, argMd)) =>
'{ caseapp.HelpMessage(${ arg.asExprOf[String] }, ${ argMd.asExprOf[String] }) }
case Apply(_, List(arg, argMd, argDetailed)) =>
'{
caseapp.HelpMessage(
${ arg.asExprOf[String] },
${ argMd.asExprOf[String] },
${ argDetailed.asExprOf[String] }
)
}
}
'{
val parser = $parserExpr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,14 @@ object LowPriorityParserImplicits {
val helpMessage = sym.annotations
.find(_.tpe =:= TypeRepr.of[caseapp.HelpMessage])
.collect {
case Apply(_, List(arg, argMd)) =>
'{ caseapp.HelpMessage(${ arg.asExprOf[String] }, ${ argMd.asExprOf[String] }) }
case Apply(_, List(arg, argMd, argDetailed)) =>
'{
caseapp.HelpMessage(
${ arg.asExprOf[String] },
${ argMd.asExprOf[String] },
${ argDetailed.asExprOf[String] }
)
}
}
val hidden = sym.annotations.exists(_.tpe =:= TypeRepr.of[caseapp.Hidden])
val group = sym.annotations
Expand Down
14 changes: 11 additions & 3 deletions core/shared/src/main/scala/caseapp/core/help/Help.scala
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@ import caseapp.HelpMessage
printUsage(b, format)
b.append(format.newLine)

for (desc <- helpMessage.map(_.message))
val helpDescription = helpMessage.map {
case HelpMessage(_, _, detailedMessage) if showHidden && detailedMessage.nonEmpty =>
detailedMessage
case HelpMessage(message, _, _) => message
}

for (desc <- helpDescription)
Help.printDescription(
b,
desc,
Expand All @@ -135,9 +141,11 @@ import caseapp.HelpMessage

def printOptions(b: StringBuilder, format: HelpFormat, showHidden: Boolean): Unit =
if (args.nonEmpty) {
val filteredArgs = format.filterArgs.map(args.filter).getOrElse(args)
val filteredArgs =
if (showHidden) format.filterArgsWhenShowHidden.map(args.filter).getOrElse(args)
else format.filterArgs.map(args.filter).getOrElse(args)
val groupedArgs = filteredArgs.groupBy(_.group.fold("")(_.name))
val groups = format.sortGroupValues(groupedArgs.toVector)
val groups = format.sortGroupValues(groupedArgs.toVector, showHidden)
val sortedGroups = groups.filter(_._1.nonEmpty) ++ groupedArgs.get("").toSeq.map("" -> _)
for {
((groupName, groupArgs), groupIdx) <- sortedGroups.zipWithIndex
Expand Down
19 changes: 12 additions & 7 deletions core/shared/src/main/scala/caseapp/core/help/HelpFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ import dataclass._
sortedCommandGroups: Option[Seq[String]] = None,
hidden: fansi.Attrs = fansi.Attrs.Empty,
terminalWidthOpt: Option[Int] = None,
@since filterArgs: Option[Arg => Boolean] = None
@since filterArgs: Option[Arg => Boolean] = None,
@since filterArgsWhenShowHidden: Option[Arg => Boolean] = None,
hiddenGroupsWhenShowHidden: Option[Seq[String]] = None
) {
private def sortValues[T](
sortGroups: Option[Seq[String] => Seq[String]],
sortedGroups: Option[Seq[String]],
elems: Seq[(String, T)]
elems: Seq[(String, T)],
showHidden: Boolean
): Seq[(String, T)] = {
val sortedGroups0 = sortGroups match {
case None =>
Expand All @@ -37,13 +40,15 @@ import dataclass._
val sorted = sort(elems.map(_._1)).zipWithIndex.toMap
elems.sortBy { case (group, _) => sorted.getOrElse(group, Int.MaxValue) }
}
sortedGroups0.filter { case (group, _) => hiddenGroups.forall(!_.contains(group)) }
sortedGroups0.filter { case (group, _) =>
(if (showHidden) hiddenGroupsWhenShowHidden else hiddenGroups).forall(!_.contains(group))
}
}

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)
def sortGroupValues[T](elems: Seq[(String, T)], showHidden: Boolean): Seq[(String, T)] =
sortValues(sortGroups, sortedGroups, elems, showHidden)
def sortCommandGroupValues[T](elems: Seq[(String, T)], showHidden: Boolean): Seq[(String, T)] =
sortValues(sortCommandGroups, sortedCommandGroups, elems, showHidden)
}

object HelpFormat {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ import dataclass._
commands
.filter(c => showHidden || !c.hidden)
.groupBy(_.group)
.toVector
.toVector,
showHidden
)

def table(commands: Seq[RuntimeCommandHelp[_]]) =
Expand Down
2 changes: 1 addition & 1 deletion project/Mima.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import scala.sys.process._
object Mima {

def binaryCompatibilityVersions: Set[String] =
Seq("git", "tag", "--merged", "HEAD^", "--contains", "c199a3037771d09af0a190a2b99fa8b287e6812f")
Seq("git", "tag", "--merged", "HEAD^", "--contains", "d83b49829681f550c39e31b4c526dffd8c6dba89")
.!!
.linesIterator
.map(_.trim)
Expand Down
8 changes: 7 additions & 1 deletion tests/shared/src/test/scala/caseapp/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,18 @@ object Definitions {
bar: Int
)

@HelpMessage("Example help message")
@HelpMessage("Example help message", "", "Example detailed help message")
final case class ExampleWithHelpMessage(
foo: String,
bar: Int
)

@HelpMessage("Example help message")
final case class SimpleExampleWithHelpMessage(
foo: String,
bar: Int
)

sealed trait Command

case class First(
Expand Down
187 changes: 152 additions & 35 deletions tests/shared/src/test/scala/caseapp/HelpTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,36 @@ object HelpTests extends TestSuite {
checkLines(message, expectedMessage)
}

test("generate a help message with detailed description") {

val message = Help[ExampleWithHelpMessage].help(format, showHidden = true)

val expectedMessage =
"""Usage: example-with-help-message [options]
|Example detailed help message
|
|Options:
| --foo string
| --bar int""".stripMargin

checkLines(message, expectedMessage)
}

test("generate a help message falling back to standard description") {

val message = Help[SimpleExampleWithHelpMessage].help(format, showHidden = true)

val expectedMessage =
"""Usage: simple-example-with-help-message [options]
|Example help message
|
|Options:
| --foo string
| --bar int""".stripMargin

checkLines(message, expectedMessage)
}

test("group options") {

val orderedGroups = Seq("Something", "Bb").zipWithIndex.toMap
Expand Down Expand Up @@ -373,7 +403,7 @@ object HelpTests extends TestSuite {

assert(help == expected)
}
test("help message with hidden group") {
test("short help message with hidden group") {
val entryPoint = new CommandsEntryPoint {
def progName = "foo"
override def defaultCommand = Some(CommandGroups.First)
Expand All @@ -383,24 +413,69 @@ object HelpTests extends TestSuite {
CommandGroups.First.group,
CommandGroups.Third.group
)))
val help = entryPoint.help.help(formatWithHiddenGroup)
val expected =
"""Usage: foo <COMMAND> [options]
|
|Help options:
| --usage Print usage and exit
| -h, -help, --help Print help message and exit
|
|Other options:
| -f, --foo string
| --bar int
|
|Bb commands:
| second""".stripMargin
val shortHelp = entryPoint.help.help(formatWithHiddenGroup)
val fullHelp = entryPoint.help.help(formatWithHiddenGroup, showHidden = true)
val hiddenGroupEntries = """
|
|Aa commands:
| first
| third Third help message""".stripMargin
def expected(showHidden: Boolean) =
s"""Usage: foo <COMMAND> [options]
|
|Help options:
| --usage Print usage and exit
| -h, -help, --help Print help message and exit
|
|Other options:
| -f, --foo string
| --bar int${if (showHidden) hiddenGroupEntries else ""}
|
|Bb commands:
| second""".stripMargin

assert(shortHelp == expected(showHidden = false))
assert(fullHelp == expected(showHidden = true))
}
test("full help message with hidden group") {
val entryPoint = new CommandsEntryPoint {
def progName = "foo"

assert(help == expected)
override def defaultCommand = Some(CommandGroups.First)

def commands = Seq(CommandGroups.First, CommandGroups.Second, CommandGroups.Third)
}
val formatWithHiddenGroup = format.withHiddenGroupsWhenShowHidden(Some(Seq(
CommandGroups.First.group,
CommandGroups.Third.group
)))
val shortHelp = entryPoint.help.help(formatWithHiddenGroup)
val fullHelp = entryPoint.help.help(formatWithHiddenGroup, showHidden = true)
val hiddenGroupEntries =
"""
|
|Aa commands:
| first
| third Third help message""".stripMargin

def expected(showHidden: Boolean) =
s"""Usage: foo <COMMAND> [options]
|
|Help options:
| --usage Print usage and exit
| -h, -help, --help Print help message and exit
|
|Other options:
| -f, --foo string
| --bar int${if (showHidden) "" else hiddenGroupEntries}
|
|Bb commands:
| second""".stripMargin

assert(shortHelp == expected(showHidden = false))
assert(fullHelp == expected(showHidden = true))
}
test("help message with filtered args") {
test("short help message with filtered args") {
val entryPoint: CommandsEntryPoint = new CommandsEntryPoint {
def progName = "foo"

Expand All @@ -410,25 +485,67 @@ object HelpTests extends TestSuite {
}
val filterArgsFunction = (a: Arg) => !a.tags.exists(_.name == "foo")
val formatWithHiddenGroup = format.withFilterArgs(Some(filterArgsFunction))
val help = entryPoint.help.help(formatWithHiddenGroup)
val expected =
"""Usage: foo <COMMAND> [options]
|
|Help options:
| --usage Print usage and exit
| -h, -help, --help Print help message and exit
|
|Other options:
| --bar int
|
|Aa commands:
| first
| third Third help message
|
|Bb commands:
| second""".stripMargin
val shortHelp = entryPoint.help.help(formatWithHiddenGroup)
val fullHelp = entryPoint.help.help(formatWithHiddenGroup, showHidden = true)
val fooEntry =
"""
| -f, --foo string""".stripMargin
def expected(showHidden: Boolean) =
s"""Usage: foo <COMMAND> [options]
|
|Help options:
| --usage Print usage and exit
| -h, -help, --help Print help message and exit
|
|Other options:${if (showHidden) fooEntry else ""}
| --bar int
|
|Aa commands:
| first
| third Third help message
|
|Bb commands:
| second""".stripMargin
assert(shortHelp == expected(showHidden = false))
assert(fullHelp == expected(showHidden =
true
)) // the filter shouldn't be applied for full help
}
test("full help message with filtered args") {
val entryPoint: CommandsEntryPoint = new CommandsEntryPoint {
def progName = "foo"

assert(help == expected)
override def defaultCommand = Some(CommandGroups.First)

def commands = Seq(CommandGroups.First, CommandGroups.Second, CommandGroups.Third)
}
val filterArgsFunction = (a: Arg) => !a.tags.exists(_.name == "foo")
val formatWithHiddenGroup = format.withFilterArgsWhenShowHidden(Some(filterArgsFunction))
val shortHelp = entryPoint.help.help(formatWithHiddenGroup)
val fullHelp = entryPoint.help.help(formatWithHiddenGroup, showHidden = true)
val fooEntry =
"""
| -f, --foo string""".stripMargin
def expected(showHidden: Boolean) =
s"""Usage: foo <COMMAND> [options]
|
|Help options:
| --usage Print usage and exit
| -h, -help, --help Print help message and exit
|
|Other options:${if (showHidden) "" else fooEntry}
| --bar int
|
|Aa commands:
| first
| third Third help message
|
|Bb commands:
| second""".stripMargin
assert(shortHelp == expected(showHidden =
false
)) // the filter shouldn't be applied for short help
assert(fullHelp == expected(showHidden = true))
}
test("hidden commands in help message") {
val entryPoint = new CommandsEntryPoint {
Expand Down

0 comments on commit c0f8c7f

Please sign in to comment.