c-blake / cligen

Nim library to infer/generate command-line-interfaces / option / argument parsing; Docs at
https://c-blake.github.io/cligen/
ISC License
501 stars 23 forks source link

Ho do I make a parameter an argument instead of an option? #174

Closed halloleo closed 3 years ago

halloleo commented 3 years ago

I have the following proc signature:

proc main(cmd: string, path: string):string =

and I expect cmd and path to be (required) arguments, but --help reveals they are options.

How can I tell cligen, that cmd and path are arguments?

SolitudeSF commented 3 years ago

cligen can only collect freestanding arguments into seq type and doesnt have a way to enforce their quantity. so you have to use args: seq[string] and enforce and extract 2 arguments yourself.

c-blake commented 3 years ago

They are mandatory/required options, at least (since there is no explicit default value). The workaround I personally use is exactly as @SolitudeSF describes. It's not much work, especially for string parameters as here.


This problem arises because Nim call syntax is more flexible than traditional CLI syntax. Nim can call either positionally or via name while CLIs pick one or the other. The first 3 years of cligen's existence had a different API<->CLI mapping that was more towards this positional for non-explicit defaults, but @timotheecour in https://github.com/c-blake/cligen/issues/20 persuaded me to change it (and then asked for it back as non-default 7 months later). (Nim has probably changed enough by now that just checking out an ancient cligen version might be less helpful to see how it used to work, but the VC history of test/ref and test/Mandatory.nim might help.)

cligen is past 1.0 now, though, and so I would steer away from going back to that. Just information for the interested.


Since the workaround here is pretty easy, and since there's already a TODO.md item (the 2nd paragraph), I am closing this issue. PRs welcome if anyone feels it important and wants to try.

To elaborate a little, the idea in the use case here would be dispatch(main, positionals = ["cmd", "path"]) or some such, maybe with a magic/keyword name rest to capture an optional variable length seq after the initial positionals.

There might be a bit of a mapping conflict/complexity if any of the named positionals had any explicit defaults in the Nim signature. Such could become "optional positional" parameters. The mapping is not so bad if in the Nim signature the "tail" is all defaulted, but if, say, only cmd but not path were defaulted it becomes a mess. C++ blocks such cases with its own optional positionals, for example, but flexible Nim does not. Anyway, I think non-trailing-only-defaults could maybe go unsupported, raising some error. The cligen.dispatch caller would already be manually listing names in this scenario..So, we could just say something like "non-contiguous defaulting tail" or something. That aforementioned rest could still be used for everything after cmd with manual work, just like the workaround mentioned at the top. So, depending upon what Nim API is being wrapped it might look like "no progress" had really been made and only complexity added. The utility mostly comes down to how common tail-only default chains are.

halloleo commented 3 years ago

Hey @SolitudeSF and @c-blake! Thanks a lot for the details.

Coming from Python (having used argh!) I do think it would be nice to have positional arguments via the signature somehow, but I can see that required options make sense too. And a positionals = ["cmd", "path"] construct is similar to the decorator in Python's click library...

For now I will go with the seq approach. - Thanks again for the information!

halloleo commented 3 years ago

BTW @c-blake, you are mentioning a positionals parameter to dispatch.

In the documenation I find the parameter positional = AUTO.

Is that something different? Was can it do? Do the parameters have somewhere some documentation?

c-blake commented 3 years ago

From the documentation you link to (I know it's big. cligen has a lot of features):

By default, cligen maps the first non-defaulted seq[] proc parameter
to any non-option/positional command args. positional selects another.
Set positional to the empty string ("") to disable this entirely.

So, AUTO is the default where cligen infers the first non-explicitly-defaulted seq to be what the variable number of positional CL args map to. You can set it to something else. As with many things in cligen, grep'ing/searching for the thing in test/ yields an example or two. In this case a good query string is grep "positional *=".

halloleo commented 3 years ago

Hey, thanks for the details. It's all there. - I just didn't find it.

gpanders commented 3 years ago

Sorry to resurrect an old issue, but when using the workaround that @SolitudeSF described:

cligen can only collect freestanding arguments into seq type and doesnt have a way to enforce their quantity. so you have to use args: seq[string] and enforce and extract 2 arguments yourself.

is it possible to have cligen show the help text if there aren't enough positional parameters? e.g.

proc foo(args: seq[string]) =
  if len(args) < 1:
    ????

Is there anything I can put in ???? above that would tell cligen that this is an invalid invocation? I know I could just print a message with echo, but I'd prefer to print the usage information.

c-blake commented 3 years ago

Sure. Just raise HelpOnly. You will have to have the import cligen before the wrapped proc to do that, though. You can add whatever supporting error text as well, obviously.

c-blake commented 3 years ago

(That will go to stdout..you need to raise ParseError for stderr/exit 1.)

gpanders commented 3 years ago

Thanks for the quick response. I added the following:

if args.len == 0:
  raise newException(HelpOnly, "")

but when I omit the positional arguments I don't see any output at all. Here's a minimal example that I can reproduce with:

import cligen

proc foo(args: seq[string]) =
  if args.len == 0:
    raise newException(HelpOnly, "")

  echo args.len

if isMainModule:
  dispatch(foo)

If I compile this and run

./test

I don't see anything, but if I run

./test hello world

I see 2 (as expected).

c-blake commented 3 years ago

Hrm. Let me look into it. For now I suggest:

import cligen
proc foo(args: seq[string]) =
  if args.len < 2:
    stderr.write "Need at least 2 args; Run with --help for more info.\n"
    raise newException(ParseError, "")
  echo args.len
if isMainModule: dispatch(foo)
c-blake commented 3 years ago

(This is roughly how other parse errors in cligen work like ./foo --noSuchOption...So, it may be all you need.)

c-blake commented 3 years ago

So, the exception is only used for control flow in cligenQuit. The help message is built up in dispatchfoo in a local variable ap.help.

If you really need this, I could copy that local into some global like cgLastHelp just before calling foo. Then foo could dump it to either stdout or stderr as desired.

gpanders commented 3 years ago

The solution you gave above works well enough for me, thanks.

c-blake commented 3 years ago

For what it's worth, I used to dump full help on syntax errors, but I think people complained. { Or maybe just my own brain complained. :-) }

c-blake commented 2 years ago

Readers of this issue might also care about the new HelpError exception. See test/UserError.nim for usage.