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

Constants in dispatch's `help` dictionary parameter are not expanded #177

Closed halloleo closed 3 years ago

halloleo commented 3 years ago

When I call dispatch the following way:

const args_info = ... # computed from an array
dispatch(mycmd,  help = { "args": args_info })

and I run mycmd --help the output contains as the arguments the string "args_info" (i.e. not the content of args_info but the characters "args_info").

c-blake commented 3 years ago

I think it can work presently if you can pass the entire help table as a value as in test/PassValues.nim.

I admit this could be better than it is. When I have had some sleep, I will look into seeing how complex it would be to make it work how you are trying with just the V in {K:V} being a compile-time value. But let me know if doing the whole table like test/PassValues.nim is enough for you.

halloleo commented 3 years ago

I'll certainly try this. Thanks.

I just don't really understand why this problem occurs at all...My Nim basics are pretty shaky: Can table/dictionary structures not contain of variables? Does this happen because dispatch is a template and not a proc? Sorry, I guess this is probably a question better for a forum or similar...

c-blake commented 3 years ago

Well, I can try to explain a little but if this is not enough then you should read the tutorial on Nim macro meta programming and dumpTree and then come back here and read this again. :)

When you write a macro like dispatchGen, it receives - at compile time - an abstract syntax tree representation of the program. I.e., the code "on the inside" of the macro parameter list is parsed but not yet compiled and delivered to the macro body parsed. The macro lets you intercede in the translation process by using the input AST to create an output AST.

In the beginning cligen needed string literals or {} "alist literals" (they are not really tables, but pure syntax such that {K1:V1, K2:V2} === [(K1,V1), (K2,V2)] and "alist" = "association list" is ancient Lisp terminology). but then because of https://github.com/c-blake/cligen/issues/110 I added a bunch of "forks/switches" where I test the input AST for either literalhood or symbolhood and get the string value cligen needs to generate its help messages (really the whole CL parser-validator-helper) two different ways. I just check .kind == nnkSym and if it's that I can getImpl on the symbol, but otherwise if it's a string literal I get strVal. That kind of thing. With macros you are sort of "creating syntax" or at least re-interpreting syntax.

So, I think I could do the same thing I did there, but I'd need to loop over each Vi and check if it is a literal or a symbol and extracting the needed value for the UI gets that much more complex. It's kind of a hassle (especially if I do it for both the help={} and short={} dictionaries). So, if doing the entire table like test/PassValue.nim can work for you, I'd prefer to just have you do that and close this issue. That const value that you create with toTable is all done before the cligen macro do anything. So, Nim proper has made it "a bit nicer" to use.

For example, even if I did this, complex expressions like a & b concatenating strings would still not work "inline" in the { } constructor, but it would in the earlier const construct. It's also simpler to just tell people "literal or symbol" for the whole top-level parameter. (Not that I document this well...)

c-blake commented 3 years ago

Since I haven't heard back from you but have checked that this works, I am closing this issue:

import cligen, strutils, tables
proc mycmd(args: seq[string]) = discard
const help = { "args": join(["a", "b", "c"], " ")}.toTable
dispatch(mycmd,  help=help)

As I mentioned, making what you tried first work directly is probably do-able, but also increases explanation burden on the whole Nim API to UI generation. You could also layer that "args" value with an earlier const if you like.

A maybe simpler way to explain the complexity you seemed confused about is that I think "binding to a symbol" and later doing getImpl is probably the easiest to do "compile-time eval" with full expression generality. ("easiness" is often subjective). It also encourages cleaner client code in this case (IMO). A corollary of this is that we could probably bind constants internally inside cligen stuff for each macro/template parameter, and then pass these to a deeper macro that just always does the getImpl. If anyone wants to try a PR, I think it would be an good, non-trivial exercise for someone trying to learn Nim macro programming.

Full compile-time-expressional generality has not been requested much in the 5 years of cligens existence, though, and I have never felt the need in my own programs which include several dozens of use cases. E.g., for your args case I would probably say cmdPath: seq[string] instead of args: seq[string] and then help["args"] is almost superfluous (but still nice!).

halloleo commented 3 years ago

Thanks for all the details and sorry for the delay!

Have read through your explanations. They makes sense, but I still need to get my head around this (very interesting) compile-time pre-execution model...

And I will try your suggestion begin of the week when I'm back at my Nim machine.

halloleo commented 3 years ago

Yep, this works as advertised, @c-blake. For me that's good enough. :smile: