Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Differentiate between --help and --full-help help format #460

Merged
merged 4 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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