Type-level & seamless command-line argument parsing for Scala
See the website for more details (website sources on GitHub if the website isn't responsive)
Type-level & seamless command-line argument parsing for Scala
Home Page: https://alexarchambault.github.io/case-app/
License: Other
Type-level & seamless command-line argument parsing for Scala
See the website for more details (website sources on GitHub if the website isn't responsive)
I love this library, thank you!
Some things I would like to see that afaict it doesn't have now:
case class
for the help-string@Recurse
annotation apparently does); ideally that would just happen automatically, but could be configured to e.g. prepend a string to the front of all the fields in the nested class, or just flatten them into the top-level class's argument/field namepace, etc.I may try to dig in to this when I have a moment, will post any notable updates here
This perplexes me; help will not run if required arguments are not supplied:
> run --help
[info] Running io.enoble.svg2d.Main --help
Required option input / List(Name(input), Name(i)) not specified
Is this not the reason why help is... helpful? ;)
To catch #15 before hand.
Using caseapp 1.2.0, and a slight tweak from the example code in the README:
import caseapp._
case class ExampleOptions(
theBar: Int
)
object Example extends CaseApp[ExampleOptions] {
def run(options: ExampleOptions, arg: RemainingArgs): Unit = ()
}
If you run without specifying --the-bar
, you see:
$ sbt -error run
[error] Required option --theBar not specified
The error message should display --the-bar
, not --theBar
. This can cause some major confusion:
$ sbt -error run --theBar 0
[error] Required option --theBar not specified
Hi,
Would you be open for me to open a PR to support scala.concurrent.duration.{Duration, FiniteDuration}
?
As in:
implicit val durationParser: ArgParser[Duration] =
SimpleArgParser.from[Duration]("Duration")(s =>
Either.catchOnly[NumberFormatException](Duration(s)).leftMap(e => MalformedValue("Duration", e.getMessage))
)
implicit val finiteDurationParser: ArgParser[FiniteDuration] =
SimpleArgParser.from[FiniteDuration]("FiniteDuration")(s =>
Either
.catchOnly[NumberFormatException](Duration(s))
.leftMap(e => MalformedValue("FiniteDuration", e.getMessage))
.flatMap {
case finite: FiniteDuration => Right(finite)
case _ => Left(MalformedValue("FiniteDuration", s"Expected finite duration but found infinite duration: $s"))
}
)
Let me know
Thanks
Add support for commands
Should likely amount to have CaseApp.parse[...]
accept an ADT base type as argument, e.g.
sealed trait Commands
case class Meh(options...) extends Commands
case class Neh(options...) extends Commands
CaseApp.parse[Commands]
The resulting Commands
in the return type would either be a Meh
or a Neh
, depending on the command found.
Commands
could also be replaced by a (shapeless) union type, like
CaseApp.parse[Union.`'meh -> Meh, 'neh -> Neh`.T]
A convenient way to specify options accepted when no command is specified has to be found, though.
SemanticDB uses helper for this purpose:
https://github.com/scalameta/scalameta/blob/master/semanticdb/cli/src/main/scala/scala/meta/internal/cli/Args.scala
The idea is to use the SemanticDB Args.expand utility in Parser.detailedParse's call to the inner helper function.
They have to be unwrapped (...), to get the relevant value (the Int
out of verbose: Int @@ Counter
say). Add a note about it in README, and/or remove them in next version (replacing them with a real class Counter
, like verbose: Counter
).
Would it be possible to introduce a new parser type that ignores the unrecognized arguments, flags, etc?
Very tiny issue with zsh completion.
I'm not sure what typeset -A opt_args
does here
case-app/core/shared/src/main/scala/caseapp/core/complete/Zsh.scala
Lines 12 to 19 in fbeaa6b
but with it, on a new shell, I always need to hit tab twice for the first completion to work, e.g. try scala-cli re|
on a new shell, but after that everything works normally. After taking out that line the first completion on a new shell works on the first tab. Tested with scala-cli on Arch Linux and Mac OS (intel), zsh 5.9.
I have the following common options
case class CommonOptions(
prod: Boolean = false,
dry: Boolean = false,
@Recurse slack: SlackOptions
)
with slack options defined with
case class SlackOptions(
slackToken: Option[String],
slackChannel: Option[String]
)
I would like to be able to define my case class like this
case class SlackOptions(
token: Option[String],
channel: Option[String]
)
But I want to keep context informations when I use command line
--slack-token=... --slack-channel=...
It should be great to have one annotation "@PrefixRecurse" (or similar) that uses the name used in parent in command line
--slack.token=... --slack.channel=...
Maybe do you have some tricks to do that? Using "ExtraName" is for the moment the best solution I found but I will have conflict If I have same keys in different context
case class SlackOptions(
@ExtraName("slack-token")
token: Option[String],
@ExtraName("slack-channel")
channel: Option[String]
)
Hi
I'm trying to abstract over parsing arguments and preparing general class skeleton, but this code does not compile and it shows implicits missing:
import caseapp._
import caseapp.core.help.Help
case class Arguments(foo: Int, bar: String)
object MyApp extends AbstractApp[Arguments] {
run[Arguments]()
}
trait AbstractApp[T] extends App {
def run[T: Parser: Help]() = {
val parsedArgs = CaseApp.parse[T](args)
println(parsedArgs)
}
}
How can I make this work?
Also where does those implicits come from when used in just a single file?
Annotations (@ExtraName
, @AppName
, @AppVersion
, ...) expect litteral arguments. They'll likely crash with variable arguments.
Add a note about that in doc.
Is it be possible to take a case class and turn it into a List [String]
that would generate the given case class?
The help messages generated by case-app are currently rather raw. All the options are printed, without any grouping of related options, with no reflow of text depending on the terminal width, no colors, etc. For example:
$ cs launch --help
Command: launch
Usage: cs launch <org:name:version|app-name[:version]*>
--main-class | -M | --main <string>
--extra-jars <string*>
Extra JARs to be added to the classpath of the launched application. Directories accepted too.
--property | -D <key=value>
Set Java properties before launching the app
--fork <bool?>
โฆ
(there's approx. ~70 options like this in this command!)
scopt or picocli could be used as sources of inspiration (there's probably many other libraries worth a look tooโฆ)
About implementing that, colors and reflow should just be a matter of changing some methods in Help
.
Grouping arguments is more tricky I think, and requires some changes in the typelevel core of case-app. In more detail, I believe it should be possible to group arguments, by
val group: String = ""
argument to Recurse
,recurse: HList
argument to HListParserBuilder.apply
, and passing it recurse
around here,group: List[String]
field to Arg
,group: Option[String]
field to RecursiveConsParser
,HListParserBuilder.hconsRecursive
, we should be able to call recurse.head
in a typesafe way, and pass recurse.head.group
to RecursiveConsParser
.We should then adjust RecursiveConsParser.args
, so that it prepends its group (if it has one) to each of the Arg
s it returns.
Lastly, in Help
, we can then use the Arg.group
of its args
to group / show arguments however we want in the help message generated there. I didn't actually try to implement it, hopefully I didn't miss blockers along the way.
Users would then be able to group arguments like
case class Group1(arg: String = "", n: Int = 0)
case class Group2(other: String = "", m: Int = 0)
case class Options(
@Recurse("Group 1")
group1: Group1 = Group1(),
@Recurse("Group 2")
group2: Group2 = Group2()
)
Just some fancy features that I think would be cool to have. It would be nice if the --help message could
<type>
of the flag instead of "<value>
". For example, <bool>
for Boolean
or <int*>
for List[Int]
.Scala-native can really use a nice command line argument parsing library like case-app.
Here is a short test program:
import caseapp._
case class EwApiDemoOptions (
@ExtraName("l") displaySpeciesList: Boolean = false,
@ExtraName("s") displaySpecies: String = ""
) extends App {
if (displaySpeciesList) {
println("speciesList")
} else if (displaySpecies.nonEmpty) {
println("displaySpecies")
} else if (!displaySpeciesList && displaySpecies.isEmpty)
Console.err.println(CaseApp.helpMessage[EwApiDemoOptions])
}
object EwApiDemo extends AppOf[EwApiDemoOptions]
delayedinit-select
, which causes this warning to appear several times: Selecting value displaySpeciesList from class EwApiDemoOptions, which extends scala.DelayedInit, is likely to yield an uninitialized value
. Yes, I can specify:scalacOptions ++= Seq(
"-Xlint",
"-Xlint:-delayedinit-select"
)
But that turns off the warnings for any other delayed init issues that may exist. Is there another way to structure the program to avoid delayed initialization entirely?
argument missing
message is displayed and then an ugly exception is thrown:$ sbt "runMain EwApiDemo2 -s"
... lots of output ...
argument missing
Exception: sbt.TrapExitSecurityException thrown from the UncaughtExceptionHandler in thread "run-main-0"
java.lang.RuntimeException: Nonzero exit code: 1
at scala.sys.package$.error(package.scala:27)
[trace] Stack trace suppressed: run last compile:runMain for the full output.
[error] (compile:runMain) Nonzero exit code: 1
[error] Total time: 1 s, completed Mar 1, 2017 5:43:30 PM
Users should not see the exception. Instead, they should politely be told what happened, and what they need to do to correct the problem. A better output would be:
Error: the -s/--displaySpecies option requires an argument, but none was supplied. Please retry with a value for the option.
Until this is fixed, how might I catch the exception?
In our codebase, we instantiate a lot of CaseApp
s.
With a profiling, we noticed we instantiate a lot of time the same Parser
s.
Hence, we would like to cache them for the implicit resolution. However I do not successfully instantiate one. Here is a MVE:
import caseapp._
object Main {
final case class Foo(foo: Int)
implicit val parserFoo: Parser[Foo] = Parser.generic
}
I get the error could not find implicit value for parameter lowPriority: caseapp.util.LowPriority
.
I guess I am missing some imports, am I right?
Which imports could help?
Is there a simpler way to generate a Parser? (I see that the API changes in 2.1.0-M8)
I am using:
I can't get the Commands work for the CaseApp.parse[T]
method. The compiler can't find the implicit Parser.
What am I missing?
import caseapp._
sealed trait DemoCommand
case class First() extends DemoCommand
case class Second() extends DemoCommand
object MyApp extends App{
CaseApp.parse[DemoCommand](List())
}
could not find implicit value for evidence parameter of type caseapp.core.parser.Parser[DemoCommand]
When using a custom case class type, like
case class Custom(...)
in an argument case class, like:
case class Options(
custom: Custom
)
// Then used like
CaseApp.parse[Options](args)
it can have two meanings:
ArgParser[Custom]
available,A possible implicit ArgParser[Custom]
put at the wrong place, or given the wrong type, should lead to a compilation error. But it doesn't because of the latter case acting as a fallback.
The two cases could be disambiguated by enforcing nested options to be annotated, like
case class Options(
@nested extraOptions: ExtraOptions
)
This way, there would be no ambiguity between the two (either the field is annotated with @nested
, or we look for an implicit ArgParser[...]
).
With version 1.2.0, it seems that defaultDescription
are always set to None
Here is a small example to reproduce the bug : https://scastie.scala-lang.org/YgickCTpTkWDK7xpoVTOJw
Arg(i,List(Name(i)),None,None,false,false,int,None)
Arg(s,List(Name(s)),None,None,false,false,string,None)
Arg(b,List(Name(b)),None,None,false,true,bool,None)
Arg(li,List(Name(li)),None,None,false,false,int*,None)
And it works with version 1.2.0-M2
Arg(i,List(Name(i)),None,None,false,false,int,Some(2))
Arg(s,List(Name(s)),None,None,false,false,string,Some("defaultt"))
Arg(b,List(Name(b)),None,None,false,true,bool,Some(false))
Arg(li,List(Name(li)),None,None,false,false,int*,Some(List()))
While adding documentation for #202 . I started thinking the README.md
could be getting a touch large at this point.
The the docusaurus stuff from coursier could be reused here to produce a doc site https://github.com/coursier/coursier/blob/b08cd47ef2b5f2c269b97d32a36228dc4f94ba02/doc/docs/dev-website.md .
Maybe I'm a bit picky, but the Scala style guide recommends annotation names to be in lower camel case like @appName
. Unless there are a certain reason, how about following the guide?
One concern is source compatibility as I'm not sure if we can deprecate an annotation itself smoothly.
In following example, help message annotated against Start command is not printed anywhere when we run either app --help
or app start --help
command.
sealed trait Command
@CommandName("start")
@HelpMessage("Starts all the services when no option is provided")
case class Start(
@HelpMessage("Start Config Service")
config: Boolean = false
@HelpMessage("Start Event Service")
event: Boolean = false
) extends Command
Following help gets printed when I run app start --help
Command: start
Usage: app start
--config <bool>
start config server
--event <bool>
start event server
If an option type is boolean, and a default value of true
is provided, there doesn't seem to be a way to set it to false on the command line:
[info] Starting scala interpreter...
Welcome to Scala 2.13.3 (Java HotSpot(TM) 64-Bit Server VM, Java 11.0.4).
Type in expressions for evaluation. Or try :help.
scala> import caseapp._
import caseapp._
scala> case class Options(enableFoo: Boolean = false)
class Options
scala> CaseApp.parse[Options](Seq())
val res0: Either[caseapp.core.Error,(Options, Seq[String])] = Right((Options(false),List()))
scala> CaseApp.parse[Options](Seq("--enable-foo", "true"))
val res2: Either[caseapp.core.Error,(Options, Seq[String])] = Right((Options(true),List(true)))
scala> CaseApp.parse[Options](Seq("--enable-foo", "false"))
val res3: Either[caseapp.core.Error,(Options, Seq[String])] = Right((Options(true),List(false)))
scala> case class Options2(enableFoo: Boolean = true)
class Options2
scala> CaseApp.parse[Options2](Seq("--enable-foo", "false"))
val res5: Either[caseapp.core.Error,(Options2, Seq[String])] = Right((Options2(true),List(false)))
scala> CaseApp.parse[Options2](Seq())
val res6: Either[caseapp.core.Error,(Options2, Seq[String])] = Right((Options2(true),List()))
For the case of res3
above, I would expect to be able to explicitly pass false
, although I can get a false
value by not providing the argument at all (e.g. res0
). Unexpectedly --enable-foo false
sets the value to true
, because "false" is treated as an extra argument.
For the case of the default true value (Options2
), there doesn't seem to be any command line that can set it to false.
$ scala-cli e
(eval):5: unmatched '
(eval):5: unmatched '
(eval):5: unmatched '
The problem seems to be with those escaped '
characters:
$ scala-cli complete zsh-v1 "1" "e"
local -a args306746944
args306746944=(
'export:The \'export\' sub-command is experimental.'
)
_describe command args306746944
Without the backslashes the completions work, but the quotes simply aren't printed. I'm not sure if there's any way to output single quotes succesfully...
The current ways to construct a CommandParser
are fairly limited. It would be cool to have something like this:
CommandParser.fromRecord(
"build" ->> BuildCommand ::
"check" ->> CheckCommand ::
HNil
)
I'm a little fuzzy on the details, though. Ideally, this should support common and command-specific options, e.g.
cli-app --config /home/lars/.local/cli-app.conf build --session HOL
Maybe something like this?
CommandParser.fromRecord[Common] { common =>
"build" ->> BuildCommand(common) ::
"check" ->> CheckCommand(common) ::
HNil
}
... where the return type of *Command
is some Parser[T]
.
Thanks for the fantastic library @alexarchambault , we've been using this quite a bit internally. Is there any appetite to add a case-app-cats
module, the code to support this would look roughly like https://gist.github.com/Slakah/9b905b0fd20b715c28862da913ff73c5 .
In effect IOCaseApp
and IOCommandApp
leverage IOApp
to provide the override def run(args: List[String]): IO[ExitCode]
.
Currently multiple options with the same @Short
would silently run, so the behavior would be nondeterministic.
Following up with coursier/coursier#797 (review)
If you run following example without providing any arguments, app exits with zero exit code without help/usage
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 = {}
}
Currently having issues with publishing on the CI. sbt-ci-release and sbt-pgp don't really allow for fine grained customization of gpg-related things (passing custom gpg options, etc.). We should switch the build here to Mill, and use the same setup as many of my other projects (setup that must itself be based on the Ammonite's one IIRC), not using mill-ci-release, that doesn't allow for customization / tweakings of gpg things either. This particular setup, with secrets uploaded via touch Foo.scala && scala-cli publish setup --ci Foo.scala
has never failed me.
Make it more readable. (Generated, ร la scalatex?)
Note about the different versions (which shapeless version they depend on, ...)
Using caseapp 1.2.0, if you have:
import caseapp._
case class ExampleOptions(
foo: String,
bar: Int
)
object Example extends CaseApp[ExampleOptions] {
def run(options: ExampleOptions, arg: RemainingArgs): Unit = {
println(options)
println(arg)
}
}
If you run without arguments, you see:
$ sbt -error run
[error] Required option --foo not specified
Which is correct, but it'd be awesome if both --foo
and --bar
were reported missing.
The automatic Parser
derivation doesn't seem to work with shapeless 2.3. See this ammonite session
$ amm
@ load.ivy("com.chuusai" %% "shapeless" % "2.3.1")
@ load.ivy("com.github.alexarchambault" %% "case-app" % "1.0.0-RC3")
@ import caseapp._
import caseapp._
@ case class HasStr(str: String)
defined class HasStr
@ CaseApp.parse[HasStr](Seq("--str", "banana"))
cmd4.sc:1: could not find implicit value for evidence parameter of type caseapp.Parser[$sess.cmd3.HasStr]
val res4 = CaseApp.parse[HasStr](Seq("--str", "banana"))
^
Compilation Failed
@
If I remove the first line that loads shapeless 2.3 the compilation error goes away. My project transitively pulled in shapeless 2.3 via another dependency.
It would be great if a bash autocompletion string could be generated from the options adt. It could then be called by an sbt task that drops it in the target folder to be included by in a native packager. In the Rust world, Clap has this built in: https://github.com/kbknapp/clap-rs/blob/ebe14558e65e961f4340e2e74ec4eaa5f566e440/src/completions/bash.rs#L1
We need to access the default value of each arg of a CaseApp. with version 1.2.0, we used to do something like:
val caseApp: CaseApp[T] = ???
for (arg <- app.parser.args) {
arg.defaultDescription
}
In version 2.0.0-M3, defaultDescription
seems removed, are there any alternative way to do that please?
Stumbled across this while working on fixing #70. The SimpleArgParser
reports any duplicate arguments it finds as "???", instead of the arg name (here's a test that asserts on the behavior).
It seems like the SimpleArgParser doesn't have any knowledge of the names attached to the values it consumes, so I'm wondering if the responsibility for checking duplicate names should be moved to some other enclosing parser.
Using a custom parser in a command leads to an error when called with --help (if any (custom) required argument is not passed):
Main.scala:
sealed trait UtilsCommand extends Command
case class Test(file: java.io.File) extends UtilsCommand {
println(file)
}
object Main extends CommandAppOf[UtilsCommand]
someThingInScope.scala:
implicit val clJFileParser: ArgParser[java.io.File] =
ArgParser.instance[java.io.File] { s =>
Try(new java.io.File(s)) match {
case Success(x) if x.exists() => Right(x)
case Success(x) => Left(s"$s does not exist!")
case _ => Left(s"$s is not a valid file?")
}
}
command line:
java -jar .\utils.jar test --help
Required option file / List(Name(file)) not specified // In HListParser::get ?
It seems that caseapp can't find annotations on commands.
When running the following command in sbt:
sbt:case-app-root> testsJVM/test:runMain caseapp.demo.CommandAppTest --help
I expect to see:
Demo
Usage: demo-cli [options] [command] [command-options]
Available commands: first, second
Type demo-cli command --help for help on an individual command
Actual result:
None.type
Usage: none.type [options] [command] [command-options]
Available commands: first, second
Type none.type command --help for help on an individual command
It seems it's do-able, see this question and this one.
As of this commit shapeless no longer depends on macro-compat for Scala > 2.10, instead it provides a local definition of macrocompat.bundle
to preserve source compatibility for later Scala versions.
Unfortunately this interferes with case-app's extension of CaseClassMacros
in AnnotationListMacros
, as discovered by @SethTisue in the 2.12.x community build.
I suggest making a similar change here: provide a local definition of macrocompat.bundle
which is only used for Scala > 2.10.
At some point in the next year or two shapeless will drop 2.10 support, at which point the macro-compat dependency will go away altogether. If case-app drops 2.10 support now it could immediately drop the macro-compat dependency which would also solve this issue.
Hi @alexarchambault!
I'm just wondering if adding support for other types of option names (i.e camel case --camelCase
) is this something that could be added to case-app
or if you want to keep it opinionated and only support --camel-case
.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.