bkirwi / decline

A composable command-line parser for Scala.
http://monovore.com/decline/
Apache License 2.0
647 stars 71 forks source link

Document how to use Decline with Ammonite (specifically manually passing args) #270

Open mdedetrich opened 3 years ago

mdedetrich commented 3 years ago

So I am trying to use decline within Ammonite and there doesn't seem to be a good uesr guide on how to do this. From the docs on Ammonite, i.e. https://ammonite.io/#ScriptArguments you can access the command line arguments by doing the following

@main
def entrypoint(args: String*) = {

}

Assuming you have a an app MyApp as follows (taken from example)

object MyApp extends CommandApp(
  name = "my-app",
  header = "This compiles to JavaScript!",
  main = {
    val loudOpt = Opts.flag("loud", "Do something noisy!").orFalse

    for (loud <- loudOpt) yield {
      if (loud) println("HELLO WORLD!")
      else println("hello world!")
    }
  }
)

You would do something like this

@main
def entrypoint(args: String*) = {
  MyApp.main(args.toArray)
}

The problem is that the main method in CommandApp is apparently deprecated, i.e.

  @deprecated(
    """
The CommandApp.main method is not intended to be called by user code.
For suggested usage, see: http://monovore.com/decline/usage.html#defining-an-application""",
    "0.3.0"
  )
  final def main(args: Array[String]): Unit =
    command.parse(PlatformApp.ambientArgs getOrElse args, sys.env) match {
      case Left(help) => System.err.println(help)
      case Right(_) => ()
    }

Ontop of this the command variable inside the CommandApp class is not publicly visible outside the class (its missing the val modifier in the constructor) so its not like you can manually do MyApp.command.parse (should this deprecated method main be removed in the future). So how exactly is one meant to manually parse args to a CommandApp without using the deprecated main method? For example https://ben.kirw.in/decline/scalajs.html#ambient-arguments details how to get the args on various platforms but never how to pass it into a CommandApp and the provided link at https://ben.kirw.in/decline/usage.html#defining-an-application details how you can manually use args when you have a Command but not when you have a CommandApp (this goes to the previous problem where the command inside the CommandApp is not publicly visible).

Conclusively I guess I also don't understand why the CommandApp.main method is deprecated, if you are using something like Ammonite it is by far the easiest way to make the repl/scripts work with an existing CommandApp (which is the ideal way to use decline). This would also be the case for any other kind of dynamic repl/interactive session where args are passed in a different way.

bkirwi commented 3 years ago

So how exactly is one meant to manually parse args to a CommandApp without using the deprecated main method?

The idea, at least, is that main is expected to be called by the JVM and not by user code. This lets us make choices that are useful for an entry point but bad for regular code. (For example, the CommandIOApp version will sys.exit(1) when passed invalid args.)

Concretely, if your code is intended to just be used from Ammonite, I'd skip the CommandApp and just use a command (and command.parse) directly. For cases where a command is used in multiple places (as an app and in tests, for example) folks will often store it in a constant on an object somewhere. I don't have much to recommend in the context of Ammonite specifically... this is the first I've heard of decline being used with Ammonite in this way.

mdedetrich commented 3 years ago

So the reason I am using Decline in Ammonite is that even though Ammonite has its own functionality for command line parsing which works great for more trivial use cases, Decline is much better for complex cases (i.e. creating Argument for custom types) and the script that I was working on in Ammonite ended up being more complex than I thought.

I guess the thing is that Ammonite does actually expose a main (as you can obviously see in my code) and more generally I don't think a case that in all cases the main is only the standard JVM main (i.e. Scala becoming more portable to other platforms such as Scala.js/native and even other cases as well).

I understand that you can use Command instead of CommandApp but that sought of defeats the main point of CommandApp which is meant to be used as a single global main which is my use case here, its just that the main is being provided by Ammonite's repl session rather than a standard JVM main.

Do you have a strong objection to removing the deprecation and just documenting clearly how its meant to be used?

bkirwi commented 3 years ago

I guess the thing is that Ammonite does actually expose a main (as you can obviously see in my code) and more generally I don't think a case that in all cases the main is only the standard JVM main (i.e. Scala becoming more portable to other platforms such as Scala.js/native and even other cases as well).

So JS is actually a great example, because that is supported by CommandApp, but in a fairly surprising way. ScalaJS will always pass an empty array of arguments, so we ignore the input array and go rummaging around in the nodejs stdlib to find the "real" arguments. This would be terrible behaviour for a library function, but we can get away with it since we know exactly in which context main will be called.

Do you have a strong objection to removing the deprecation and just documenting clearly how its meant to be used?

Unfortunately, yes! In part because we used to do this and it was a significant maintenance burden, and in part because we need to make strong assumptions about the calling context of main so we can do stuff that would otherwise be inappropriate.

Turning this the other way... I would imagine that the way to do this without *App in ammonite would look something like:


val app = Command(...) {
 ...
}

@main
def entrypoint(args: String*) = {
  command.parse(args) match {
    case Left(help) => exit(help)
    case Right(_) =>
  }
}

Does that work in your case, or is there some important functionality that's not available outside of CommandApp?

mdedetrich commented 2 years ago

Sorry for the late response but I came across this problem in another context which alludes to your point

Unfortunately, yes! In part because we used to do this and it was a significant maintenance burden, and in part because we need to make strong assumptions about the calling context of main so we can do stuff that would otherwise be inappropriate.

I have this situation which is that I actually want to test a Main extends CommandApp application and by testing Main I don't mean only testing the Command part of the App (i.e. the parsing) but also the initialization of App along with the Command part.

I understand there may some deliberation when it comes to the ideal way to test an the Main entry point but In my case I just want to do a very quick litmus/"smoke" test where I pass in some arguments into CommandApp.main and see that the App properly initializes along with catching any exception that happens to be thrown and at least from what I can tell by far the simplest and easiest way to test this is by using CommandApp.main.