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
}
}
Add to your build.sbt
resolvers += Resolver.sonatypeRepo("releases")
libraryDependencies += "com.github.alexarchambault" %% "case-app" % "2.0.1"
// cats-effect module
libraryDependencies += "com.github.alexarchambault" %% "case-app-cats" % "2.0.1"
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.12, and 2.13, and supports Scala.js too.
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.