Getting Started

You can depend on the project from maven central in SBT like so:

libraryDependencies += "net.andimiller" %% "recline" % "0.0.11"

For basic usage you should have these imports

import com.monovore.decline._
import net.andimiller.recline.annotations._
import net.andimiller.recline.generic._

A standard CLI

If you’ve already got a case class representing your configuration, you can just derive a command line parser for it.

case class Configuration(port: Int, hostname: String)

val command = Command("server", "")(deriveOpts[Configuration])
// command: Command[Configuration] = com.monovore.decline.Command@603d4dcf

command.parse(List("--help"))
// res0: Either[Help, Configuration] = Left(Usage: server [--port <integer>] [--hostname <string>]
// 
// 
// 
// Options and flags:
//     --help
//         Display this help text.
//     --port <integer>
// 
//     --hostname <string>
// 
// 
// Environment Variables:
//     PORT=<integer>
//     
//     HOSTNAME=<string>)

command.parse(List("--port", "1234", "--hostname", "myserver"))
// res1: Either[Help, Configuration] = Right(Configuration(1234,myserver))

command.parse(List.empty, env=Map(
  "PORT" -> "8080",
  "HOSTNAME" -> "somehost"
))
// res2: Either[Help, Configuration] = Right(Configuration(8080,somehost))

Nested CLIs with some fancy types

case class GraphiteConfig(hostname: String, port: Int)
case class Configuration(name: String, graphite: Option[GraphiteConfig])

val command = Command("server", "")(deriveOpts[Configuration])
// command: Command[Configuration] = com.monovore.decline.Command@22bfae08

This time it’s nested, so let’s see how the command line works

command.parse(List("--help"))
// res4: Either[Help, Configuration] = Left(Usage: server [--name <string>] [[--graphite-hostname <string>] [--graphite-port <integer>]]
// 
// 
// 
// Options and flags:
//     --help
//         Display this help text.
//     --name <string>
// 
//     --graphite-hostname <string>
// 
//     --graphite-port <integer>
// 
// 
// Environment Variables:
//     NAME=<string>
//     
//     GRAPHITE_HOSTNAME=<string>
//     GRAPHITE_PORT=<integer>)

Alright, so we’ve automatically got a graphite prefix on our CLI flags, and GRAPHITE on the environment variables, let’s use those:

command.parse(List("--name", "myprogram", "--graphite-hostname", "localhost", "--graphite-port", "2003"))
// res5: Either[Help, Configuration] = Right(Configuration(myprogram,Some(GraphiteConfig(localhost,2003))))

And what if I forgot the graphite port?

command.parse(List("--name", "myprogram", "--graphite-hostname", "localhost"))
// res6: Either[Help, Configuration] = Left(Missing expected flag --graphite-port, or environment variable (GRAPHITE_PORT)!
// 
// Usage: server [--name <string>] [[--graphite-hostname <string>] [--graphite-port <integer>]]
// 
// 
// 
// Options and flags:
//     --help
//         Display this help text.
//     --name <string>
// 
//     --graphite-hostname <string>
// 
//     --graphite-port <integer>
// 
// 
// Environment Variables:
//     NAME=<string>
//     
//     GRAPHITE_HOSTNAME=<string>
//     GRAPHITE_PORT=<integer>)

And if there was a default value for it?

case class GraphiteConfig(hostname: String, port: Int = 2003)
case class Configuration(name: String, graphite: Option[GraphiteConfig])

val command = Command("server", "")(deriveOpts[Configuration])
// command: Command[Configuration] = com.monovore.decline.Command@1c664611
command.parse(List("--name", "myprogram", "--graphite-hostname", "localhost"))
// res8: Either[Help, Configuration] = Right(Configuration(myprogram,Some(GraphiteConfig(localhost,2003))))

Okay cool but what if I want to parse into an unusual type?

import java.time.Instant
case class Configuration(name: String, timestamp: Instant)
val cli = deriveOpts[Configuration]
// error: diverging implicit expansion for type net.andimiller.recline.types.Cli[repl.Session.App2.Configuration]
// starting with method fromCliDeriver in object Cli
// val cli = deriveOpts[Configuration]
//                     ^

So this means we’re probably missing one of the types we need, let’s try providing one:

import cats.implicits._, cats.data._
import scala.util.Try
implicit val instantArgument: Argument[Instant] = new Argument[Instant] {
  override def read(string: String): ValidatedNel[String, Instant] =
    Try { Instant.parse(string) }.toEither.leftMap(_.getLocalizedMessage).toValidatedNel
  override def defaultMetavar = "timestamp"
}
// instantArgument: Argument[Instant] = repl.Session$App9$$anon$6@306253ba
val cli = deriveOpts[Configuration]
// cli: Opts[Configuration] = Opts([--name <string>] [--timestamp <timestamp>])

Command("my program", "")(cli).parse(List("--name", "foo", "--timestamp", "2019-07-02T12:23:58.006Z"))
// res11: Either[Help, Configuration] = Right(Configuration(foo,2019-07-02T12:23:58.006Z))