Using Decline

Welcome to decline! Here, we’ll run through all of decline’s major features and look at how they fit together.

decline is packaged under com.monovore.decline, so let’s pull that in:

import com.monovore.decline._

Basic Options

‘Normal’ options take a single argument, with a specific type. (It’s important that you specify the type here; the compiler usually can’t infer it!) This lets you parse options like the -n50 in tail -n50.

val lines = Opts.option[Int]("lines", short = "n", metavar = "count", help = "Set a number of lines.")
// lines: com.monovore.decline.Opts[Int] = Opts(--lines <count>)

Flags are similar, but take no arguments. This is often used for ‘boolean’ flags, like the --quiet in tail --quiet.

val quiet = Opts.flag("quiet", help = "Don't print any metadata to the console.")
// quiet: com.monovore.decline.Opts[Unit] = Opts(--quiet)

Positional arguments aren’t marked off by hyphens at all, but they do take a type parameter. This handles arguments like the file.txt in tail file.txt.

import java.nio.file.Path
// import java.nio.file.Path

val file = Opts.argument[Path](metavar = "file")
// file: com.monovore.decline.Opts[java.nio.file.Path] = Opts(<file>)

Each of these option types has a plural form, which are useful when you want users to pass the same kind of option multiple times. Instead of just returning a value A, repeated options and positional arguments will return a NonEmptyList[A], with all the values that were passed on the command line; repeated flags will return the number of times that flag was passed.

val settings = Opts.options[String]("setting", help = "...")
// settings: com.monovore.decline.Opts[cats.data.NonEmptyList[String]] = Opts(--setting <string> [--setting <string>]...)

val verbose = Opts.flags("verbose", help = "Print extra metadata to the console.")
// verbose: com.monovore.decline.Opts[Int] = Opts(--verbose [--verbose]...)

val files = Opts.arguments[String]("file")
// files: com.monovore.decline.Opts[cats.data.NonEmptyList[String]] = Opts(<file>...)

You can also read a value directly from an environment variable.

val port = Opts.env[Int]("PORT", help = "The port to run on.")
// port: com.monovore.decline.Opts[Int] = Opts()

Default Values

All of the above options are required: if they’re missing, the parser will complain. We can allow missing values with the withDefault method:

val linesOrDefault = lines.withDefault(10)
// linesOrDefault: com.monovore.decline.Opts[Int] = Opts([--lines <count>])

That returns a new Opts[Int] instance… but this one can always return a value, whether or not --lines is passed on the command line.

There’s a few more handy combinators for some particularly common cases:

val optionalFile = file.orNone
// optionalFile: com.monovore.decline.Opts[Option[java.nio.file.Path]] = Opts([<file>])

val fileList = files.orEmpty
// fileList: com.monovore.decline.Opts[List[String]] = Opts([<file>...])

val quietOrNot = quiet.orFalse
// quietOrNot: com.monovore.decline.Opts[Boolean] = Opts([--quiet])

Transforming and Validating

Like many other Scala types, Opts can be mapped over.

lines.map { _.toString }
// res0: com.monovore.decline.Opts[String] = Opts(--lines <count>)

validate is much like filter – the parser will fail if the parsed value doesn’t match the given function – but it comes with a spot for a better error message. mapValidated lets you validate and transform at once, since that’s sometimes useful.

import cats.data.Validated
// import cats.data.Validated

val validated = lines.validate("Must be positive!") { _ > 0 }
// validated: com.monovore.decline.Opts[Int] = Opts(--lines <count>)

val both = lines.mapValidated { n =>
  if (n > 0) Validated.valid(n.toString)
  else Validated.invalidNel("Must be positive!")
}
// both: com.monovore.decline.Opts[String] = Opts(--lines <count>)

Combining Options

You can combine multiple Opts instances using catsapplicative syntax:

import cats.implicits._
// import cats.implicits._

val tailOptions = (linesOrDefault, fileList).mapN { (n, files) =>
  println(s"LOG: Printing the last $n lines from each file in $files!")
}
// tailOptions: com.monovore.decline.Opts[Unit] = Opts([--lines <count>] [<file>...])

Other options are mutually exclusive: you might want to pass --verbose to make a command noisier, or --quiet to make it quieter, but it doesn’t make sense to do both at once!

val verbosity = verbose orElse quiet.map { _ => -1 } withDefault 0
// verbosity: com.monovore.decline.Opts[Int] = Opts([--verbose [--verbose]... | --quiet])

When you combine Opts instances with orElse like this, the parser will choose the first alternative that matches the given command-line arguments.

Commands and Subcommands

A Command bundles up an Opts instance with some extra metadata, like a command name and description.

val tailCommand = Command(
  name = "tail",
  header = "Print the last few lines of one or more files."
) {
  tailOptions
}
// tailCommand: com.monovore.decline.Command[Unit] = com.monovore.decline.Command@75103c9b

To embed the command as part of a larger application, you can wrap it up as a subcommand.

val tailSubcommand = Opts.subcommand(tailCommand)
// tailSubcommand: com.monovore.decline.Opts[Unit] = Opts(tail)

// or, equivalently and more concisely...

val tailSubcommand = Opts.subcommand("tail", help = "Print the few lines of one or more files.") {
  tailOptions
}
// tailSubcommand: com.monovore.decline.Opts[Unit] = Opts(tail)

A subcommand is an instance of Opts… and can be transformed, nested, and combined just like any other option type. (If you’re supporting multiple subcommands, the orElse method is particularly useful: tailSubcommand orElse otherSubcommand orElse ....)

Defining an Application

Commands aren’t just useful for defining subcommands – they can also be used to parse an array of command-line arguments directly. Calling parse returns either the parsed value, if the arguments were good, or a help text if something went wrong.

tailCommand.parse(Seq("-n50", "foo.txt", "bar.txt"))
// LOG: Printing the last 50 lines from each file in List(foo.txt, bar.txt)!
// res3: Either[com.monovore.decline.Help,Unit] = Right(())

tailCommand.parse(Seq("--mystery-option"))
// res4: Either[com.monovore.decline.Help,Unit] =
// Left(Unexpected option: --mystery-option
// 
// Usage: tail [--lines <count>] [<file>...]
// 
// Print the last few lines of one or more files.
// 
// Options and flags:
//     --help
//         Display this help text.
//     --lines <count>, -n <count>
//         Set a number of lines.)

If your parser reads environment variables, you’ll want to pass in the environment as well.

tailCommand.parse(Seq("foo.txt"), sys.env)
// LOG: Printing the last 10 lines from each file in List(foo.txt)!
// res5: Either[com.monovore.decline.Help,Unit] = Right(())

If you have a Command[Unit], extending CommandApp will wire it up to a main method for you.

object Tail extends CommandApp(tailCommand)
// defined object Tail

The resulting application can be called like any other Java app.

Instead of defining a separate command, it’s often easier to just define everything inline:

object Tail extends CommandApp(
  name = "tail",
  header = "Print the last few lines of one or more files.",
  main = (linesOrDefault, fileList).mapN { (n, files) =>
    println(s"LOG: Printing the last $n lines from each file in $files!")
  }
)
// defined object Tail

That’s it! To see a slightly larger example, have a look at the quick start.