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))