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

How can I define a CLI with exactly one required argument without option-dashes #160

Closed halloleo closed 3 years ago

halloleo commented 3 years ago

Thanks for cligen! It looks very promising to me. (In Python I have used argh for quite a while very happily.)

But I have some problems: I try to define a program which accepts one required argument and this argument should not be an option-dash parameter. So I want a CLI program (let's call it tool) which I can call exactly like this:

tool myfile

I tried the Nim code

proc main(files: string): int =
  ...
dispatch(main)

but this gives me a tool I have to call with tool --file=myfile.txt.

Then I tried

proc main(files: seq[string]): int =

but this tool I can call without any argument which I don't want. It should error out, beacuse file should be required.

How can I define the signature of main for this?

c-blake commented 3 years ago

There isn't a way to specify this exactly, but you can get a close approximation like this (side note: if you put Nim after the triple backtick github will highlight the code for you):

import cligen
proc main(file: seq[string]): int =
  ## What this program does
  if file.len != 1:
    raise newException(ValueError, "Need exactly one file")
dispatch(main, usage="$command {file}\n${doc}Options:\n$options")

The sense in which it is an approximation is that the generated help message will say [file: string...] seeming to imply that 0..n files are possible only to be blocked by the run-time unless you ammend it to just {file} as I show above. Sometimes people say <file>.

Of course if you add other options you'll want to add some help={ "newParam1": "what newParam1 does" } after that usage=.

halloleo commented 3 years ago

Thanks a lot. :-) So there's no built-in way. But the workaround is cool.

I guess, I need this workaround for "At least 1 argument" as well. Then I have to write:

  if file.len < 1:
    raise...

Correct?

halloleo commented 3 years ago

One problem though occurs: dispatch does not seem to wrap the raised newException. The user gets a stack trace:

/home/halloleo/nim/mytool.nim(32) mytool
/home/halloleo/.nimble/pkgs/cligen-1.2.0/cligen.nim(741) cligenDoNotCollideWithGlobalVar
/home/halloleo/.nimble/pkgs/cligen-1.2.0/cligen.nim(659) dispatchmain
/home/halloleo/nim/mytool.nim(21) main
Error: unhandled exception: Need at least one file [ValueError]

Can I raise another exception, so that cligen handles it?

c-blake commented 3 years ago

No problem. And yes, you are correct about >= 1 argument.

And to suppress the stack trace you can raise ParseError instead, but you have to write your own error message. Like

stderr.write "Need exactly one file; Run with --help for more info\n"
raise newException(ParseError, "")

Oh, and end CL-users are able to override your usage template in a $HOME/.config/cligen file to say whatever they want by default. This is to allow almost any formatting of help that such end users want, but really they could make the help say anything.

The contact of that ability with this use case of yours is that the generated $args used in such templates will remain [file: string...] because that is inferred from the seq[string] type. The config file will just be for all cligen programs and so should use $args.

Now, [file: string...] is not so misleading, of course, but if it is truly hated then the best long-term idea might be to add a new named parameter argsHelp="{file}" to dispatch to override the inferred $args. That's already how $command and $doc work. It is also already possible today (by defining you own config file parser) to disable the ability to override that one [templates]/usage field, but that is pretty awkward.

Also, I would be remiss if I didn't point out that with only string command arguments and no options that you could also just use os.paramCount and os.paramStr and not use cligen at all. It's only barely doing any real work for you in these cases.

c-blake commented 3 years ago

Oh, and I should maybe have mentioned that cligen translates that ParseError exception into an exit status of 1. Any non-zero is meant to indicate failure. So, on a Unix shell you could check the shell $? variable or say if myprogram; then echo worked; else echo failed; fi.

halloleo commented 3 years ago

Very detailed! Thanks a lot! Will try it all out tomorrow.

c-blake commented 3 years ago

By the way, I assume from the title of the issue that you know that just saying proc main(file: string) (with no default value) leads to the "option syntax" variation of this.

I mention this in case I was misled by such from-Title-reasoning. foo -fpath isn't so hard to type, but I get why you might not want to.

Before this commit I had a different mapping more to your particular use case where things like string -> integer/float/enum/etc. conversion would still have been automatic. The 2nd item in the current TODO.md refers to maybe getting back some of that, but hopefully you are good for now.