Type-level & seamless command-line argument parsing for Scala
The code snippets below assume that the content of caseapp
is imported,
import caseapp._
case class Options(
user: Option[String],
enableFoo: Boolean = false,
file: List[String]
)
CaseApp.parse[Options](
Seq("--user", "alice", "--file", "a", "--file", "b")
) == Right((Options(Some("alice"), false, List("a", "b")), Seq.empty))
All arguments are required by default. To define an optional argument simply
wrap its type into Option[T]
.
Optional arguments can also be defined by providing a default value. There are two ways to do that:
- providing default value ad hoc in the case class definition
- defining default value for a type with Default type class
case class Options(
user: Option[String],
enableFoo: Boolean = false,
file: List[String] = Nil
)
CaseApp.parse[Options](Seq()) == Right((Options(None, false, Nil), Seq.empty))
Some arguments can be specified several times on the command-line. These
should be typed as lists, e.g. file
in
case class Options(
user: Option[String],
enableFoo: Boolean = false,
file: List[String]
)
CaseApp.parse[Options](
Seq("--file", "a", "--file", "b")
) == Right((Options(None, false, List("a", "b")), Seq.empty))
If an argument is specified several times, but is not typed as a List
(or an accumulating type,
see below), the final value of its corresponding field is the last provided in the arguments.
By default, all arguments are parsed as-is. To enable expanding arguments before argument parsing, override
If supported by the platform, case-app can expand each argument of the form: @<filename>
with
the contents of <filename>
where each line constitutes a distinct argument.
For example, @args
where args
is a file containing the following:
--
-foo
1
is equivalent to: -- -foo
.
This behavior is disabled by default.
To enable argument file expansion, override CaseApp.expandArgs
as follows:
import caseapp.core.parser.PlatformArgsExpander
override def expandArgs(args: List[String]): List[String] = PlatformArgsExpander.expand(args)
Alternatively, override this function with a custom argument expansion mechanism.
case-app can take care of the creation of the main
method parsing
command-line arguments.
import caseapp._
case class ExampleOptions(
foo: String,
bar: Int
)
object Example extends CaseApp[ExampleOptions] {
def run(options: ExampleOptions, arg: RemainingArgs): Unit = {
// Core of the app
// ...
}
}
Example
in the above example will then have a main
method, parsing
the arguments it is given to an ExampleOptions
, then calling the run
method
if parsing was successful.
Running the above example with the --help
(or -h
) option will print an help message
of the form
Example
Usage: example [options]
--foo <value>
--bar <value>
Calling it with the --usage
option will print
Usage: example [options]
Several parts of the above help message can be customized by annotating
ExampleOptions
or its fields:
@AppName("MyApp")
@AppVersion("0.1.0")
@ProgName("my-app-cli")
case class ExampleOptions(
@HelpMessage("the foo")
@ValueDescription("foo")
foo: String,
@HelpMessage("the bar")
@ValueDescription("bar")
bar: Int
)
Called with the --help
or -h
option, would print
MyApp 0.1.0
Usage: my-app-cli [options]
--foo <foo>: the foo
--bar <bar>: the bar
Note the application name that changed, on the first line. Note also the version
number appended next to it. The program name, after Usage:
, was changed too.
Lastly, the options value descriptions (<foo>
and <bar>
) and help messages
(the foo
and the bar
), were customized.
Alternative option names can be specified, like
case class ExampleOptions(
@ExtraName("f")
foo: String,
@ExtraName("b")
bar: Int
)
--foo
and -f
, and --bar
and -b
would then be equivalent.
Field names, or extra names as above, longer than one letter are considered
long options, prefixed with --
. One letter long names are short options,
prefixed with a single -
.
case class ExampleOptions(
a: Int,
foo: String
)
would accept --foo bar
and -a 2
as arguments to set foo
or a
.
Field names or extra names as above, written in pascal case, are split and hyphenized.
case class Options(
fooBar: Double
)
would accept arguments like --foo-bar 2.2
.
Sets of options can be shared between applications:
case class CommonOptions(
foo: String,
bar: Int
)
case class First(
baz: Double,
@Recurse
common: CommonOptions
) {
// ...
}
case class Second(
bas: Long,
@Recurse
common: CommonOptions
) {
// ...
}
case-app has a support for commands.
sealed trait DemoCommand
case class First(
foo: Int,
bar: String
) extends DemoCommand
case class Second(
baz: Double
) extends DemoCommand
object MyApp extends CommandApp[DemoCommand] {
def run(command: DemoCommand, args: RemainingArgs): Unit = {}
}
MyApp
can then be called with arguments like
my-app first --foo 2 --bar a
my-app second --baz 2.4
- help messages
- customization
- base command
- ...
Needs to be updated
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.
Needs to be updated
Use your own option types by defining implicit ArgParser
s for them, like in
import caseapp.core.argparser.{ArgParser, SimpleArgParser}
trait Custom
implicit val customArgParser: ArgParser[Custom] =
SimpleArgParser.from[Custom]("custom") { s =>
// parse s
// return
// - Left(a caseapp.core.Error instance) in case of error
// - Right(custom) in case of success
???
}
Then use them like
case class Options(
custom: Custom,
foo: String
)
A cats-effect module is available, providing
IO
versions of the application classes referenced above. They all extend IOApp
so Timer
and ContextShift
are conveniently available.
// additional imports
import caseapp.cats._
import cats.effect._
object IOCaseExample extends IOCaseApp[ExampleOptions] {
def run(options: ExampleOptions, arg: RemainingArgs): IO[ExitCode] = IO {
// Core of the app
// ...
ExitCode.Success
}
}
object IOCommandExample extends CommandApp[DemoCommand] {
def run(command: DemoCommand, args: RemainingArgs): IO[ExitCode] = IO {
// ...
ExitCode.Success
}
}
Shared options used to be automatic, and now require the @Recurse
annotation on the field corresponding to the shared options. This prevents
ambiguities with custom types as above.
Add to your build.sbt
resolvers += Resolver.sonatypeRepo("releases")
libraryDependencies += "com.github.alexarchambault" %% "case-app" % "2.0.0-M3"
// cats-effect module
libraryDependencies += "com.github.alexarchambault" %% "case-app-cats" % "2.0.0-M3"
Note that case-app depends on shapeless 2.3. Use the 1.0.0
version if you depend on shapeless 2.2.
It is built against scala 2.10, 2.11, and 2.12, and 2.13.0-M1, and supports both scala-js and scala-native.
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)
See the full list of contributors on GitHub.
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.
Copyright (c) 2014-2017 Alexandre Archambault and contributors. See LICENSE file for more details.
Released under Apache 2.0 license.