alexarchambault / case-app

Type-level & seamless command-line argument parsing for Scala
https://alexarchambault.github.io/case-app/
Other
296 stars 44 forks source link

Positional arguments, parsing scaladocs #58

Open ryan-williams opened 7 years ago

ryan-williams commented 7 years ago

I love this library, thank you!

Some things I would like to see that afaict it doesn't have now:

I may try to dig in to this when I have a moment, will post any notable updates here

alexarchambault commented 7 years ago

Sorry to answer that a bit late...

Maybe auto-recursing can be only enabled via an import. Then I'd fine to merge it I think.

ryan-williams commented 7 years ago

Positional Arguments

What do you mean by "positional arguments"? :-)

Arguments that are not associated with a flag, that currently go in RemainingArgs.remainingArgs afaict.

For example, file1 and file2 from man comm:

SYNOPSIS
     comm [-123i] file1 file2

Python argparse treats options that don't start with - as positional, afaict.

I'm imagining an annotation like @Arg(idx = …), where if the idx argument to @Arg is not provided, it can be inferred from the order of the @Arg-annotated fields.

comm example

Modeling comm's CLI above:

import caseapp.{_, ExtraName => Opt}
case class Args(
  @Opt("1") suppressCol1,
  @Opt("2") suppressCol2,
  @Opt("3") suppressCol3,
  @Opt("i") caseInsensitive,
  @Arg file1,
  @Arg file2
)

object Comm extends CaseApp[Args] {
  def run(args: Args): Unit = {
    println(s"Comparing ${args.file1} to ${args.file2}…")
  }
}

This example infers each positional argument's position from its order in the case class: file1 is the first arg, file2 is the second; note also that def run(…) no longer takes a RemainingArgs… that's basically always what I want (no unparsed args allowed, and positional args folded into the Parser machinery), though a version that takes unparsed arguments can be optionally available as well if that's what others want.

The other option (which could also co-exist with the above) is that the positions are explicitly provided:

case class Args(
  @Opt("1") suppressCol1,
  @Opt("2") suppressCol2,
  @Opt("3") suppressCol3,
  @Opt("i") caseInsensitive,
  @Arg(0) file1,
  @Arg(1) file2
)

this seems only useful if you want to be able to do something like:

case class Args(
  @Opt("1") suppressCol1,
  @Opt("2") suppressCol2,
  @Opt("3") suppressCol3,
  @Opt("i") caseInsensitive,
  @Arg(1) file2,
  @Arg(0) file1
)

where the order has been switched.

My gut is that this just shouldn't be allowed and the positions should be inferred from the field ordering as in the first example.

Implementation thoughts

It seems like an extra AnnotationOptions type can be plumbed through the Parser machinery to do this without too much trouble.

Ideally you would get errors if a field has @Opt and @Arg annotations, and I think this approach would let that happen at compile time?

So I've been imagining I could hack this up when I have an hour or two; if there are issues with this approach let me know!

Parsing scaladocs

It would be cool to get some infos from the scaladoc, I'm not sure that's straightforward though (maybe with scalameta?).

Yea this one is just me hand-waving bc I keep hearing things about scalameta parsing scaladocs.

I would tackle the positional args feature first but at some point we should try to loop in the scalameta ppl for help/advice with this bc I think it would be an amazing feature.

My coworkers make https://github.com/hammerlab/ppx_deriving_cmdliner which is a pretty fully-realized ocaml version of what I would love this library to become, and it has this feature and is magical.

Auto-recursing

Thanks for the context on that; I'm imagining that a case class will always be parsed as a product of fields… leaving both options available sounds fine though.

ContextParser

Over the weekend I made a parallel version of the Parser/ArgParser/HListParser stack that takes an arbitrary Context instance which must be implicitly present during each fields arg-parsing: https://github.com/alexarchambault/case-app/compare/e00f40c...hammerlab:ctx

The impetus was that I had some field types where I wanted to parse them / instantiate them in the presence of a hadoop Configuration object.

I got it all working but then ultimately decided to refactor my code to not have these types / to use them differently so that I no longer needed this feature, but just wanted to mention it in case it is interesting. It was my first hands-on project with some of this shapeless magic so it was a valuable learning experience for me 😎.

alexarchambault commented 7 years ago

@Arg looks cool! With or without the index as param (Like you commented, I'm not sure the param would be that useful either).

My understanding is that these are likely to be required arguments. Beware that there's currently a small glitch in the handling of mandatory arguments in case-app - the help cannot be printed if the mandatory arguments are not specified (like --help says --mandatory required, and one has to do --help --mandatory foo to actually print the help). I might have a fix for it... I'll push it if it works.

ryan-williams commented 7 years ago

Good to know!

Just in case I'm missing something that case-app already handles, what do you mean "mandatory arguments"? I'm assuming you mean fields in the arguments-case-class that have no default value provided and therefore must be specified on the CLI in order for the arguments-case-class to be instantiated?

alexarchambault commented 7 years ago

Yes, that's what I mean. It's quite recent, it's been added in 1.2.0-M1.

Also, forget my point about the issue with help and these mandatory args, https://github.com/alexarchambault/case-app/pull/63 fixes it :-)

ryan-williams commented 7 years ago

Random other question I asked in the case-app gitter but I'll repost here since that room doesn't seem active yet:

I'm trying to port some args4j-based CLI param objects to case-app and one issue I'm facing is that I have sets of arguments defined in traits that I mix in to multiple different apps.

I'm wondering whether/how i can mimic that with case-app; as long as a shapeless Generic is available for my args type, I'll be fine, right? Any good ways to make a Generic available for a trait, based on its fields? I'll think about this more and ask in the shapeless room if I don't hear/decide otherwise.

alexarchambault commented 7 years ago

@ryan-williams A LabelledGeneric should be enough (LabelledGeneric itself requiring Generic and DefaultSymbolicLabelling). Yeah, you should be able to find help on the shapeless room.

nightscape commented 5 years ago

The Scaladoc parsing could be done using https://github.com/takezoe/runtime-scaladoc-reader I'm using it in a project and it works quite well 👍

chadselph commented 4 years ago

I can't seem to figure out positional args. Is it documented somewhere?

joprice commented 4 years ago

@chadselph Seems like positional arguments still go to RemainingArgs. No change has been made to support the @Arg proposal right @alexarchambault?

chadselph commented 4 years ago

I think that's right. I've been using RemainingArgs where relevant but since it only supports String types, I have a mix where validation/type conversion is happening between options and args even though they're doing the same thing.