bkirwi / decline Goto Github PK
View Code? Open in Web Editor NEWA composable command-line parser for Scala.
Home Page: http://monovore.com/decline/
License: Apache License 2.0
A composable command-line parser for Scala.
Home Page: http://monovore.com/decline/
License: Apache License 2.0
If we did this, we could imagine that Argument can be a coherent typeclass. As it is, it couples information from parsing and the semantics of how a particular argument is being used.
Instead, we could require the metavar at the Opts site, and if we did that, we could really give a batteries totally included suite of Argument instances for all common types. Indeed, we could put them in the Argument companion for primitives and standard types (BigInt, Path, uuid, etc...)
I have a program that takes in strings as arguments and some of those strings may contain flags:
❯ runs new --name 'Pendulum-v0/maiden' config '--policy SAC --max-time-steps 100000'
Unexpected option: --policy SAC --max-time-steps 100000
Usage: runs new config <config>...
Pass the string of arguments given to launch script as in
❯ docker run -d --rm -it <image> <config>
Options and flags:
--help
Display this help text.
So it appears that decline is parsing '--policy SAC --max-time-steps 100000''
as an option, not as a string argument. Is there a way to fix this?
By the way I love this library! Wish there was something like this for every language.
I can't update a project of mine to scalajs 1.0 due to being blocked by decline.
any objections?
I see that a PR was merged for 1.0.0-RC1 … is there a milestone build out there?
Hi maintainers, I propose adding decline as a related project in cats' README. Please let me know if you approve.
The main advantage of Enum Opts over the string is that you have a full list of all available values. I believe that this should be included in help (currently I do it manually in the brackets which is timeconsuming and leads to errors if I change the enum and forget to change help) and also when the user makes a mistake in the option value it should tell her the list of all possible values from the enum in the error report
It would be nice to be able to add the option to parse arguments from an environment variable. Is that something this library would be interested in doing or should it be provided by some other library?
It would be nice if we could run the application once and then have a sort of REPL in which we could write a command, wait for its completion and write another one. As a further step (much further down the line) commands could change states, resulting in the available command set changing.
The major upside of having this is of course fast startup (not loading a fresh JVM on every time & not allocating resources, if the app has any) and not having to write the application's path every time.
I had an argument of type String, that I expected to be able to pass json in:
cmd --foo "[1, 2, 3]"
but I get a confusing error as it looks like decline is splitting the space json argument.
Hello,
I've done a simple PoC with decline
and I've stumbled on a difficulty or maybe on my own feet.
This is the scenario:
My application is made of multiple modules and the vast majority of them provide a command line parser.
I can pick and choose these modules so that I can build specialized fat .jar files according to circumstances.
In a nutshell, I would like to have the ability of composing several command line parsers.
If I'm not mistaken, "the way composition works" (or at least: the way it is suggested in the documentation) is via tuples. However, the concern that immediately comes to mind is that tuples are limited to 22 elements at the moment, something which will only be lifted with Dotty.
Could someone suggest a way of composing parsers so that an arbitrary number of command line arguments would be manageable?
Thanks
This is mostly a question, and possibly a request to add more examples to documentation.
Suppose I have:
val vehicleType: Opts[VehicleType] = ???
If the user specifies vehicleType Car
, then I want to require another option:
val licensePlate: Opts[String] = ???
If the user specifies vehicleType Bicycle
, then I don't want to require any other options.
How can I accomplish this?
@bkirwi what do you think of a method like this:
def fromMap[A](defmeta: String, nameToValue: Map[String, A]): Argument[A] =
new Argument[A] {
def defaultMetavar: String = defmeta
val keys = nameTo.keys.toList.sorted.mkString(", ")
def read(string: String): ValidatedNel[String, A] =
nameToValue.get(string) match {
case Some(t) => Validated.valid(t)
case None =>
Validated.invalidNel(s"unknown value: $string, expected one of: $keys")
}
This can make it convenient to provide a string to value mapping and get an Argument from it. This is useful for custom enum-like ADTs.
This idea originated from #126
Basically, if the structure of the error is preserved, it'll be possible to write a custom render function for Help
. This can have uses such as for localization and different render formats (in my particular case, "compact" mode which renders with fewer newlines).
I think most of the mac users are using zsh instead of bash. zsh has this awesome plugin for auto completion. I think it could be a great extension for "decline" to autogenerate this zsh-autocompletion output from the Opts. I have seen this output in several CLIs like kubectl for kubernetes (source <(kubectl completion zsh)
).
So my suggestion is to build an plugin for decline that generates the needed output for example with ./my-decline-cli completion zsh
. Perhaps as "com.monovore" %% "decline-zsh-completions" % "1.3.0"?
many of the option parsing statements can be stated in terms of scalacheck pretty easily (for all A, if we call .toString on them, we can get them back). It would be nice to have tests like this to check for any corner cases.
Is it possible to combine a flag with fallback to environment variable and ultimately a default value?
ex)
...
val myArg = (Opts
.flag(
"with-foo",
help = "With foo"
) orElse Opts
.env[String]("WITH_FOO", help = "With foo", metavar = "true||false")) orFalse
...
The desired outcome is to allow for CLI to use the --with-foo
flag but alternatively allow users to instead specify WITH_FOO=true
as an env variable, with both cases defaulting to false
.
Because of the intentional missing instances we can't set Opts.env[Boolean]
, but in the example above the orElse
gives a warning that a type was inferred to be Any; this may indicate a programming error.
and orFalse
gives an error Cannot prove that Any <:< Unit.
Any guidance on how best to achieve the desired outcome?
I have also tried to validate and then cast the Opts.env
value, then use withDefault("false")
instead of orFalse
for the default fallback value:
...
Opts
.env[String]("WITH_FOO", help = "With foo", metavar = "true||false")
.validate("Value must be Boolean") { (s: String) => Try(s.toBoolean).getOrElse(false) }
.map { _.toBoolean } withDefault ("false")
...
I have a use case where I want to throw an exception (in order to fail an IO) if a particular flag isn't set in a particular context. I want that exception to contain information about how to turn the flag on i.e.
new Exception(s"Failed to Foo, turn on ${Usage.fromOpt(BarOption)}
with the intention that the string look something like Failed to Foo, turn on --bar
. For now I'm using the toString
method, but it's output isn't exactly what I want: Opts([--rewrite]
. Usage itself is private to the module, so I can't really work around this.
I understand why making Usage private to the module would help hide implementation details, but some mechanism of printing --rewrite
seems generally useful to the end-user
It seems that the decline-refined
module has not been published in Maven Central yet. Would it be possible to publish it for Scala 2.11 and 2.12?
We need to remove (or extract) Path
Argument
to make it possible to use it from scalajs. WDYT?
Hey there. Considering that the work was done 5 months ago now. Can we get a release of decline that uses those versions? Thanks in advance.
@bkirwi any objection to adding decline here:
https://github.com/scala-steward-org/repos/blob/master/repos-github.md
It would be nice to keep up to date with dependencies more formally.
An aspect of subcommands perplexes me. When a set of subcommands are composed together using orElse
, there seems to be no way to distinguish which command was fired, if the Opts for commands are the same (eg Int
, Unit
etc).
An example of the problem can be found in ParseSpec. Snippet:
val opts = run orElse clear
opts.parse(List("run", "--foo", "77")) should equal(Valid(Some(77)))
opts.parse(List("clear", "--bar", "16")) should equal(Valid(Some(16)))
Note how there is nothing in the response that tells us whether run
or clear
was the selected command, and since the Opts type Int
is the same for both, that doesn't help.
The solution I propose is an alternate combinator orElseLabelled
that returns Opts[(String, A)]
when composing Command[A]
, the String in the tuple being the name of the command. Happy to help but want to discuss the problem before going to code...
One "workaround" is to wrap each commands Opts in a case class (as in eg #44) but that adds extra overhead.
First off - decline
is the most usable and sensible CLI parsing library in the Scala ecosystem I've encountered to date - so thanks for that! :-)
It would be great if when parsing the library could match on a subcommand when enough letters are specified to uniquely identify the subcommand.
As a toy example, consider re-implementing bits of the kubectl
cli:
lazy val get: Command[Observable[String]] = Command[String](
name = "get",
header = "Get resources from the k8s API"
)(Opts.subcommands(
get_pods,
get_statefulsets,
get_services,
...
))
It would be nice to trigger execution on get po
or get st
or get se
instead of having to spell out the full resource each time.
For example, in the "real" kubectl
, I can do something like kubectl get po
rather than kubectl get pods
. For CLIs that are complex and heavily used it's nice to be able to trim characters wherever possible. It's probably not part of the POSIX standard, but it certainly does improve quality-of-life for end users and seems prevalent enough in the different CLI tools I've encountered over the years.
Could perhaps be implemented as an optional behavior - e.g.: Opts.subcommands(..., partialMatch = true)
I know Opts
is implemented as an Alternative
right now. I won't claim to have thought much about whether or not this parsing change would influence that... gut says probably not, but if the answer is "can't get there from here - algebra laws won't allow it" that's fine too.
Also - this could be related to #52 - may need similar matching logic in place (somewhere) for bash completion. Possible that the partial subcommand logic belongs there instead of as a built-in on the library itself... not sure.
I'd like to use decline from some bazel projects, and I don't want to have to figure out bintray support.
Any chance we can just do maven central?
As a user, I would expect to be able to use ZIO's runtime system, and use its effect types, like Task.
I think these could be done with two improvements:
With this approach, even Monix could be supported by creating a third implementation based on his own runtime system (TaskApp). Some insights on why each IO monad implementation benefits from his own runtime system can be found here.
Let me know what you think about this and if you are interested in supporting ZIO at all, if yes I could try to sketch a PR.
Is there a way or plan to display arguments for subcommands?
I think it would be handy to have them in detail in Subcommands section and in usage part with shorter version.
I hope this is ok to add this info in an issue - I haven't seen any other way to share it with decline users.
Hi I created an example on how to use Decline from within a ZIO App
What do you think about having faster communication channel?
If agreed to set it up then lets remember about badge in readme :)
Currently if you chain subcommands with orElse
you would get Product
type as a result and lose exhaustiveness checks during pattern matching. It would be great to return something like Shapeless Coproduct instead (if this even makes sense?) or something in order to properly type it.
Eg:
case class AOpts(a: Boolean, aa: Int, aaa: String)
case class BOpts(b: String, bb: Int, bbb: Boolean)
val aSub: Opts[AOpts] = Opts.subcommand
val bSub: Opts[BOpts] = Opts.subcommand
// You get product, but ideally it should be something like `[AOpts with BOpts]`
val main: Command[Product] = Command() { aSub orElse bSub }
main.parse() match {
case Left(_) =>
case Right(AOpts(...)) =>
case Right(BOpts(...)) =>
case Right(_) => // mandatory because of the Product type inside
}
I'm running into a performance issue. My app has many options(20 ~ 30) and it takes about 1 minute to parse arguments. It seems the performance is getting worse exponentially according to the number of options. The following is minimal repro:
object App {
import cats.syntax.apply._
def main(args: Array[String]): Unit = {
val N = 20
val command = Command("demo", "") {
(1 to N).map { i =>
Opts.option[String](s"opt$i", "").withDefault(s"$i")
}.reduce { (a, b) => (a, b).mapN(_ + _) }.map(println(_))
}
while (true) {
println("Start")
val s = System.currentTimeMillis()
command.parse(args) match {
case Left(help) => println(help)
case Right(_) =>
}
val e = System.currentTimeMillis()
println(s"Finished took ${e - s}[ms]")
}
}
}
Output (N=18):
Start
123456789101112131415161718
Finished took 1175[ms]
Start
123456789101112131415161718
Finished took 222[ms]
Start
123456789101112131415161718
Finished took 324[ms]
Start
123456789101112131415161718
Finished took 220[ms]
...
Output(N=19):
Start
12345678910111213141516171819
Finished took 1262[ms]
Start
12345678910111213141516171819
Finished took 553[ms]
Start
12345678910111213141516171819
Finished took 548[ms]
Start
12345678910111213141516171819
Finished took 560[ms]
...
Output(N=20):
Start
1234567891011121314151617181920
Finished took 2361[ms]
Start
1234567891011121314151617181920
Finished took 1488[ms]
Start
1234567891011121314151617181920
Finished took 1088[ms]
Start
1234567891011121314151617181920
Finished took 849[ms]
...
Any thoughts?
We have a project on top of scio which is being on top of Apache Beam / Google Dataflow supports only --arg=value
format as opposed to decline's --arg value
. Scio provides its own tooling for argparsing, but I find decline style way more composable, type-safe and just purely awesome.
I was wondering if somebody considered adding an option for equals sign or would it be considered as contribution at all.
I am in a minor dependency hell where I need cats 1.0.0 for something, but decline won't run with cats 1.0.0
I see a 0.4.0-M1 tagged, but I don't see it in maven central. Is there a way I can get it?
What would be the complexity for providing decline for Scala Native?
I've seen that it depends on refined, which I do not understand clearly if would be available for Scala Native or not.
It also depends on ScalaTest, which in general can be easily replaced by uTest.
It also depends on ScalaCheck, which I simply would disable in the case of Scala Native.
Can you provide some guidance on how it could be ported?
Thanks
When using Opts.withDefault
I would expect the given value to appear in the help message. Unfortunately it does not and it is not impossible to add it to an existing Opts
.
val maxConcurrent: Opts[Int] =
Opts.option[Int]("max-concurrent", "Maximum amount of concurrent connections")
val maxConcurrentOrDefault: Opts[Int] = maxConcurrent.withDefault(4)
Ideally I'd wish for a way to modify the help message of an existing Opts
. E.g. something like:
val maxConcurrentOrDefault: Opts[Int] = {
val default = 4
maxConcurrent.withDefault(default).updateHelp(_ + s" [default: $default]")
}
Would you be willing to expose the SubCommand
type the same way as Command
?
It would be very useful to use it instead of its parent type Opts
.
Currently, it's possible to create a command with Opts (arguments, or subcommands), it would be nice to be able to use the same approach for subcommands.
Any thoughts on adding support for user input?
I understand the downside that is, the library will likely have to come with cats-effect
by default, instead of being optional (unless this also was a separate module).
But I think it would be vital to enable complex tooling and allow the user to build complex flows.
A very simple example would be
$ decline-clt diff-push-merge
username: ....
password: ...
Success!
Furthermore, it would enable something like this ticket => #171
To be turned into
$ decline-clt generate-file
Enter File Name: ...
Choose file type:
[.] Class
[.] Trait
[.] Object
Really liking the library. I recently started using it for a project and it's the easiest and most flexible command line parser I've come across.
On feature I'd like to see is a way to group a set of flags together and provide help text for that group. I imagine something like:
(opt1, opt2).mapN(GroupedOptions.apply).groupedAs("Options for debugging")
Then the help generator could group these options together with the extra text.
I just wanted to give Decline a shot. But going through the Get Started gave me:
[109/109] cli.run
Hello world!
nbszmbp013:camundala mpa$ hello-world --help
bash: hello-world: command not found
I clearly miss a step. But there is no clue in the Get Started of the Documentation.
What do I miss here?
https://github.com/lightbend/migration-manager#usage
would be nice to make sure we don't let compatibility issues creep in.
Hi! I just started using decline and it's looking pretty cool.
Did you consider supporting cats-effect style effects? So that there'd be a variant of CommandApp
like CommandApp[Task]
, whose main
would have to return a Task[Unit]
.
Additionally, if Opts
had a Traverse
instance (didn't think about if it's possible yet, but it should be), we could validate them with effects - let's say I have an Opts[Path]
and want to make sure that the file exists - which is an IO operation, and I'd rather do it in an effect.
Of course I'm willing to help :)
What do you think?
Thanks!
The license still has placeholders for copyright year and owner. Can you please update them?
When subcommands are composed under a top command, the resultant parser wont accept the top command. See ammonite REPL session for example:
Welcome to the Ammonite Repl 1.4.2
(Scala 2.12.7 Java 11.0.1)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ import $ivy.`com.monovore:decline_2.12:0.6.2`
import $ivy.$
@ import cats.implicits._
import cats.implicits._
@ import com.monovore.decline._
import com.monovore.decline._
@
Command("top", "top")(Opts.subcommand(Command("Sub1", "sub1")(Opts.unit)).orElse(Opts.subcommand(Command("Sub2", "sub2")(Opts.unit)))
)
res3: Command[Unit] = com.monovore.decline.Command@5ab5924c
@ res3.showHelp
res4: String = """Usage:
top Sub1
top Sub2
top
Options and flags:
--help
Display this help text.
...
@ res3.parse(List("top", "Sub1"))
res5: Either[Help, Unit] = Left(
Help(
List("Unexpected argument: top"),
NonEmptyList("top", List()),
List("Sub1", "Sub2"),
List(
"top",
"""Options and flags:
--help
...
I'd expect that input List("top", "Sub1")
should be parsed to Right(Unit)
. That's shown as valid input in the generated help text.
Decline provides the product
combinator to compose multiple subcommands in a row, but it appears the parser has an internal assumption that there will be just one subcommand.
In this example, we construct a Opts for two parallel subcommands, but the parser is unable to parse the corresponding input.
Welcome to the Ammonite Repl 1.4.2
(Scala 2.12.7 Java 11.0.1)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ import $ivy.`com.monovore:decline_2.12:0.6.2`
import $ivy.$
@ import cats.implicits._
import cats.implicits._
@ import com.monovore.decline._
import com.monovore.decline._
@ import cats._
import cats._
@ val nel = Apply[Opts].product(Opts.subcommand("FooInt", "fint")(Opts.argument[Int]("n")), Opts.subcommand("FooS", "foos")(Opts.argument[String]("s")))
nel: Opts[(Int, String)] = App(
App(Pure(cats.Apply$$Lambda$1767/0x0000000800a07040@636e4bf8), Subcommand(com.monovore.decline.Command@302da330)),
Subcommand(com.monovore.decline.Command@3568ea59)
)
@ val cmd = Command("test", "test")(nel)
cmd: Command[(Int, String)] = com.monovore.decline.Command@30bd39d5
@ cmd.parse(List("FooInt", "6", "FooS", "s" ))
res6: Either[Help, (Int, String)] = Left(
Help(
List("Unexpected argument: FooS"),
NonEmptyList("test", List("FooInt")),
List("<n>"),
List(
"fint",
"""Options and flags:
--help
Display this help text."""
)
)
)
It seems like there is an assumption in the parser logic that there is just one subcommand, although the Opts
algebra seems to allow it.
Any chance of a 0.6.1 release?
I'd love to be able to use the Argument[UUID] I added in #51 . No rush though, this is mostly for tracking purposes.
Opts.option
s that are used in separate branches of, e.g., an orElse
only appear once in the help output, but Opts.env
s are repeated in the "Environment Variables" section.
This (nonsensical) sample:
import cats.implicits._
import com.monovore.decline.{CommandApp, Opts}
object TestApp extends CommandApp(
name = "hello-world",
header = "Says hello!",
main = {
val userOpt =
Opts.option[String]("target", help = "Person to greet.") orElse
Opts.env[String]("TARGET", help = "Person to greet.").withDefault("world")
val quietOpt = Opts.flag("quiet", help = "Whether to be quiet.")
val opts: Opts[String] = (quietOpt, userOpt).mapN((_, u) => u) orElse userOpt
opts.map { u =>
println(s"Hello $u!")
}
}) {
}
produces the following when run with --help
:
Usage: hello-world [--quiet [--target <string>] | --target <string>]
Says hello!
Options and flags:
--help
Display this help text.
--quiet
Whether to be quiet.
--target <string>
Person to greet.
Environment Variables:
TARGET=<string>
Person to greet.
TARGET=<string>
Person to greet.
I've never really looked into it in earnest, but it feels like it could be cool to generate a bash autocomplete file from a supplied com.monovore.decline.Command
. This functionality feels like it would be a separate module though.
We could definitely have Apply[Argument]
it seems (we don't have pure because can't supply a defaultMetavar) if we take the "left most" or "right most" defaultMetavar.
We can certainly have Functor[Argument]
even without the above constraint.
But if we would allow the above (say take the left side). I think we can also have Alternative[Argument]
which can be useful to compose different Arguments.
One thing I like is an
def either[A, B](a: Argument[A], b: Argument[B]): Argument[Either[A, B]] =
Alternative[Argument].combineK(b.map(Right(_)), a.map(Left(_)))
This allows a single flag like --input
to parse multi values (say uris but also local file paths)
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.