diff --git a/build.sc b/build.sc index b9567485..6116206b 100644 --- a/build.sc +++ b/build.sc @@ -84,33 +84,18 @@ object core extends Module { annotations.jvm(), util.jvm() ) - - object test extends Tests with TestCrossSources { - def ivyDeps = Agg(Deps.utest) - def testFramework = "utest.runner.Framework" - } } trait CoreJs extends Core with CaseAppScalaJsModule with MimaChecks { def moduleDeps = Seq( annotations.js(), util.js() ) - - object test extends SbtModuleTests with ScalaJSTests with TestCrossSources { - def ivyDeps = Agg(Deps.utest) - def testFramework = "utest.runner.Framework" - } } trait CoreNative extends Core with CaseAppScalaNativeModule { def moduleDeps = Seq( annotations.native(), util.native() ) - - object test extends SbtModuleTests with ScalaNativeTests with TestCrossSources { - def ivyDeps = Agg(Deps.utest) - def testFramework = "utest.runner.Framework" - } } trait Core extends CrossSbtModule with CrossSources with CaseAppPublishModule { @@ -171,7 +156,7 @@ object cats2 extends Module { trait Cats2Jvm extends Cats2 with MimaChecks { def moduleDeps = Seq(core.jvm()) - def sources = cats.jvm().sources() + def sources = T.sources(cats.jvm().sources()) object test extends Tests with TestCrossSources { def ivyDeps = Agg(Deps.utest) @@ -180,7 +165,7 @@ object cats2 extends Module { } trait Cats2Js extends Cats2 with CaseAppScalaJsModule with MimaChecks { def moduleDeps = Seq(core.js()) - def sources = cats.js().sources() + def sources = T.sources(cats.js().sources()) object test extends SbtModuleTests with ScalaJSTests with TestCrossSources { def ivyDeps = Agg(Deps.utest) diff --git a/cats/shared/src/main/scala/caseapp/cats/IOCaseApp.scala b/cats/shared/src/main/scala/caseapp/catseffect/IOCaseApp.scala similarity index 99% rename from cats/shared/src/main/scala/caseapp/cats/IOCaseApp.scala rename to cats/shared/src/main/scala/caseapp/catseffect/IOCaseApp.scala index 0916e145..91e265f3 100644 --- a/cats/shared/src/main/scala/caseapp/cats/IOCaseApp.scala +++ b/cats/shared/src/main/scala/caseapp/catseffect/IOCaseApp.scala @@ -1,4 +1,4 @@ -package caseapp.cats +package caseapp.catseffect import caseapp.core.Error import caseapp.core.help.{Help, WithHelp} diff --git a/cats/shared/src/main/scala/caseapp/cats/CatsArgParser.scala b/cats/shared/src/main/scala/caseapp/catseffect/package.scala similarity index 91% rename from cats/shared/src/main/scala/caseapp/cats/CatsArgParser.scala rename to cats/shared/src/main/scala/caseapp/catseffect/package.scala index 25aa6c86..8292cec3 100644 --- a/cats/shared/src/main/scala/caseapp/cats/CatsArgParser.scala +++ b/cats/shared/src/main/scala/caseapp/catseffect/package.scala @@ -1,9 +1,9 @@ -package caseapp.cats +package caseapp import caseapp.core.argparser.{AccumulatorArgParser, ArgParser} import cats.data.NonEmptyList -object CatsArgParser { +package object catseffect { implicit def nonEmptyListArgParser[T]( implicit parser: ArgParser[T] ): AccumulatorArgParser[NonEmptyList[T]] = diff --git a/cats/shared/src/test/scala/caseapp/cats/CatsTests.scala b/cats/shared/src/test/scala/caseapp/catseffect/CatsTests.scala similarity index 93% rename from cats/shared/src/test/scala/caseapp/cats/CatsTests.scala rename to cats/shared/src/test/scala/caseapp/catseffect/CatsTests.scala index b28af382..aef5cbff 100644 --- a/cats/shared/src/test/scala/caseapp/cats/CatsTests.scala +++ b/cats/shared/src/test/scala/caseapp/catseffect/CatsTests.scala @@ -1,15 +1,13 @@ -package caseapp.cats +package caseapp.catseffect -import _root_.cats.effect._ -import _root_.cats.effect.unsafe.implicits.global -import _root_.cats.data.NonEmptyList +import cats.effect._ +import cats.effect.unsafe.implicits.global +import cats.data.NonEmptyList import caseapp._ import caseapp.core.help.Help import caseapp.core.Error import utest._ -import caseapp.cats.CatsArgParser._ - sealed trait RecordedApp { val stdoutBuff: Ref[IO, List[String]] = Ref.unsafe(List.empty) diff --git a/cats2/shared/src/test/scala/caseapp/cats/Definitions.scala b/cats/shared/src/test/scala/caseapp/catseffect/Definitions.scala similarity index 90% rename from cats2/shared/src/test/scala/caseapp/cats/Definitions.scala rename to cats/shared/src/test/scala/caseapp/catseffect/Definitions.scala index 76d66487..3fa1e175 100644 --- a/cats2/shared/src/test/scala/caseapp/cats/Definitions.scala +++ b/cats/shared/src/test/scala/caseapp/catseffect/Definitions.scala @@ -1,7 +1,7 @@ package caseapp -package cats +package catseffect -import _root_.cats.data.NonEmptyList +import cats.data.NonEmptyList object Definitions { diff --git a/cats2/shared/src/test/scala/caseapp/cats/CatsTests.scala b/cats2/shared/src/test/scala/caseapp/catseffect/CatsTests.scala similarity index 92% rename from cats2/shared/src/test/scala/caseapp/cats/CatsTests.scala rename to cats2/shared/src/test/scala/caseapp/catseffect/CatsTests.scala index d6a654c0..bb05316b 100644 --- a/cats2/shared/src/test/scala/caseapp/cats/CatsTests.scala +++ b/cats2/shared/src/test/scala/caseapp/catseffect/CatsTests.scala @@ -1,16 +1,14 @@ -package caseapp.cats +package caseapp.catseffect -import _root_.cats.effect._ -import _root_.cats.effect.concurrent.Ref -import _root_.cats.implicits._ -import _root_.cats.data.NonEmptyList +import cats.effect._ +import cats.effect.concurrent.Ref +import cats.implicits._ +import cats.data.NonEmptyList import caseapp._ import caseapp.core.help.Help import caseapp.core.Error import utest._ -import caseapp.cats.CatsArgParser._ - sealed trait RecordedApp { val stdoutBuff: Ref[IO, List[String]] = Ref.unsafe(List.empty) diff --git a/cats/shared/src/test/scala/caseapp/cats/Definitions.scala b/cats2/shared/src/test/scala/caseapp/catseffect/Definitions.scala similarity index 90% rename from cats/shared/src/test/scala/caseapp/cats/Definitions.scala rename to cats2/shared/src/test/scala/caseapp/catseffect/Definitions.scala index 76d66487..3fa1e175 100644 --- a/cats/shared/src/test/scala/caseapp/cats/Definitions.scala +++ b/cats2/shared/src/test/scala/caseapp/catseffect/Definitions.scala @@ -1,7 +1,7 @@ package caseapp -package cats +package catseffect -import _root_.cats.data.NonEmptyList +import cats.data.NonEmptyList object Definitions { diff --git a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala index a49662c5..06dbda63 100644 --- a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala +++ b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala @@ -1,6 +1,12 @@ package caseapp.core.app -import caseapp.core.complete.{Bash, Fish, Zsh} +import caseapp.core.complete.{ + Bash, + CompletionsInstallOptions, + CompletionsUninstallOptions, + Fish, + Zsh +} import java.io.File import java.nio.charset.{Charset, StandardCharsets} @@ -21,7 +27,7 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => // Adapted from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletions.scala def completionsInstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { - val (options, rem) = CaseApp.process[PlatformCommandsMethods.CompletionsInstallOptions](args) + val (options, rem) = CaseApp.process[CompletionsInstallOptions](args) lazy val completionsDir = Paths.get(options.output.getOrElse(completionsWorkingDirectory)) @@ -128,7 +134,7 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => } def completionsUninstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { - val (options, rem) = CaseApp.process[PlatformCommandsMethods.CompletionsUninstallOptions](args) + val (options, rem) = CaseApp.process[CompletionsUninstallOptions](args) val name = options.name.getOrElse(Paths.get(progName).getFileName.toString) @@ -164,55 +170,6 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => } object PlatformCommandsMethods { - import caseapp.{HelpMessage, Name} - import caseapp.core.help.Help - import caseapp.core.parser.Parser - - // from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletionsOptions.scala - // format: off - final case class CompletionsInstallOptions( - @HelpMessage("Print completions to stdout") - env: Boolean = false, - @HelpMessage("Custom completions name") - name: Option[String] = None, - @HelpMessage("Name of the shell, either zsh, fish or bash") - @Name("shell") - format: Option[String] = None, - @HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)") - @Name("o") - output: Option[String] = None, - @HelpMessage("Custom banner in comment placed in rc file (bash or zsh only)") - banner: String = "{NAME} completions", - @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell (bash or zsh only)") - rcFile: Option[String] = None - ) - // format: on - - object CompletionsInstallOptions { - implicit lazy val parser: Parser[CompletionsInstallOptions] = Parser.derive - implicit lazy val help: Help[CompletionsInstallOptions] = Help.derive - } - - // from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/uninstallcompletions/SharedUninstallCompletionsOptions.scala - // format: off - final case class CompletionsUninstallOptions( - @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell (bash or zsh only)") - rcFile: Option[String] = None, - @HelpMessage("Custom banner in comment placed in rc file") - banner: String = "{NAME} completions", - @HelpMessage("Custom completions name") - name: Option[String] = None, - @HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)") - @Name("o") - output: Option[String] = None, - ) - // format: on - - object CompletionsUninstallOptions { - implicit lazy val parser: Parser[CompletionsUninstallOptions] = Parser.derive - implicit lazy val help: Help[CompletionsUninstallOptions] = Help.derive - } - def getFormat(format: Option[String]): Option[String] = format.map(_.trim).filter(_.nonEmpty) .orElse { diff --git a/core/shared/src/main/scala-2/caseapp/core/help/HelpCompanion.scala b/core/shared/src/main/scala-2/caseapp/core/help/HelpCompanion.scala index b2b0a229..dce43678 100644 --- a/core/shared/src/main/scala-2/caseapp/core/help/HelpCompanion.scala +++ b/core/shared/src/main/scala-2/caseapp/core/help/HelpCompanion.scala @@ -41,7 +41,13 @@ abstract class HelpCompanion { helpMessage: AnnotationOption[HelpMessage, T] ): Help[T] = { - val appName0 = appName().fold(typeable.describe.stripSuffix("Options"))(_.appName) + val appName0 = appName() match { + case None => + if (typeable.describe == "Options") typeable.describe + else typeable.describe.stripSuffix("Options") + case Some(name) => + name.appName + } Help( parser.args, diff --git a/core/shared/src/main/scala/caseapp/core/Arg.scala b/core/shared/src/main/scala/caseapp/core/Arg.scala index 0c99ef7b..28da64d7 100644 --- a/core/shared/src/main/scala/caseapp/core/Arg.scala +++ b/core/shared/src/main/scala/caseapp/core/Arg.scala @@ -36,6 +36,7 @@ import dataclass._ def withDefaultOrigin(defaultOrigin: String): Arg = if (origin.isEmpty) this.withOrigin(Some(defaultOrigin)) else this + lazy val names = name +: extraNames } object Arg { diff --git a/core/shared/src/main/scala/caseapp/core/RemainingArgs.scala b/core/shared/src/main/scala/caseapp/core/RemainingArgs.scala index d0a65e18..199d5dae 100644 --- a/core/shared/src/main/scala/caseapp/core/RemainingArgs.scala +++ b/core/shared/src/main/scala/caseapp/core/RemainingArgs.scala @@ -14,13 +14,16 @@ import dataclass.data indexedUnparsed: Seq[Indexed[String]] ) { - def remaining: Seq[String] = indexedRemaining.map(_.value) - def unparsed: Seq[String] = indexedUnparsed.map(_.value) + lazy val remaining: Seq[String] = indexedRemaining.map(_.value) + lazy val unparsed: Seq[String] = indexedUnparsed.map(_.value) /** Arguments both before and after a `--`. * * The first `--`, if any, is not included in this list. */ - def all: Seq[String] = + lazy val all: Seq[String] = remaining ++ unparsed + + lazy val indexed: Seq[Indexed[String]] = + indexedRemaining ++ indexedUnparsed } diff --git a/core/shared/src/main/scala/caseapp/core/app/CaseApp.scala b/core/shared/src/main/scala/caseapp/core/app/CaseApp.scala index 1ddbcc6e..9c17cc55 100644 --- a/core/shared/src/main/scala/caseapp/core/app/CaseApp.scala +++ b/core/shared/src/main/scala/caseapp/core/app/CaseApp.scala @@ -10,9 +10,14 @@ import caseapp.core.util.Formatter abstract class CaseApp[T](implicit val parser0: Parser[T], val messages: Help[T]) { + def name: String = + help.progName + def hasHelp: Boolean = true def hasFullHelp: Boolean = false + def help: Help[T] = messages + def parser: Parser[T] = { val p = parser0.nameFormatter(nameFormatter) if (ignoreUnrecognized) @@ -69,10 +74,14 @@ abstract class CaseApp[T](implicit val parser0: Parser[T], val messages: Help[T] exit(1) } - lazy val finalHelp: Help[_] = - if (hasFullHelp) messages.withFullHelp - else if (hasHelp) messages.withHelp - else messages + lazy val finalHelp: Help[_] = { + val h = + if (hasFullHelp) messages.withFullHelp + else if (hasHelp) messages.withHelp + else messages + if (name == h.progName) h + else h.withProgName(name) + } def fullHelpAsked(progName: String): Nothing = { val help = if (progName.isEmpty) finalHelp else finalHelp.withProgName(progName) diff --git a/core/shared/src/main/scala/caseapp/core/app/Command.scala b/core/shared/src/main/scala/caseapp/core/app/Command.scala index e3766ce7..e85844f4 100644 --- a/core/shared/src/main/scala/caseapp/core/app/Command.scala +++ b/core/shared/src/main/scala/caseapp/core/app/Command.scala @@ -7,8 +7,6 @@ abstract class Command[T](implicit parser: Parser[T], help: Help[T]) extends CaseApp()(parser, help) { def names: List[List[String]] = List(List(name)) - def name: String = - help.progName def group: String = "" def hidden: Boolean = false } diff --git a/core/shared/src/main/scala/caseapp/core/app/CommandsEntryPoint.scala b/core/shared/src/main/scala/caseapp/core/app/CommandsEntryPoint.scala index 41c9df8e..d3336ef2 100644 --- a/core/shared/src/main/scala/caseapp/core/app/CommandsEntryPoint.scala +++ b/core/shared/src/main/scala/caseapp/core/app/CommandsEntryPoint.scala @@ -48,6 +48,28 @@ abstract class CommandsEntryPoint extends PlatformCommandsMethods { printLine("", toStderr = true) } + def completePrintInstructions(toStderr: Boolean): Unit = { + val formats = Seq(Bash.id, Zsh.id, Fish.id) + printLine("To manually get completions, run", toStderr = toStderr) + printLine("", toStderr = toStderr) + printLine( + s" $progName ${completeCommandName.mkString(" ")} ${formats.mkString("|")} index command...", + toStderr = toStderr + ) + printLine("", toStderr = toStderr) + printLine( + "where index starts from one, and command... includes the command name, like", + toStderr = toStderr + ) + printLine("", toStderr = toStderr) + printLine( + s" $progName ${completeCommandName.mkString(" ")} ${Zsh.id} 2 $progName --", + toStderr = toStderr + ) + printLine("", toStderr = toStderr) + printLine("to get completions for '--'", toStderr = toStderr) + } + def completionsPrintUsage(): Nothing = { completionsPrintInstructions() exit(1) @@ -58,11 +80,6 @@ abstract class CommandsEntryPoint extends PlatformCommandsMethods { exit(1) } - def completePrintUsage(): Nothing = { - completionsPrintInstructions() - exit(1) - } - def completionsWorkingDirectory: Option[String] = None def completionsMain(args: Array[String]): Unit = { @@ -122,8 +139,11 @@ abstract class CommandsEntryPoint extends PlatformCommandsMethods { case _ => completeUnrecognizedFormat(format) } + case Array("--help" | "--usage" | "-h") => + completePrintInstructions(toStderr = false) case _ => - completePrintUsage() + completePrintInstructions(toStderr = true) + exit(1) } } diff --git a/core/shared/src/main/scala/caseapp/core/argparser/ArgParser.scala b/core/shared/src/main/scala/caseapp/core/argparser/ArgParser.scala index efd6bf4d..e7515df1 100644 --- a/core/shared/src/main/scala/caseapp/core/argparser/ArgParser.scala +++ b/core/shared/src/main/scala/caseapp/core/argparser/ArgParser.scala @@ -86,6 +86,12 @@ object ArgParser extends PlatformArgParsers { /** Look for an implicit `ArgParser[T]` */ def apply[T](implicit parser: ArgParser[T]): ArgParser[T] = parser + implicit def byte: ArgParser[Byte] = + SimpleArgParser.byte + + implicit def short: ArgParser[Short] = + SimpleArgParser.short + implicit def int: ArgParser[Int] = SimpleArgParser.int diff --git a/core/shared/src/main/scala/caseapp/core/argparser/SimpleArgParser.scala b/core/shared/src/main/scala/caseapp/core/argparser/SimpleArgParser.scala index 071af8a3..6f48d1f8 100644 --- a/core/shared/src/main/scala/caseapp/core/argparser/SimpleArgParser.scala +++ b/core/shared/src/main/scala/caseapp/core/argparser/SimpleArgParser.scala @@ -23,6 +23,24 @@ object SimpleArgParser { def from[T](description: String)(parse: String => Either[Error, T]): SimpleArgParser[T] = SimpleArgParser(description, (value, _, _) => parse(value)) + val byte: SimpleArgParser[Byte] = + from("byte") { s => + try Right(s.toByte) + catch { + case _: NumberFormatException => + Left(Error.MalformedValue("byte-sized integer", s)) + } + } + + val short: SimpleArgParser[Short] = + from("short") { s => + try Right(s.toShort) + catch { + case _: NumberFormatException => + Left(Error.MalformedValue("short integer", s)) + } + } + val int: SimpleArgParser[Int] = from("int") { s => try Right(s.toInt) diff --git a/core/shared/src/main/scala/caseapp/core/commandparser/RuntimeCommandParser.scala b/core/shared/src/main/scala/caseapp/core/commandparser/RuntimeCommandParser.scala index 0178f4c5..f258ab8a 100644 --- a/core/shared/src/main/scala/caseapp/core/commandparser/RuntimeCommandParser.scala +++ b/core/shared/src/main/scala/caseapp/core/commandparser/RuntimeCommandParser.scala @@ -8,19 +8,19 @@ import caseapp.core.complete.CompletionItem object RuntimeCommandParser { - def parse( - apps: Map[List[String], CaseApp[_]], + def parse[T]( + apps: Map[List[String], T], args: List[String] - ): Option[(List[String], CaseApp[_], List[String])] = { + ): Option[(List[String], T, List[String])] = { val tree = CommandTree.fromCommandMap(apps) tree.command(args) } - def parse( - defaultApp: CaseApp[_], - apps: Map[List[String], CaseApp[_]], + def parse[T]( + defaultApp: T, + apps: Map[List[String], T], args: List[String] - ): (List[String], CaseApp[_], List[String]) = { + ): (List[String], T, List[String]) = { val tree = CommandTree.fromCommandMap(apps) tree.command(args).getOrElse((Nil, defaultApp, args)) } diff --git a/core/shared/src/main/scala/caseapp/core/complete/Completer.scala b/core/shared/src/main/scala/caseapp/core/complete/Completer.scala index 709e30f9..200a1b8b 100644 --- a/core/shared/src/main/scala/caseapp/core/complete/Completer.scala +++ b/core/shared/src/main/scala/caseapp/core/complete/Completer.scala @@ -16,25 +16,83 @@ trait Completer[-T] { self => def postDoubleDash(state: Option[T], args: RemainingArgs): Option[Completer[T]] = None - def contramapOpt[U](f: U => Option[T]): Completer[U] = - new Completer[U] { - def optionName(prefix: String, state: Option[U], args: RemainingArgs): List[CompletionItem] = - self.optionName(prefix, state.flatMap(f), args) - def optionValue( - arg: Arg, - prefix: String, - state: Option[U], - args: RemainingArgs - ): List[CompletionItem] = - self.optionValue(arg, prefix, state.flatMap(f), args) - def argument(prefix: String, state: Option[U], args: RemainingArgs): List[CompletionItem] = - self.argument(prefix, state.flatMap(f), args) - - override def postDoubleDash(state: Option[U], args: RemainingArgs): Option[Completer[U]] = - self.postDoubleDash(state.flatMap(f), args).map(_.contramapOpt(f)) - } - def withHelp: Completer[WithHelp[T]] = + final def contramapOpt[U](f: U => Option[T]): Completer[U] = + Completer.Mapped(this, f) + final def withHelp: Completer[WithHelp[T]] = contramapOpt(_.baseOrError.toOption) - def withFullHelp: Completer[WithFullHelp[T]] = + final def withFullHelp: Completer[WithFullHelp[T]] = contramapOpt(_.withHelp.baseOrError.toOption) } + +object Completer { + + implicit class CompleterOps[T](private val completer: Completer[T]) extends AnyVal { + def completeOptionValue(f: ( + Arg, + String, + Option[T], + RemainingArgs + ) => Option[List[CompletionItem]]): Completer[T] = + WithOptionValue(completer, f) + } + + class DefaultTo[-T](default: Completer[T]) extends Completer[T] { + override def optionName( + prefix: String, + state: Option[T], + args: RemainingArgs + ): List[CompletionItem] = + default.optionName(prefix, state, args) + override def optionValue( + arg: Arg, + prefix: String, + state: Option[T], + args: RemainingArgs + ): List[CompletionItem] = + default.optionValue(arg, prefix, state, args) + override def argument( + prefix: String, + state: Option[T], + args: RemainingArgs + ): List[CompletionItem] = + default.argument(prefix, state, args) + override def postDoubleDash(state: Option[T], args: RemainingArgs): Option[Completer[T]] = + default.postDoubleDash(state, args) + + override def toString(): String = + s"Completer.DefaultTo($default)" + } + + private final case class WithOptionValue[T]( + self: Completer[T], + f: (Arg, String, Option[T], RemainingArgs) => Option[List[CompletionItem]] + ) extends DefaultTo[T](self) { + override def optionValue( + arg: Arg, + prefix: String, + state: Option[T], + args: RemainingArgs + ): List[CompletionItem] = + f(arg, prefix, state, args).getOrElse { + super.optionValue(arg, prefix, state, args) + } + } + + private final case class Mapped[T, U](self: Completer[T], f: U => Option[T]) + extends Completer[U] { + def optionName(prefix: String, state: Option[U], args: RemainingArgs): List[CompletionItem] = + self.optionName(prefix, state.flatMap(f), args) + def optionValue( + arg: Arg, + prefix: String, + state: Option[U], + args: RemainingArgs + ): List[CompletionItem] = + self.optionValue(arg, prefix, state.flatMap(f), args) + def argument(prefix: String, state: Option[U], args: RemainingArgs): List[CompletionItem] = + self.argument(prefix, state.flatMap(f), args) + + override def postDoubleDash(state: Option[U], args: RemainingArgs): Option[Completer[U]] = + self.postDoubleDash(state.flatMap(f), args).map(_.contramapOpt(f)) + } +} diff --git a/core/shared/src/main/scala/caseapp/core/complete/CompletionsInstallOptions.scala b/core/shared/src/main/scala/caseapp/core/complete/CompletionsInstallOptions.scala new file mode 100644 index 00000000..d3227fa7 --- /dev/null +++ b/core/shared/src/main/scala/caseapp/core/complete/CompletionsInstallOptions.scala @@ -0,0 +1,30 @@ +package caseapp.core.complete + +import caseapp.{HelpMessage, Name} +import caseapp.core.help.Help +import caseapp.core.parser.Parser + +// from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletionsOptions.scala +// format: off +final case class CompletionsInstallOptions( + @HelpMessage("Print completions to stdout") + env: Boolean = false, + @HelpMessage("Custom completions name") + name: Option[String] = None, + @HelpMessage("Name of the shell, either zsh, fish or bash") + @Name("shell") + format: Option[String] = None, + @HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)") + @Name("o") + output: Option[String] = None, + @HelpMessage("Custom banner in comment placed in rc file (bash or zsh only)") + banner: String = "{NAME} completions", + @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell (bash or zsh only)") + rcFile: Option[String] = None +) +// format: on + +object CompletionsInstallOptions { + implicit lazy val parser: Parser[CompletionsInstallOptions] = Parser.derive + implicit lazy val help: Help[CompletionsInstallOptions] = Help.derive +} diff --git a/core/shared/src/main/scala/caseapp/core/complete/CompletionsUninstallOptions.scala b/core/shared/src/main/scala/caseapp/core/complete/CompletionsUninstallOptions.scala new file mode 100644 index 00000000..6d12237b --- /dev/null +++ b/core/shared/src/main/scala/caseapp/core/complete/CompletionsUninstallOptions.scala @@ -0,0 +1,25 @@ +package caseapp.core.complete + +import caseapp.{HelpMessage, Name} +import caseapp.core.help.Help +import caseapp.core.parser.Parser + +// from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/uninstallcompletions/SharedUninstallCompletionsOptions.scala +// format: off +final case class CompletionsUninstallOptions( + @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell (bash or zsh only)") + rcFile: Option[String] = None, + @HelpMessage("Custom banner in comment placed in rc file") + banner: String = "{NAME} completions", + @HelpMessage("Custom completions name") + name: Option[String] = None, + @HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)") + @Name("o") + output: Option[String] = None, +) +// format: on + +object CompletionsUninstallOptions { + implicit lazy val parser: Parser[CompletionsUninstallOptions] = Parser.derive + implicit lazy val help: Help[CompletionsUninstallOptions] = Help.derive +} diff --git a/core/shared/src/main/scala/caseapp/core/complete/Fish.scala b/core/shared/src/main/scala/caseapp/core/complete/Fish.scala index 36f970ce..dd773f14 100644 --- a/core/shared/src/main/scala/caseapp/core/complete/Fish.scala +++ b/core/shared/src/main/scala/caseapp/core/complete/Fish.scala @@ -8,8 +8,7 @@ object Fish { s"$shellName-v1" def script(progName: String): String = - s""" - complete $progName -a '($progName complete $id (math 1 + (count (__fish_print_cmd_args))) (__fish_print_cmd_args))' + s"""complete $progName -a '($progName complete $id (math 1 + (count (__fish_print_cmd_args))) (__fish_print_cmd_args))' |""".stripMargin private def escape(s: String): String = diff --git a/core/shared/src/main/scala/caseapp/core/complete/HelpCompleter.scala b/core/shared/src/main/scala/caseapp/core/complete/HelpCompleter.scala index ec453072..1e09b031 100644 --- a/core/shared/src/main/scala/caseapp/core/complete/HelpCompleter.scala +++ b/core/shared/src/main/scala/caseapp/core/complete/HelpCompleter.scala @@ -9,7 +9,7 @@ class HelpCompleter[T](help: Help[T]) extends Completer[T] { .args .iterator .flatMap { arg => - val names = (arg.name +: arg.extraNames) + val names = arg.names .map(help.nameFormatter.format) .map(n => (if (n.length == 1) "-" else "--") + n) .filter(_.startsWith(prefix)) diff --git a/core/shared/src/main/scala/caseapp/core/help/Help.scala b/core/shared/src/main/scala/caseapp/core/help/Help.scala index 779bab2f..afff4071 100644 --- a/core/shared/src/main/scala/caseapp/core/help/Help.scala +++ b/core/shared/src/main/scala/caseapp/core/help/Help.scala @@ -68,8 +68,7 @@ import caseapp.HelpMessage } def duplicates: Map[String, Seq[Arg]] = { - val pairs = args.map(a => a.name.option(nameFormatter) -> a) ++ - args.flatMap(a => a.extraNames.map(n => n.option(nameFormatter) -> a)) + val pairs = args.flatMap(a => a.names.map(n => n.option(nameFormatter) -> a)) pairs .groupBy(_._1) .mapValues(_.map(_._2)) @@ -199,7 +198,7 @@ object Help extends HelpCompanion { args .collect { case arg if showHidden || !arg.noHelp => - val names = (arg.name +: arg.extraNames).distinct + val names = arg.names.distinct // FIXME Flags that accept no value are not given the right help message here val valueDescription = arg @@ -229,8 +228,9 @@ object Help extends HelpCompanion { for (arg <- args if showHidden || !arg.noHelp) yield { val namesToShow = format.namesLimit match { case Some(limit) if !showHidden && limit >= 0 => - (arg.name +: arg.extraNames).take(limit) - case _ => arg.name +: arg.extraNames + arg.names.take(limit) + case _ => + arg.names } val sortedNames = namesToShow diff --git a/core/shared/src/main/scala/caseapp/core/parser/StandardArgument.scala b/core/shared/src/main/scala/caseapp/core/parser/StandardArgument.scala index fcf1a139..df90cc50 100644 --- a/core/shared/src/main/scala/caseapp/core/parser/StandardArgument.scala +++ b/core/shared/src/main/scala/caseapp/core/parser/StandardArgument.scala @@ -32,7 +32,8 @@ import caseapp.Name Right(None) case firstArg :: rem => - val matchedOpt = (Iterator(arg.name) ++ arg.extraNames.iterator) + val matchedOpt = arg.names + .iterator .map(n => n -> n(firstArg, nameFormatter)) .collectFirst { case (n, Right(valueOpt)) => n -> valueOpt diff --git a/core/shared/src/main/scala/caseapp/core/util/Formatter.scala b/core/shared/src/main/scala/caseapp/core/util/Formatter.scala index 91d5236b..3fd20599 100644 --- a/core/shared/src/main/scala/caseapp/core/util/Formatter.scala +++ b/core/shared/src/main/scala/caseapp/core/util/Formatter.scala @@ -12,8 +12,10 @@ object Formatter { * * Default formatter will format option arguments as `foo-bar`. */ - val DefaultNameFormatter: Formatter[Name] = new Formatter[Name] { - override def format(name: Name): String = + val DefaultNameFormatter: Formatter[Name] = Default + + case object Default extends Formatter[Name] { + def format(name: Name): String = CaseUtil .pascalCaseSplit(name.name.toList) .map(_.toLowerCase) diff --git a/core/shared/src/main/scala/caseapp/package.scala b/core/shared/src/main/scala/caseapp/package.scala index 754edf65..486bdc00 100644 --- a/core/shared/src/main/scala/caseapp/package.scala +++ b/core/shared/src/main/scala/caseapp/package.scala @@ -17,7 +17,8 @@ package object caseapp { type CaseApp[T] = core.app.CaseApp[T] val CaseApp = core.app.CaseApp - type Command[T] = core.app.Command[T] + type Command[T] = core.app.Command[T] + type CommandsEntryPoint = core.app.CommandsEntryPoint type RemainingArgs = core.RemainingArgs val RemainingArgs = core.RemainingArgs