Getting started with clipp

Clipp lets you describe an immutable specification of how to turn command line arguments (a sequence of strings) into an application-specific data structure.

The specification is monadic, which makes it very easy to express sub-command behavior, as the parsing steps can depend on a previously parsed value.

Let’s see a simple example first:

import java.io.File

import io.github.vigoo.clipp._
import io.github.vigoo.clipp.parsers._
import io.github.vigoo.clipp.syntax._

case class Parameters1(inputUrl: String,
                       outputFile: File,
                       verbose: Boolean)

val paramSpec1 = 
  for {
    _ <- metadata(programName = "Example 1")
 
    inputUrl <- parameter[String]("URL to download", "url")
    outputFile <- parameter[File]("Target file", "file")
    
    verbose <- flag("Verbose output", 'v', "verbose")
  } yield Parameters1(inputUrl, outputFile, verbose)

This takes two arguments in order, optionally with a -v or --verbose flag which can be in any location (see the exact semantics below), for example:

app -v http://something.to.download /tmp/to 

By using named parameters, the order of them does not matter anymore:

val paramSpec2 = 
  for {
    _ <- metadata(programName = "Example 2")
 
    inputUrl <- namedParameter[String]("URL to download", "url", "input")
    outputFile <- namedParameter[File]("Target file", "file", "output")
    
    verbose <- flag("Verbose output", 'v', "verbose")
  } yield Parameters1(inputUrl, outputFile, verbose)

These can be specified in any order like:

app --output /tmp/to --verbose --input http://something.to.download 

We can use the optinal modifier to mark parts of the parser optional, making their result of type Option[T]. We can for example modify the previous example to make the output optional (and print the downloaded data to the console if it’s not there):

case class Parameters3(inputUrl: String,
                       outputFile: Option[File],
                       verbose: Boolean)

val paramSpec3 = 
  for {
    _ <- metadata(programName = "Example 3")
 
    inputUrl <- namedParameter[String]("URL to download", "url", "input")
    outputFile <- optional { namedParameter[File]("Target file", "file", "output") }
    
    verbose <- flag("Verbose output", 'v', "verbose")
  } yield Parameters3(inputUrl, outputFile, verbose)

Commands

Support for commands is a primary feature of clipp. The idea is that at a given point in the sequence of command line arguments, a command selects the mode the application will operate in, and it selects possible parameters accepted after it. It is possible to create a hierarchy of commands. Think of aws-cli as an example.

Because the specification is monadic, it is very convenient to express this kind of behavior:

sealed trait Subcommand
case class Create(name: String) extends Subcommand
case class Delete(id: Int) extends Subcommand

sealed trait Command
case class First(input: String) extends Command
case class Second(val1: Int, val2: Option[Int]) extends Command
case class Third(interactive: Boolean, subcommand: Subcommand) extends Command

case class Parameters4(verbose: Boolean,
                       command: Command)

val paramSpec4 =
  for {
    _ <- metadata(programName = "Example 4")

    verbose <- flag("Verbose output", 'v', "verbose")
    commandName <- command("first", "second", "third")
    command <- commandName match {
      case "first" =>
        for {
          input <- namedParameter[String]("Input value", "value", "input")
        } yield First(input)
      case "second" =>
        for {
          val1 <- namedParameter[Int]("First input value", "value", "val1")
          val2 <- optional { namedParameter[Int]("Second input value", "value", "val2") }
        } yield Second(val1, val2)
      case "third" =>
        for {
          interactive <- flag("Interactive mode", "interactive")
          subcommandName <- command("create", "delete")
          subcommand <- subcommandName match {
            case "create" => parameter[String]("Name of the thing to create", "name").map(Create(_))
            case "delete" => parameter[Int]("Id of the thing to delete", "id").map(Delete(_))
          }
        } yield Third(interactive, subcommand)
    }
  } yield Parameters4(verbose, command)

Custom validations and lifting effects

It is possible to perform validations from the parameter parser with the fail parser command:

val paramSpec5 =
  for {
    input  <- namedParameter[Int]("Input value", "VALUE", "input")
    result <- if (input > 10) pure(input) else fail[String, Int]("Input is less than 10")
  } yield result
// paramSpec5: cats.free.Free[Parameter, Int] = FlatMapped(
//   c = Suspend(
//     a = NamedParameter(
//       shortName = None,
//       longNames = Set("input"),
//       placeholder = "VALUE",
//       description = "Input value",
//       explicitChoices = None,
//       parameterParser = io.github.vigoo.clipp.parsers$$anon$5@1e77ae7c
//     )
//   ),
//   f = <function1>
// )

When custom validation or transformation requires performing a side effect, it has to be lifted to the parameter parser. This guarantees that the usage info generation does not run the actual effect on example inputs.

Note that the side effects must be idempotent, because the parser may run them twice (the first pass determines the position of the commands, second pass do the parsing).

Example for this:

import java.io.File

val paramSpec6 =
  for {
    input  <- namedParameter[File]("Input", "FILE", "input")
    result <- liftEither("file existence check", new File("example")) {
        if (input.exists()) Right(input) else Left("File does not exist") 
    }
  } yield result
// paramSpec6: cats.free.Free[Parameter, File] = FlatMapped(
//   c = Suspend(
//     a = NamedParameter(
//       shortName = None,
//       longNames = Set("input"),
//       placeholder = "FILE",
//       description = "Input",
//       explicitChoices = None,
//       parameterParser = io.github.vigoo.clipp.parsers$$anon$10@327ad95b
//     )
//   ),
//   f = <function1>
// )

Semantics

The semantics of these parsing commands are the following:

  • flag looks for the given flag before the first command location (if any), in case it finds one it removes it from the list of arguments and returns true.
  • namedParameter[T] looks for a --name value pair of arguments before the first command location (if any), removes both from the list of arguments and parses the value with an instance of the ParameterParser type class
  • parameter[T] takes the first argument that does not start with - before the first command location (if any) and **removes it from the list of arguments, then parses the value with an instance of the ParameterParser type class
  • optional makes parser specification section optional
  • repeated repeates a parser specification until it fails and collects the results as a list.
  • command is a special parameter with a fix set of values, which is parsed by taking the first argument that does not start with - and it drops all the arguments until the command from the list of arguments.

The semantics of command strongly influences the set of CLI interfaces parseable by this library, but it is a very important detail for the current implementation.

Custom failures can be introduced with the fail parser command.