diff --git a/.github/scripts/setup-mkdocs.sh b/.github/scripts/setup-mkdocs.sh new file mode 100755 index 00000000..eda10098 --- /dev/null +++ b/.github/scripts/setup-mkdocs.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +pip install \ + mkdocs==1.6.0 \ + mkdocs-material==9.5.21 \ + mkdocs-git-committers-plugin-2==2.3.0 \ + mkdocs-git-revision-date-localized-plugin==1.2.6 \ + mkdocs-material==9.5.21 \ + pymdown-extensions==10.8.1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a7a1b46..cbedb786 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,21 @@ jobs: apps: scalafmt:3.7.14 - run: scalafmt --check + doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + - uses: coursier/cache-action@v6.4 + - uses: coursier/setup-action@v1.3 + with: + jvm: 17 + - run: | + .github/scripts/setup-mkdocs.sh && \ + ./mill -i docs.mkdocsBuild + publish: if: github.event_name == 'push' runs-on: ubuntu-latest @@ -78,3 +93,19 @@ jobs: PGP_SECRET: ${{ secrets.PUBLISH_SECRET_KEY }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + + update-website: + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + - uses: coursier/cache-action@v6.4 + - uses: coursier/setup-action@v1.3 + with: + jvm: 17 + - run: | + .github/scripts/setup-mkdocs.sh && \ + ./mill -i docs.mkdocsGhDeploy diff --git a/README.md b/README.md index a43f9d1a..3137caa3 100644 --- a/README.md +++ b/README.md @@ -2,439 +2,8 @@ *Type-level & seamless command-line argument parsing for Scala* -[![Build Status](https://travis-ci.org/alexarchambault/case-app.svg)](https://travis-ci.org/alexarchambault/case-app) -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/alexarchambault/case-app?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Maven Central](https://img.shields.io/maven-central/v/com.github.alexarchambault/case-app_2.12.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.alexarchambault/case-app_2.12) -[![Scaladoc](http://javadoc-badge.appspot.com/com.github.alexarchambault/case-app_2.12.svg?label=scaladoc)](http://javadoc-badge.appspot.com/com.github.alexarchambault/case-app_2.12) -[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/alexarchambault/case-app) +[![Build status](https://github.com/alexarchambault/case-app/workflows/CI/badge.svg)](https://github.com/alexarchambault/case-app/actions?query=workflow%3ACI) +[![Maven Central](https://img.shields.io/maven-central/v/com.github.alexarchambault/case-app_3.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.alexarchambault/case-app_3) +[![Scaladoc](http://javadoc-badge.appspot.com/com.github.alexarchambault/case-app_3.svg?label=scaladoc)](http://javadoc-badge.appspot.com/com.github.alexarchambault/case-app_3) -### Imports - -The code snippets below assume that the content of `caseapp` is imported, - -```scala -import caseapp._ -``` - -### Parse a simple set of options - -```scala -case class Options( - user: Option[String], - enableFoo: Boolean = false, - file: List[String] -) - -CaseApp.parse[Options]( - Seq("--user", "alice", "--file", "a", "--file", "b") -) == Right((Options(Some("alice"), false, List("a", "b")), Seq.empty)) -``` - - - - -### Required and optional arguments - -All arguments are required by default. To define an optional argument simply -wrap its type into `Option[T]`. - -Optional arguments can also be defined by providing a default value. -There are two ways to do that: -- providing default value ad hoc in the case class definition -- defining default value for a type with [Default](https://github.com/alexarchambault/case-app/blob/master/core/shared/src/main/scala/caseapp/core/default/Default.scala) -type class - -```scala -case class Options( - user: Option[String], - enableFoo: Boolean = false, - file: List[String] = Nil -) - -CaseApp.parse[Options](Seq()) == Right((Options(None, false, Nil), Seq.empty)) -``` - - - - -### Lists - -Some arguments can be specified several times on the command-line. These -should be typed as lists, e.g. `file` in - -```scala -case class Options( - user: Option[String], - enableFoo: Boolean = false, - file: List[String] -) - -CaseApp.parse[Options]( - Seq("--file", "a", "--file", "b") -) == Right((Options(None, false, List("a", "b")), Seq.empty)) -``` - - - - -If an argument is specified several times, but is not typed as a `List` (or an accumulating type, -see below), the final value of its corresponding field is the last provided in the arguments. - -### Pluggable support for argument expansion before argument parsing - -By default, all arguments are parsed as-is. -To enable expanding arguments before argument parsing, -override - -If supported by the platform, *case-app* can expand each argument of the form: `@` with -the contents of `` where each line constitutes a distinct argument. - -For example, `@args` where `args` is a file containing the following: -``` --- --foo -1 -``` - -is equivalent to: `-- -foo `. - -This behavior is disabled by default. - -To enable argument file expansion, override `CaseApp.expandArgs` as follows: - -```scala -import caseapp.core.parser.PlatformArgsExpander - -override def expandArgs(args: List[String]): List[String] = PlatformArgsExpander.expand(args) -``` - -Alternatively, override this function with a custom argument expansion mechanism. - -### Whole application with argument parsing - -*case-app* can take care of the creation of the `main` method parsing -command-line arguments. - -```scala -import caseapp._ - -case class ExampleOptions( - foo: String, - bar: Int -) - -object Example extends CaseApp[ExampleOptions] { - - def run(options: ExampleOptions, arg: RemainingArgs): Unit = { - // Core of the app - // ... - } - -} -``` - -`Example` in the above example will then have a `main` method, parsing -the arguments it is given to an `ExampleOptions`, then calling the `run` method -if parsing was successful. - -### Automatic help and usage options - -Running the above example with the `--help` (or `-h`) option will print an help message -of the form -``` -Example -Usage: example [options] - --foo - --bar -``` - - - - -Calling it with the `--usage` option will print -``` -Usage: example [options] -``` - -### Customizing items of the help / usage message - -Several parts of the above help message can be customized by annotating -`ExampleOptions` or its fields: - -```scala -@AppName("MyApp") -@AppVersion("0.1.0") -@ProgName("my-app-cli") -case class ExampleOptions( - @HelpMessage("the foo") - @ValueDescription("foo") - foo: String, - @HelpMessage("the bar") - @ValueDescription("bar") - bar: Int -) -``` - - - - -Called with the `--help` or `-h` option, would print -``` -MyApp 0.1.0 -Usage: my-app-cli [options] - --foo : the foo - --bar : the bar -``` - - - - -Note the application name that changed, on the first line. Note also the version -number appended next to it. The program name, after `Usage: `, was changed too. - -Lastly, the options value descriptions (`` and ``) and help messages -(`the foo` and `the bar`), were customized. - -### Extra option names - -Alternative option names can be specified, like -```scala -case class ExampleOptions( - @ExtraName("f") - foo: String, - @ExtraName("b") - bar: Int -) -``` - - - - -`--foo` and `-f`, and `--bar` and `-b` would then be equivalent. - -### Long / short options - -Field names, or extra names as above, longer than one letter are considered -long options, prefixed with `--`. One letter long names are short options, -prefixed with a single `-`. - -```scala -case class ExampleOptions( - a: Int, - foo: String -) -``` - - - - -would accept `--foo bar` and `-a 2` as arguments to set `foo` or `a`. - -### Pascal case conversion - -Field names or extra names as above, written in pascal case, are split -and hyphenized. - -```scala -case class Options( - fooBar: Double -) -``` - - - - -would accept arguments like `--foo-bar 2.2`. - - -### Reusing options - -Sets of options can be shared between applications: - -```scala -case class CommonOptions( - foo: String, - bar: Int -) - -case class First( - baz: Double, - @Recurse - common: CommonOptions -) { - - // ... - -} - -case class Second( - bas: Long, - @Recurse - common: CommonOptions -) { - - // ... - -} -``` - - - - -### Commands - -*case-app* has a support for commands. - -```scala -sealed trait DemoCommand - -case class First( - foo: Int, - bar: String -) extends DemoCommand - -case class Second( - baz: Double -) extends DemoCommand - -object MyApp extends CommandApp[DemoCommand] { - def run(command: DemoCommand, args: RemainingArgs): Unit = {} -} -``` - -`MyApp` can then be called with arguments like -``` -my-app first --foo 2 --bar a -my-app second --baz 2.4 -``` - -- help messages -- customization -- base command -- ... - -### Counters - -*Needs to be updated* - -Some more complex options can be specified multiple times on the command-line and should be -"accumulated". For example, one would want to define a verbose option like -```scala -case class Options( - @ExtraName("v") verbose: Int -) -``` - - - - -Verbosity would then have be specified on the command-line like `--verbose 3`. -But the usual preferred way of increasing verbosity is to repeat the verbosity -option, like in `-v -v -v`. To accept the latter, -tag `verbose` type with `Counter`: -```scala -case class Options( - @ExtraName("v") verbose: Int @@ Counter -) -``` - -`verbose` (and `v`) option will then be viewed as a flag, and the -`verbose` variable will contain -the number of times this flag is specified on the command-line. - -It can optionally be given a default value other than 0. This -value will be increased by the number of times `-v` or `--verbose` -was specified in the arguments. - - -### User-defined option types - -*Needs to be updated* - -Use your own option types by defining implicit `ArgParser`s for them, like in -```scala -import caseapp.core.argparser.{ArgParser, SimpleArgParser} - -trait Custom - -implicit val customArgParser: ArgParser[Custom] = - SimpleArgParser.from[Custom]("custom") { s => - // parse s - // return - // - Left(a caseapp.core.Error instance) in case of error - // - Right(custom) in case of success - ??? - } -``` - -Then use them like -```scala -case class Options( - custom: Custom, - foo: String -) -``` - -### Cats Effect - -A [cats-effect](https://github.com/typelevel/cats-effect) module is available, providing -`IO` versions of the application classes referenced above. They all extend [IOApp](https://typelevel.org/cats-effect/datatypes/ioapp.html) -so [`Timer`](https://typelevel.org/cats-effect/datatypes/timer.html) and [`ContextShift`](https://typelevel.org/cats-effect/datatypes/contextshift.html) -are conveniently available. - - -```scala -// additional imports -import caseapp.cats._ -import cats.effect._ - -object IOCaseExample extends IOCaseApp[ExampleOptions] { - def run(options: ExampleOptions, arg: RemainingArgs): IO[ExitCode] = IO { - // Core of the app - // ... - ExitCode.Success - } -} - -object IOCommandExample extends CommandApp[DemoCommand] { - def run(command: DemoCommand, args: RemainingArgs): IO[ExitCode] = IO { - // ... - ExitCode.Success - } -} -``` - -## Usage - -Add to your `build.sbt` -```scala -resolvers += Resolver.sonatypeRepo("releases") -libraryDependencies += "com.github.alexarchambault" %% "case-app" % "2.0.1" -// cats-effect module -libraryDependencies += "com.github.alexarchambault" %% "case-app-cats" % "2.0.1" -``` -The latest version is [![Maven Central](https://img.shields.io/maven-central/v/com.github.alexarchambault/case-app_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.alexarchambault/case-app_2.13). - -Note that case-app depends on shapeless 2.3. Use the `1.0.0` version if you depend on shapeless 2.2. - -It is built against scala 2.12, and 2.13, and supports Scala.js too. - -## Contributors - -See the [full list of contributors](https://github.com/alexarchambault/case-app/graphs/contributors) on GitHub. - -## See also - -Eugene Yokota, the current maintainer of scopt, and others, compiled -an (eeextremeeeely long) list of command-line argument parsing -libraries for Scala, in [this StackOverflow question](http://stackoverflow.com/questions/2315912/scala-best-way-to-parse-command-line-parameters-cli). - -Unlike [scopt](https://github.com/scopt/scopt), case-app is less monadic / abstract data types based, and more -straight-to-the-point and descriptive / algebric data types oriented. - -## Notice - -Copyright (c) 2014-2017 Alexandre Archambault and contributors. -See LICENSE file for more details. - -Released under Apache 2.0 license. +See the [website](https://alexarchambault.github.io/case-app/) for more details. diff --git a/build.sc b/build.sc index fea35ab3..5a3ee450 100644 --- a/build.sc +++ b/build.sc @@ -6,6 +6,8 @@ import mill.scalajslib._ import mill.scalalib._ import mill.scalanativelib._ +import java.io.File + import scala.concurrent.duration.DurationInt object Versions { @@ -23,6 +25,7 @@ object Deps { def catsEffect2 = ivy"org.typelevel::cats-effect::2.5.5" def dataClass = ivy"io.github.alexarchambault::data-class:0.2.6" def macroParadise = ivy"org.scalamacros:::paradise:2.1.1" + def mdoc = ivy"org.scalameta::mdoc:2.5.2" def osLib = ivy"com.lihaoyi::os-lib::0.10.2" def pprint = ivy"com.lihaoyi::pprint::0.9.0" def scalaCompiler(sv: String) = ivy"org.scala-lang:scala-compiler:$sv" @@ -437,3 +440,85 @@ object ci extends Module { } } + +object docs extends ScalaModule { + private def sv = Versions.scala213 + def scalaVersion = sv + def ivyDeps = Agg( + Deps.mdoc + ) + def mainClass = Some("mdoc.Main") + + def mdocInput = T.sources { + Seq(PathRef(millModuleBasePath.value / "pages")) + } + def mkdocsConfigFile = T.sources { + Seq(PathRef(millModuleBasePath.value / "mkdocs.yml")) + } + + def mdocOutput = T { + val dir = millModuleBasePath.value / "docs" + os.makeDir.all(dir) + PathRef(dir) + } + def mkdocsOutput = T { + PathRef(millModuleBasePath.value / "site") + } + + def mdocArgs = T.task { + val mdocInput0 = mdocInput().map(_.path) + assert(mdocInput0.length == 1) + define.Args( + "--in", + mdocInput0.head, + "--out", + mdocOutput().path, + "--site.VERSION", + core.jvm(sv).publishVersion(), + "--classpath", + cats.jvm(sv).runClasspath().map(_.path).mkString(File.pathSeparator) + ) + } + def mdocWatchArgs = T.task { + new define.Args( + mdocArgs().value :+ "--watch" + ) + } + + def mdoc() = T.command[Unit] { + run(mdocArgs)() + } + def mdocWatch() = T.command[Unit] { + run(mdocWatchArgs)() + } + + def mkdocsConfigArgs = T { + val mkdocsConfigFile0 = mkdocsConfigFile().map(_.path) + assert(mkdocsConfigFile0.length == 1) + Seq("--config-file", mkdocsConfigFile0.head.toString) + } + def mkdocsSiteDirArgs = T { + Seq("--site-dir", mkdocsOutput().path.toString) + } + def mkdocsServe() = T.command[Unit] { + mdoc()() + os.proc("mkdocs", "serve", mkdocsConfigArgs()) + .call(cwd = millModuleBasePath.value, stdin = os.Inherit, stdout = os.Inherit) + + () + } + def mkdocsBuild() = T.command[Unit] { + mdoc()() + os.proc("mkdocs", "build", mkdocsConfigArgs(), mkdocsSiteDirArgs()) + .call(cwd = millModuleBasePath.value, stdin = os.Inherit, stdout = os.Inherit) + + () + } + def mkdocsGhDeploy() = T.command[Unit] { + mdoc()() + os.proc("mkdocs", "gh-deploy", mkdocsConfigArgs(), mkdocsSiteDirArgs()) + .call(cwd = millModuleBasePath.value, stdin = os.Inherit, stdout = os.Inherit) + + () + } +} diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..44b04b4e --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +docs/ +site/ +.cache/ diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 00000000..21561e50 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,84 @@ +site_name: case-app + +nav: + - Home: 'index.md' + - 'setup.md' + - 'define.md' + - 'types.md' + - 'parse.md' + - 'help.md' + - 'commands.md' + - 'completion.md' + - 'misc.md' + - 'advanced.md' + - 'develop.md' + +repo_url: https://github.com/alexarchambault/case-app +edit_uri: edit/main/docs/ + +theme: + name: material + palette: + # scheme: slate + + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + + features: + - content.action.edit + - navigation.instant + - navigation.instant.progress + - navigation.tabs + - navigation.path + - navigation.top + - search.suggest + - search.highlight + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - tables + - def_list + - toc: + permalink: true + +plugins: + - git-revision-date-localized: + enable_creation_date: true + - git-committers: + repository: alexarchambault/case-app + branch: main + enabled: !ENV [CI, false] + - search + +extra: + version: + provider: mike + +validation: + links: + not_found: + anchors: + unrecognized_links: diff --git a/docs/pages/advanced.md b/docs/pages/advanced.md new file mode 100644 index 00000000..0a45e577 --- /dev/null +++ b/docs/pages/advanced.md @@ -0,0 +1,173 @@ +# Advanced topics + +## Recovering argument positions + +```scala mdoc:reset:invisible +import caseapp._ +``` + +One can recover the exact positions of each options and remaining arguments. + +For options, one should use the `Indexed` class, like +```scala mdoc +import caseapp.core.Indexed + +case class Options( + foo: Indexed[String] = Indexed("") +) + +val (options, _) = CaseApp.parse[Options](Seq("a", "--foo", "thing")).toOption.get +``` + +For arguments that are not options or option values, indices are retained in the +`RemainingArgs` instance: +```scala mdoc:silent +val (_, args) = CaseApp.detailedParse[Options](Seq("a", "--foo", "2", "b")).toOption.get +``` +```scala mdoc +args.indexed +``` + +## Partial parsing + +```scala mdoc:reset:invisible +import caseapp._ +``` + +One can stop parsing arguments at the first argument that is not an option, +or that is an unknown option. This can be useful when "pre-processing" arguments +passed to another application later on: case-app parses as much arguments as possible, +then stops, so that the other application can parse the remaining arguments later on. + +```scala mdoc:silent +case class Options( + foo: String = "" +) + +object Options { + implicit lazy val parser: Parser[Options] = { + val parser0: Parser[Options] = Parser.derive + parser0.stopAtFirstUnrecognized + } + implicit lazy val help: Help[Options] = Help.derive +} +``` + +```scala mdoc +val (options, args) = CaseApp.parse[Options](Seq("--foo", "a", "--other", "thing")).toOption.get +``` + + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +```scala mdoc:invisible +// keep in sync with Options above +case class Options( + foo: String = "" +) +``` + +Alternatively, when extending `CaseApp`, one can do: +```scala mdoc:silent +object MyApp extends CaseApp[Options] { + override def stopAtFirstUnrecognized = true + def run(options: Options, args: RemainingArgs): Unit = { + ??? + } +} +``` + +```scala mdoc:invisible +val (_, args0) = MyApp.parser.parse(Seq("--foo", "a", "--other", "thing")).toOption.get +assert(args0 == Seq("--other", "thing")) +``` + +## Ignore unrecognized options + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +One can ignore non-recognized options, like +```scala mdoc:silent +case class Options( + foo: String = "" +) + +object Options { + implicit lazy val parser: Parser[Options] = { + val parser0: Parser[Options] = Parser.derive + parser0.ignoreUnrecognized + } + implicit lazy val help: Help[Options] = Help.derive +} +``` + +```scala mdoc +val (options, args) = CaseApp.parse[Options](Seq("--other", "thing", "--foo", "a")).toOption.get +``` + +Unrecognized options end up in the "remaining arguments", along with non-option or non-option +values arguments. + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +```scala mdoc:invisible +// keep in sync with Options above +case class Options( + foo: String = "" +) +``` + +Alternatively, when extending `CaseApp`, one can do: +```scala mdoc:silent +object MyApp extends CaseApp[Options] { + override def ignoreUnrecognized = true + def run(options: Options, args: RemainingArgs): Unit = { + ??? + } +} +``` + +```scala mdoc:invisible +val (_, args0) = MyApp.parser.parse(Seq("--other", "thing", "--foo", "a")).toOption.get +assert(args0 == Seq("--other", "thing")) +``` + +## Check for duplicate options in tests + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +When using the `@Recurse` or `@Name` annotations, some options might be given the same +name. In practice, at runtime, one will shadow the other. + +To ensure your options don't contain duplicates, you can call `ensureNoDuplicates` +on the `Help` type class instance of your options, or on the `CaseApp` instance +if you defined one: +```scala mdoc:silent +case class Options( + foo: String = "", + @Name("foo") + count: Int = 0 +) + +object MyApp extends CaseApp[Options] { + def run(options: Options, args: RemainingArgs): Unit = { + ??? + } +} +``` + +```scala mdoc:crash +Help[Options].ensureNoDuplicates() +``` + +```scala mdoc:crash +MyApp.ensureNoDuplicates() +``` diff --git a/docs/pages/commands.md b/docs/pages/commands.md new file mode 100644 index 00000000..ec38336b --- /dev/null +++ b/docs/pages/commands.md @@ -0,0 +1,254 @@ +# Commands + +The use of commands relies on the same API as [`CaseApp`](parse.md#application-definition). +While it is possible to use the command argument parser +[in a standalone fashion](#standalone-use-of-the-command-argument-parser), +sections below assume you're parsing options by +extending `CaseApp` (or its `Command` sub-class). + +## Defining commands + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +Individual commands are defined as instances of `Command`, which is +itself a sub-class of `CaseApp`. `Command` adds a few methods to `CaseApp`, +that can be overridden, most notably `name` and `names`. + +```scala mdoc:silent +case class FirstOptions( + foo: String = "" +) + +object First extends Command[FirstOptions] { + override def names = List( + List("first"), + List("frst"), + List("command-one"), + List("command", "one") + ) + def run(options: FirstOptions, args: RemainingArgs): Unit = { + ??? + } +} + +case class SecondOptions( + foo: String = "" +) + +object Second extends Command[SecondOptions] { + override def name = "command-two" + def run(options: SecondOptions, args: RemainingArgs): Unit = { + ??? + } +} +``` + +Individual commands are gathered in an object extending `CommandsEntryPoint`: +```scala mdoc:silent +object MyApp extends CommandsEntryPoint { + def progName = "my-app" + def commands = Seq( + First, + Second + ) +} +``` + +## Customizing commands help + +## Enabling support for completion + +See [completion support](completion.md) + +## Advanced + +### Hidden commands + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +Overriding the `def hidden: Boolean` method of `Command` allows to hide a +command from the help message: +```scala mdoc:silent +case class FirstOptions() + +object First extends Command[FirstOptions] { + def run(options: FirstOptions, args: RemainingArgs) = { + ??? + } +} + +case class SecondOptions() + +object Second extends Command[SecondOptions] { + + // hide this command from the command listing in the help message + override def hidden = true + + def run(options: SecondOptions, args: RemainingArgs) = { + ??? + } +} + +object MyApp extends CommandsEntryPoint { + def progName = "my-app" + def commands = Seq( + First, + Second + ) +} +``` + +One gets as a help message: +```scala mdoc:passthrough +println("```text") +println { + // FIXME Find a way to keep colors in the output + MyApp.help.help( + // reset colors + MyApp.helpFormat + .withProgName(caseapp.core.util.fansi.Attrs.Empty) + .withCommandName(caseapp.core.util.fansi.Attrs.Empty) + .withOption(caseapp.core.util.fansi.Attrs.Empty) + .withHidden(caseapp.core.util.fansi.Attrs.Empty) + ) +} +println("```") +``` + +### Command groups + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +Override the `def group: String` method of `Command` to gather +similar commands together in the help message listing commands: +```scala mdoc:silent + +case class FirstOptions() + +object First extends Command[FirstOptions] { + override def group = "Main" + def run(options: FirstOptions, args: RemainingArgs) = { + ??? + } +} + +case class SecondOptions() + +object Second extends Command[SecondOptions] { + override def group = "Other" + def run(options: SecondOptions, args: RemainingArgs) = { + ??? + } +} + +object MyApp extends CommandsEntryPoint { + override def defaultCommand = None + def progName = "my-app" + def commands = Seq( + First, + Second + ) +} +``` + +One gets as a help message: +```scala mdoc:passthrough +println("```text") +println { + // FIXME Find a way to keep colors in the output + MyApp.help.help( + // reset colors + MyApp.helpFormat + .withProgName(caseapp.core.util.fansi.Attrs.Empty) + .withCommandName(caseapp.core.util.fansi.Attrs.Empty) + .withOption(caseapp.core.util.fansi.Attrs.Empty) + .withHidden(caseapp.core.util.fansi.Attrs.Empty) + ) +} +println("```") +``` + +To sort groups, set the `sortCommandGroups` or `sortedCommandGroups` command +fields of `Command#helpFormat`, like +```scala mdoc:silent +object MyOtherApp extends CommandsEntryPoint { + override def defaultCommand = None + def progName = "my-other-app" + override def helpFormat = super.helpFormat.withSortedCommandGroups( + Some(Seq("Other", "Main")) + ) + def commands = Seq( + First, + Second + ) +} +``` + +One then gets as a help message: +```scala mdoc:passthrough +println("```text") +println { + // FIXME Find a way to keep colors in the output + MyOtherApp.help.help( + // reset colors + MyOtherApp.helpFormat + .withProgName(caseapp.core.util.fansi.Attrs.Empty) + .withCommandName(caseapp.core.util.fansi.Attrs.Empty) + .withOption(caseapp.core.util.fansi.Attrs.Empty) + .withHidden(caseapp.core.util.fansi.Attrs.Empty) + ) +} +println("```") +``` + +### Standalone use of the command argument parser + +```scala mdoc:reset:invisible +import caseapp._ +``` + +Use one of the overrides of `RuntimeCommandParser.parse` to parse a list of arguments +to a command, like +```scala mdoc:silent +import caseapp.core.commandparser.RuntimeCommandParser + +case class MyCommand(name: String) + +val commandMap = Map( + List("first") -> MyCommand("First one"), + List("second") -> MyCommand("Second one"), + List("the", "first") -> MyCommand("First one"), + List("the", "second") -> MyCommand("Second one") +) +``` + +```scala mdoc +// no default command +RuntimeCommandParser.parse[MyCommand]( + commandMap, + List("the", "first", "a", "--thing", "--foo", "b") +) + +// override accepting a default command +RuntimeCommandParser.parse[MyCommand]( + MyCommand("Default one"), + commandMap, + List("the", "thing", "a", "--thing", "--foo", "b") +) + +RuntimeCommandParser.parse[MyCommand]( + MyCommand("Default one"), + commandMap, + List("first", "a", "--thing", "--foo", "b") +) +``` + +Note that there are also overrides accepting a `Seq[Command[_]]` rather +than a map like `commandMap` above, that build the command map out of the `Command[_]` +sequence. diff --git a/docs/pages/completion.md b/docs/pages/completion.md new file mode 100644 index 00000000..3e5c4314 --- /dev/null +++ b/docs/pages/completion.md @@ -0,0 +1,158 @@ +# Completion + +## Enable support for completion + +Support for completion relies on [commands](commands.md). + +### In commands + +In an application made of [commands](commands.md), enable completions by overriding +the `def enableCompletionsCommand: Boolean` and `def enableCompleteCommand: Boolean`. + +This adds two (hidden) commands to your application: +- `completions` (also aliased to `completion`): command that allows to help installing + completions +- `complete`: command run when users ask for completions in their shell + +Overriding `def completionsWorkingDirectory: Option[String]` and returning a non-empty +value from it enables two more commands: +- `completions install` (also aliased to `completion install`): command to install completions + for the current shell +- `completions uninstall` (also aliased to `completion uninstall`): command to uninstall + completions for the current shell + +### For simple applications + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +If you'd like to enable it in a simple application, make it extend +`Command` rather than `CaseApp`, and define a `CommandsEntryPoint` +with no commands, and your application as default command: +```scala mdoc:silent +case class Options( + foo: String = "" +) + +object MyActualApp extends Command[Options] { + def run(options: Options, args: RemainingArgs): Unit = { + ??? + } +} + +object MyApp extends CommandsEntryPoint { + def progName = "my-app" + def commands = Seq() + override def defaultCommand = Some(MyActualApp) + override def enableCompleteCommand = true + override def enableCompletionsCommand = true +} +``` + +## Install completions + +### Via `completions install` + +Assuming `progname` runs the main class added by `CommandEntryPoint` to the object extended +by it, you can install completions with +```text +$ progname completions install +``` + +```scala mdoc:passthrough +println("```text") +for (line <- MyApp.completionsInstalledMessage("~/.zshrc", updated = false)) + println(line) +println("```") +``` + +## Get completions + +The file installed by `completions install` above runs your application +to get completions. It runs it via the `complete` command, like +```text +$ my-app complete zsh-v1 2 my-app - +``` + +```scala mdoc:passthrough +println("```text") +MyApp.main(Array("complete", "zsh-v1", "2", "my-app", "-")) +println("```") +``` + +Usage: +```scala mdoc:passthrough +println("```text") +MyApp.main(Array("complete", "--help")) +println("```") +``` + +## Provide completions for individual option values + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +```scala mdoc:silent +import caseapp.core.complete.CompletionItem + +case class Options( + foo: String = "" +) + +object MyActualApp extends Command[Options] { + def run(options: Options, args: RemainingArgs): Unit = { + ??? + } + + override def completer = + super.completer.completeOptionValue { + val hardCodedValues = List( + "aaa", + "aab", + "aac", + "abb" + ) + (arg, prefix, state, args) => + // provide hard-coded values as completions for --foo values + if (arg.names.map(_.name).contains("foo")) { + val items = hardCodedValues.filter(_.startsWith(prefix)).map { value => + CompletionItem(value) + } + Some(items) + } + else + None + } +} + +object MyApp extends CommandsEntryPoint { + def progName = "my-app" + def commands = Seq() + override def defaultCommand = Some(MyActualApp) + override def enableCompleteCommand = true + override def enableCompletionsCommand = true +} +``` + +One can then get specific completions, like +```text +$ my-app complete zsh-v1 3 my-app --foo a +``` + +```scala mdoc:passthrough +println("```text") +MyApp.main(Array("complete", "zsh-v1", "3", "my-app", "--foo", "a")) +println("```") +``` + +```text +$ my-app complete zsh-v1 3 my-app --foo aa +``` + +```scala mdoc:passthrough +println("```text") +MyApp.main(Array("complete", "zsh-v1", "3", "my-app", "--foo", "aa")) +println("```") +``` diff --git a/docs/pages/define.md b/docs/pages/define.md new file mode 100644 index 00000000..ba3d7e25 --- /dev/null +++ b/docs/pages/define.md @@ -0,0 +1,180 @@ +# Defining options + + +## Case classes + +Options are defined in case classes, like +```scala mdoc:silent +case class Options( + foo: Int, // --foo 2, --foo=2 + enableThing: Boolean // --enableThing, --enableThing=false +) +``` + +## Caching derived type classes + +```scala mdoc:invisible:reset +import caseapp._ +``` + +```scala mdoc:silent +// should be the same as below, but for the Scala 3 section +case class Options( + foo: Int +) + +// Scala 2 +object Options { + implicit lazy val parser: Parser[Options] = Parser.derive + implicit lazy val help: Help[Options] = Help.derive +} +``` + +When defining a case class for options, it is recommended to derive case-app +type classes for it in its companion object, like +```scala +case class Options( + foo: Int +) + +// Scala 2 +object Options { + implicit lazy val parser: Parser[Options] = Parser.derive + implicit lazy val help: Help[Options] = Help.derive +} + +// Scala 3 +@derives[Parser, Help] +object Options +``` + +These derivations are omitted in all other examples through out the case-app +documentation for brevity, but we highly recommend deriving them there, in order +to make incremental compilation faster when the file defining the options isn't modified. +This is especially the case in Scala 2, where those type class derivation can be somewhat +slow. + +## Mandatory options + +```scala mdoc:invisible:reset +import caseapp._ +``` + +Options that don't have default values are assumed to be mandatory. To +make an option non-mandatory, ensure it has a default value: +```scala mdoc:silent +case class Options( + foo: Int, // --foo is mandatory + verbosity: Int = 0 // --verbosity is optional +) +``` + +## Shared options + +```scala mdoc:invisible:reset +import caseapp._ +``` + +Options can be defined across several case classes thanks to the `@Recurse` +annotation, like +```scala mdoc:silent +case class SharedOptions( + foo: Int = 0, + enableThing: Boolean = false +) + +case class Options( + @Recurse + shared: SharedOptions = SharedOptions(), + path: String +) +``` + +Shared option classes can themselves have fields marked with `@Recurse`, whose +types can also have fields marked with it, etc. + +## Extra names + +```scala mdoc:invisible:reset +import caseapp._ +``` + +Various annotations allow to customize options, like `@Name` to offer +several ways to specify an option: +```scala mdoc:silent +case class Options( + @Name("f") + foo: Int = 0, // -f 2 and --foo 2 both work + @Name("t") + @Name("thing") + enableThing: Boolean = false // -t, --thing, --enable-thing all work +) +``` + +## Field name to option name conversion + +```scala mdoc:invisible:reset +import caseapp._ +``` + +Case class field names are assumed to follow the +[camel case](https://en.wikipedia.org/wiki/Camel_case) (`camelCase`), and are converted to +[kebab case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case) (`kebab-case`) to +get the corresponding option name. + +Names consisting in a single letter are +prefixed with a single hyphen (like `-t`) while other are prefixed with two hyphens +(like `--foo-thing`). +```scala mdoc:silent +case class Options( + @Name("f") + foo: Int = 0, // defines options -f and --foo + @Name("t") + @Name("thing") + enableThing: Boolean = false // defines options -t, --thing, and --enable-thing +) +``` + +```scala mdoc:invisible:reset +import caseapp._ +``` + +To enforce a single hyphen for longer options, put the hyphen in the field name, +and use kebab case directly, like +```scala mdoc:silent +case class Options( + @Name("-foo") + foo: Int = 0, // accepts both --foo and -foo + `-thing`: Boolean = false // accepts only -thing +) +``` + +## Passing values to options + +For option types expecting a value (most of them), that value can be passed in a separated +argument, like +```text +--foo value +``` +or with an `=`, like +```text +--foo=value +``` + +For option types not expecting a value by default, like `Boolean`, an optional value +can be passed with an `=`, like +```text +--foo=false +``` + +```scala mdoc:invisible:reset +import caseapp._ +``` + +This is especially useful for boolean options that are true by default, like +```scala mdoc:silent +case class Options( + foo: Boolean = true // can only be disabled with --foo=false +) +``` + diff --git a/docs/pages/develop.md b/docs/pages/develop.md new file mode 100644 index 00000000..d0089781 --- /dev/null +++ b/docs/pages/develop.md @@ -0,0 +1,25 @@ +# Contributing + +## Building the website locally + +### Watch mode + +In a first terminal, run +```text +$ ./mill -i -w docs.mdocWatch +``` + +Leave it running, and run in a second terminal +```text +$ ./mill -i -w docs.mkdocsServe +``` + +Then open the URL printed in the console in your browser (it should be +[`http://127.0.0.1:8000`](http://127.0.0.1:8000)) + +### Once + +Build the website in the `docs/site` directory with +```text +$ ./mill -i docs.mkdocsBuild +``` diff --git a/docs/pages/help.md b/docs/pages/help.md new file mode 100644 index 00000000..56a2d76a --- /dev/null +++ b/docs/pages/help.md @@ -0,0 +1,170 @@ +# Customizing help + +## Annotations + +```scala mdoc:reset:invisible +import caseapp._ +``` + +A number of annotations can be used, either on fields or on classes defining options, +to customize the help message generated by case-app. + +```scala mdoc:silent +@AppName("My App") +@ProgName("my-app") +@AppVersion("0.1.0") +@ArgsName("things") +case class Options( + @HelpMessage("How many foos") + @Name("f") + @ValueDescription("Foo count") + foo: Int = 0 +) +``` + +This makes the help message look like +```scala mdoc:passthrough +// FIXME Find a way to keep colors in the output +import caseapp.core.help.HelpFormat +val help: Help[Options] = Help.derive +println("```text") +println(help.help(HelpFormat.default(ansiColors = false), showHidden = false)) +println("```") +``` + +Without any of the annotations, the help message looks like +```scala mdoc:passthrough +case class Options0( + @Name("f") + foo: Int = 0 +) +// FIXME Find a way to keep colors in the output +import caseapp.core.help.HelpFormat +val help0: Help[Options0] = Help.derive +println("```text") +println(help0.help(HelpFormat.default(ansiColors = false), showHidden = false)) +println("```") +``` + +## Hidden options + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +Annotate an option with `@Hidden` to hide it from the default help message: +```scala mdoc:silent +case class Options( + @Hidden + foo: Int = 0, + other: String = "" +) +``` + +This makes the help message look like +```scala mdoc:passthrough +// FIXME Find a way to keep colors in the output +import caseapp.core.help.HelpFormat + +object MyHelperApp extends CaseApp[Options] { + override def name = "my-app" + def run(options: Options, args: RemainingArgs): Unit = { + ??? + } +} +println("```text") +println(MyHelperApp.finalHelp.withProgName("my-app").help(HelpFormat.default(ansiColors = false), showHidden = false)) +println("```") +``` + +When using hidden options alongside with the `CaseApp` class, you can offer users +to get an help message that includes the hidden options with `--full-help`, like +```scala mdoc:silent +object MyApp extends CaseApp[Options] { + override def hasFullHelp = true + def run(options: Options, args: RemainingArgs): Unit = { + ??? + } +} +``` + +We get +```text +$ my-app --full-help +``` + +```scala mdoc:passthrough +println("```text") +println { + // FIXME Find a way to keep colors in the output + MyApp.finalHelp.withProgName("my-app").help( + // reset colors + MyApp.helpFormat + .withProgName(caseapp.core.util.fansi.Attrs.Empty) + .withCommandName(caseapp.core.util.fansi.Attrs.Empty) + .withOption(caseapp.core.util.fansi.Attrs.Empty) + .withHidden(caseapp.core.util.fansi.Attrs.Empty), + showHidden = true + ) +} +println("```") +``` + +## Option groups + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +```scala mdoc:silent +case class Options( + @Group("First") + foo: Int = 0, + @Group("Second") + other: String = "" +) +``` + +This makes the help message look like +```scala mdoc:passthrough +// FIXME Find a way to keep colors in the output +import caseapp.core.help.HelpFormat + +object MyHelperApp extends CaseApp[Options] { + override def name = "my-app" + def run(options: Options, args: RemainingArgs): Unit = { + ??? + } +} +println("```text") +println(MyHelperApp.finalHelp.withProgName("my-app").help(HelpFormat.default(ansiColors = false), showHidden = false)) +println("```") +``` + +When using the `CaseApp` or `Command` classes, you can sort groups with +```scala mdoc:silent +object MyApp extends CaseApp[Options] { + override def helpFormat = super.helpFormat.withSortedGroups( + Some(Seq("Help", "First", "Second")) + ) + def run(options: Options, args: RemainingArgs): Unit = { + ??? + } +} +``` + +One then gets +```scala mdoc:passthrough +// FIXME Find a way to keep colors in the output +println("```text") +println(MyApp.finalHelp.withProgName("my-app").help( + // reset colors + MyApp.helpFormat + .withProgName(caseapp.core.util.fansi.Attrs.Empty) + .withCommandName(caseapp.core.util.fansi.Attrs.Empty) + .withOption(caseapp.core.util.fansi.Attrs.Empty) + .withHidden(caseapp.core.util.fansi.Attrs.Empty) + ) +) +println("```") +``` diff --git a/docs/pages/index.md b/docs/pages/index.md new file mode 100644 index 00000000..894814ed --- /dev/null +++ b/docs/pages/index.md @@ -0,0 +1,15 @@ +# case-app + +*case-app* is command-line argument parser for Scala that relies on case classes + +It offers to +define options in simple case classes, supports sub-commands, offers +support for completion, … + +It supports Scala on the JVM, +[Scala.js](https://github.com/scala-js/scala-js), and +[Scala Native](https://github.com/scala-native/scala-native). + +It's used by [coursier](https://github.com/coursier/coursier), +[Scala CLI](https://github.com/VirtusLab/scala-cli), +[Almond](https://github.com/almond-sh/almond), … diff --git a/docs/pages/misc.md b/docs/pages/misc.md new file mode 100644 index 00000000..d8481c01 --- /dev/null +++ b/docs/pages/misc.md @@ -0,0 +1,38 @@ +# Miscellaneous + +## cats-effect + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +case-app has a module helping using it in cats-effect applications. + +Add a dependency to it like + +```scala mdoc:passthrough +println("```scala") +println("//> using dep com.github.alexarchambault::case-app-cats:@VERSION@") +println("```") +``` +(for other build tools, see [setup](setup.md) and change `case-app` to `case-app-cats`) + +Then use it like +```scala mdoc:silent +import caseapp.catseffect._ +import cats.data.NonEmptyList +import cats.effect._ + +case class ExampleOptions( + foo: String = "", + thing: NonEmptyList[String] +) + +object IOCaseExample extends IOCaseApp[ExampleOptions] { + def run(options: ExampleOptions, arg: RemainingArgs): IO[ExitCode] = IO { + // Core of the app + // ... + ExitCode.Success + } +} +``` diff --git a/docs/pages/parse.md b/docs/pages/parse.md new file mode 100644 index 00000000..2283861d --- /dev/null +++ b/docs/pages/parse.md @@ -0,0 +1,127 @@ +# Parsing options + +case-app offers several ways to parse input arguments: + +- method calls, +- app definitions. + +## Method calls + +The `CaseApp` object contains a number of methods that can be used to parse arguments. + +### `parse` + +```scala mdoc:invisible:reset +import caseapp._ +``` + +`CaseApp.parse` accepts a sequence of strings, and returns either an error or +parsed options and remaining arguments: +```scala mdoc:silent +case class Options( + foo: Int = 0 +) +val args = Seq("a", "--foo", "2", "b") +val (options, remaining) = CaseApp.parse[Options](args).toOption.get +assert(options == Options(2)) +assert(remaining == Seq("a", "b")) + +val either = CaseApp.parse[Options](Seq("--foo", "a")) +assert(either.left.toOption.nonEmpty) +``` + +### `parseWithHelp` + +`CaseApp.parseWithHelp` does the same as `CaseApp.parse`, but also accepts +`--help` / `-h` / `--usage` options. + +```scala mdoc:silent +CaseApp.parseWithHelp[Options](args) match { + case Left(error) => // invalid options… + case Right((Left(error), helpAsked, usageAsked, remaining)) => + // missing mandatory options, but --help or --usage could be parsed + case Right((Right(options), helpAsked, usageAsked, remaining)) => + // All is well: + // Options were parsed, resulting in options + // helpAsked and / or usageAsked are true if either has been requested + // remaining contains non-option arguments +} +``` + +### `detailedParse`, `detailedParseWithHelp` + +`CaseApp.detailedParse` and `CaseApp.detailedParseWithHelp` behave the same way +as `CaseApp.parse` and `CaseApp.parseWithHelp`, but return their remaining arguments +as a `RemainingArgs` instance, rather than a `Seq[String]`. See [below](#double-hyphen) +for what `RemainingArgs` brings. + +### `process` + +`CaseApp.process` is the most straightforward method to parse arguments. Note that +it exits the current application if parsing arguments fails or if users request +help, with `--help` for example. + +```scala mdoc:reset:invisible +val args = Array("a") +``` + +It aims at being used from Scala CLI `.sc` files ("Scala scripts"), where one +would rather have case-app handle all errors cases, like +```scala mdoc:silent +//> using dep com.github.alexarchambault::case-app::@VERSION@ +import caseapp._ + +case class Options( + foo: Int = 0, + path: Option[String] = None +) + +val (options, remaining) = CaseApp.process[Options](args.toSeq) + +// … +``` + +## Application definition + +case-app allows one to alter one's main class definitions, so that one +defines a method accepting parsed options, rather than raw arguments: + +```scala mdoc:reset-object:invisible +import caseapp._ +``` + +```scala mdoc:silent +case class Options( + foo: Int = 0 +) + +object MyApp extends CaseApp[Options] { + def run(options: Options, remaining: RemainingArgs): Unit = { + ??? + } +} +``` + +In that example, case-app defines the `MainApp#main` method, so that +`MyApp` can be used as a "main class". + +## Parsing + +### Double-hyphen + +```scala mdoc:invisible:reset +import caseapp._ +``` + +case-app assumes any argument after `--` is not an option. It stops looking +for options after `--`. Arguments before and after `--` can be differentiated +in the `RemainingArgs` class: +```scala mdoc:silent +case class Options() +val (_, args) = CaseApp.detailedParse[Options](Seq("first", "--", "foo")).toOption.get +``` +```scala mdoc +args.remaining +args.unparsed +args.all +``` diff --git a/docs/pages/setup.md b/docs/pages/setup.md new file mode 100644 index 00000000..be6019ed --- /dev/null +++ b/docs/pages/setup.md @@ -0,0 +1,95 @@ +# Setup + +Depend on case-app via `com.github.alexarchambault::case-app:@VERSION@`. +The latest version is [![Maven Central](https://img.shields.io/maven-central/v/com.github.alexarchambault/case-app_3.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.alexarchambault/case-app_3). + +## JVM + +```scala mdoc:invisible +val isSnapshot = "@VERSION@".endsWith("SNAPSHOT") +``` + +From [Mill](https://github.com/com-lihaoyi/Mill): +```scala mdoc:passthrough +def millMaybeAddSonatypeSnapshots() = + if (isSnapshot) + println( + """def repositoriesTask = T { + | super.repositoriesTask() ++ Seq( + | coursier.Repositories.sonatype("snapshots") + | ) + |}""".stripMargin + ) +println("```scala") +millMaybeAddSonatypeSnapshots() +println( + """def ivyDeps = Agg( + | ivy"com.github.alexarchambault::case-app:@VERSION@" + |)""".stripMargin +) +println("```") +``` + +From [Scala CLI](https://github.com/VirtusLab/scala-cli): +```scala mdoc:passthrough +def scalaCliMaybeAddSonatypeSnapshots() = + if (isSnapshot) + println("//> using repository sonatype:snapshots") +println("```scala") +scalaCliMaybeAddSonatypeSnapshots() +println("//> using dep com.github.alexarchambault::case-app:@VERSION@") +println("```") +``` + +From [sbt](https://github.com/sbt/sbt): +```scala mdoc:passthrough +def sbtMaybeAddSonatypeSnapshots() = + if (isSnapshot) + println("""resolvers ++= Resolver.sonatypeOssRepos("snapshots")""") +println("```scala") +sbtMaybeAddSonatypeSnapshots() +println("""libraryDependencies += "com.github.alexarchambault" %% "case-app" % "@VERSION@"""") +println("```") +``` + +## Scala.js and Scala Native + +Scala.js and Scala Native dependencies need to be marked as platform-specific, usually +[with an extra `:` or `%`](https://youforgotapercentagesignoracolon.com). + +From [Mill](https://github.com/com-lihaoyi/Mill): +```scala mdoc:passthrough +println("```scala") +millMaybeAddSonatypeSnapshots() +println( + """def ivyDeps = Agg( + | ivy"com.github.alexarchambault::case-app::@VERSION@" + |)""".stripMargin +) +println("```") +``` + +From [Scala CLI](https://github.com/VirtusLab/scala-cli): +```scala mdoc:passthrough +println("```scala") +scalaCliMaybeAddSonatypeSnapshots() +println("//> using dep com.github.alexarchambault::case-app::@VERSION@") +println("```") +``` + +From [sbt](https://github.com/sbt/sbt): +```scala mdoc:passthrough +println("```scala") +sbtMaybeAddSonatypeSnapshots() +println("""libraryDependencies += "com.github.alexarchambault" %%% "case-app" % "@VERSION@"""") +println("```") +``` + +## Imports + +Most case-app classes that are of relevance for end-users have aliases in the +`caseapp` package object. Importing its content is usually fine to use most +case-app features: +```scala mdoc:reset +import caseapp._ +``` diff --git a/docs/pages/types.md b/docs/pages/types.md new file mode 100644 index 00000000..8a9da349 --- /dev/null +++ b/docs/pages/types.md @@ -0,0 +1,143 @@ +# Option types + +case-app pre-defines parsers for a number of standard types, and allows you +to define parsers of your own. + +## Booleans + +Boolean fields are mapped to "flags", that is options not requiring a value: +```scala mdoc:reset:silent +case class Options( + foo: Boolean // --foo +) +``` + +While these do not require a value, one can be passed explicitly, with an `=` sign, +like `--foo=true` or `--foo=false`. This is especially useful for boolean fields +whose default value is `true` and is not changed when users specify the flag without a +value. + +## Strings + +```scala mdoc:reset:invisible +import caseapp._ +``` + +String fields are given the value passed to the option as is: +```scala mdoc +case class Options( + foo: String +) + +val (options, _) = CaseApp.parse[Options](Seq("--foo", "123")).toOption.get +``` + +## Numerical values + +Integer types like `Int`, `Long`, `Short`, `Byte`, and floating ones like +`Double`, `Float`, `BigDecimal` are all accepted as field types. + +## Options + +Field types wrapped in `Option[_]` are automatically non-mandatory, even +when the field doesn't have a default value. + +Option types are convenient to know whether an option was specified (the +field value is then `Some(…)`) or not (field value is `None`), +and behave differently if the option wasn't specified, like default to the value +of another option say. + +## Sequences + +```scala mdoc:reset:invisible +import caseapp._ +``` + +Sequences allow users to specify an argument more than once, and get the different values +passed each time: +```scala mdoc +case class Options( + path: List[String] +) + +val (options, _) = CaseApp.parse[Options](Seq("--path", "/a", "--path", "/b", "--path", "/c")).toOption.get +``` + +Only `List` and `Vector` are accepted with the default parsers, no generic `Seq` for example. + +The options corresponding to sequence fields are not mandatory even if the field +doesn't have a default value. If no option for it is specified, it will default to an +empty sequence. + +## Last + +`Last` is an ad-hoc type defined by case-app. Like sequence types, it allows an option +to be specified multiple times. + +Yet, unlike sequence types, it just discards the values passed to the option, but for the +last one. + +So `Last` makes option parsing not fail if its option is specified multiple time, and just +retains the last occurrence of it. + +## Counters + +```scala mdoc:reset:invisible +import caseapp._ +``` + +One may want to allow flags to be specified multiple times. This can be achieved in two +ways out-of-the-box with case-app: +```scala mdoc +case class Options( + verbose: Int @@ Counter = Tag.of(0), + debug: List[Unit] +) + +val (options, _) = CaseApp.parse[Options]( + Seq("--verbose", "--debug", "--verbose", "--verbose", "--debug") +).toOption.get +Tag.unwrap(options.verbose) // --verbose specified 3 times +options.debug.length // --debug specified 2 times +``` + +`@@` and `Tag` are ad-hoc types defined by case-app, that look like types with similar +names defined in the [shapeless](https://github.com/milessabin/shapeless) library. These aim +at helping "tagging" or "annotating" a type. +`Counter` is also defined in case-app. + +Using `List[Unit]` as a type also works. `Unit` itself could be used as a type, but would +be of little help. A field with type `Unit` is mapped to a flag option, like +[`Boolean`](#booleans), but the `Unit` type itself doesn't allow to retain if the flag +was specified by users or not. In a `List` on the other hand, counting the number +of `()` (`Unit` instance) in the list allows to know how many types the flag was specified. + +## Custom parsers + +```scala mdoc:reset:invisible +import caseapp._ +``` + +For a type to be accepted as an option, it needs to have an implicit `ArgParser` in scope. + +The abstract and overriddable methods of `ArgParser` are quite low-level, but allow to +implement all the kind of parsers defined in case-app. + +For simple types, when one only wants to parse a string to a given type, `SimpleArgParser` +allows to define an `ArgParser` out of a `String => T` method. For example, one can +define a parser for integer values with: +```scala mdoc:silent +import caseapp.core.argparser.{ArgParser, SimpleArgParser} +import caseapp.core.Error + +case class MyInt(value: Int) + +implicit lazy val myIntParser: ArgParser[MyInt] = + SimpleArgParser.from("my-int") { input => + try Right(MyInt(input.toInt)) + catch { + case _: NumberFormatException => + Left(Error.MalformedValue("integer", input)) + } + } +```