Skip to content

Latest commit

 

History

History
320 lines (255 loc) · 8.57 KB

README-v0.3.md

File metadata and controls

320 lines (255 loc) · 8.57 KB

case-app

Type-level & seamless command-line argument parsing for Scala

Build Status Gitter

Just put your options in one or several case classes, like in

case class Options(
  user: Option[String],
  enableFoo: Boolean,
  files: List[String]
)

or

case class AuthOptions(
  user: String,
  password: String
)

case class PathOptions(
  @ExtraName("f") fooPath: String,
  @ExtraName("b") barPath: String
)

case class Options(
  auth: AuthOptions,
  paths: PathOptions
)

Parse them with

CaseApp.parse[Options](args) match {
  case Left(err) =>
    // Option parsing failed, error description in
    //   err: String
  case Right((options, remainingArgs)) =>
    // Success, we have
    //   options: Options, and
    //   remainingArgs: Seq[String]
}

Alternatively, accept help and usage options with

CaseApp.parseWithHelp[Options](args) match {
  case Left(err) =>
    // Option parsing failed, error description in err
  case Right((options, help, usage, remainingArgs)) =>
    // Success, we have options and remainingArgs

    // help and usage booleans tell us whether help or usage were requested
    if (help) {
      CaseApp.printHelp[Options]()
      sys exit 0
    }

    if (usage) {
      CaseApp.printUsage[Options]()
      sys exit 0
    }
}

Last refinement, you can also avoid the pain of having to define an intermediary scala.App singleton or a main method with

case class MyApp(
  user: Option[String], // Nested case classes of options like above also accepted
  enableFoo: Boolean,
  @ExtraName("f") files: List[String]
) extends App { // Extending caseapp.App

  // core of your app, i.e. what would have been in main
  // remaining arguments are in
  //   def remainingArgs: Seq[String]

}

object MyApp extends AppOf[MyApp] {
  val parser = default
}

The singleton MyApp will then contain a main method that will:

  • parse the command-line arguments,
  • if requested, print a help or usage message and exit,
  • run the content of a MyApp case class instance with the options set, and the remaining arguments available through remainingArgs. This uses DelayedInit under the hood.

MyApp can then by run with options, e.g.

--user aaa --enable-foo --file some_file extra_arg other_extra_arg

or

--user bbb -f first_file -f second_file

Usage

Add to your build.sbt

resolvers += Resolver.sonatypeRepo("releases")

libraryDependencies += "com.github.alexarchambault" %% "case-app" % "0.2.2"

For the development version, add instead,

resolvers ++= Seq(
  Resolver.sonatypeRepo("releases"),
  Resolver.sonatypeRepo("snapshots")
)

libraryDependencies += "com.github.alexarchambault" %% "case-app" % "0.2.3-SNAPSHOT"

If you are already using shapeless 2.2, replace 0.2.3-SNAPSHOT by 0.3.0-SNAPSHOT.

If you are using scala 2.10.x, also add the macro paradise plugin to your build,

libraryDependencies +=
  compilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full)

Then import caseapp._ (or just caseapp.CaseApp if you don't need caseapp.App and annotations).

See above for usage examples.

Features

Default values

Options can be given default values. These will be kept if no corresponding option was specified in the arguments.

Also, for options whose type does not match (hard-coded) usual types like Int, String, etc., you must specify a default value for them. (This restriction may be removed in the future - then parsing would simply fail with a Left if no corresponding option was found in the arguments. For now, a default value must be provided.)

Extra names

Options can be given several names, by annotating the corresponding variable:

case class Options(
  @ExtraName("bar") foo: Option[String]
)

One letter names are assumed to be short options (name "a" will match option -a), longer names are assumed to be long options ("bar" will match "--bar").

Names are hyphenized and converted to lower case in order to get the corresponding option, e.g. "fooBar" matches option --foo-bar.

Nested option definitions

As illustrated above, case classes of options can be nested in other case classes to facilitate re-usability of option definitions, like in:

case class CommonOptions(
  user: String,
  password: String
)

case class FirstAppOptions(
  common: CommonOptions,
  foo: Boolean
)

case class SecondAppOptions(
  common: CommonOptions,
  bar: Boolean
)

Options defined in CommonOptions are common to FirstAppOptions and SecondAppOptions.

Counters

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

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:

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.

Lists

You may also expect a list of values for an option, like in

case class Options(
  list: List[String]
)

In this case, case-app will accumulate the options in a list: --list first --list second will make the list variable have the value List("first", "second").

Here too, list can be given a non-Nil default value. Items from the arguments will simply be appended to it.

Help message and options

The help message generated by caseapp can be enhanced with annotations, like in

  @AppName("Glorious App")
  @AppVersion("0.1.0")
case class AppOptions(
    @ValueDescription("a foo")
    @HelpMessage("Specify some foo")
  foo: Option[String],
    @ValueDescription("bar count")
    @HelpMessage("Specify the bar count")
  bar: Int
)

whose help message will be:

Glorious App 0.1.0
Usage: my-app [options]
  --foo  <a foo>
        Specify some foo
  --bar  <bar count>
        Specify the bar count

instead of the default

MyApp
Usage: my-app [options]
  --foo  <value>
  --bar  <value>

Default supported option types

The following types are supported by default: Boolean, Int, Long, Float, Double, String, Calendar (from java.util), Unit (ignored flag argument), and Int @@ Counter (see above), and lists/options of these types.

User-defined option types

Use your own option types by defining implicit ArgParsers for them, like in

implicit val customArgParser: ArgParser[Custom] =
  ArgParser.value[Custom] { (current, arg) =>
    //   current: Current
    // is the current value of this option,
    //   arg: String
    // is the option to parse.
    // Return either:
    //   Success(newValue) if arg contains a valid value for a `Custom`
    //   Failure(reason)   else
  }

Internals

case-app uses the type class facilities from shapeless (mainly through Lazy). It also uses a bit of reflection, mainly to get annotations and default values of case classes parameters. Reflection should be replaced by macros (and more type classes and type level programming) at some point.

TODO

Commands à la git or hadoop or like in scopt (called like app command command arguments...), described by a (shapeless) union type whose keys are command names and values are case classes (as above) of the commands.

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.

Unlike 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-2015 Alexandre Archambault. See LICENSE file for more details.

Released under Apache 2.0 license.