c-blake / cligen

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

[Feature request] Support for std/options #212

Closed aMOPel closed 2 years ago

aMOPel commented 2 years ago

Incredibly handy library. Thank you a lot.

What do you think about integrating std/options into the current api?

Atm, when you want to express that an argument is optional, you have to give it a default value. But some types don't offer a good default value, which is devoid of meaning. Eg enums.

type Foo = enum
  Bar
  Baz
  Bax

proc foo(optionalArgWithTypeFoo = Bar): int =
  # but how do I distiguisch if the default value was passed by the user or the program?
  1
# to solve this I would have to create a dummy value in my enum or make `optionalArgWithTypeFoo = ""` or something 
# and then convert to my enum in the function. 

This issue gets even worse, when you're using the full range of a type.

proc foo(optionalArgWithTypeInt = 0): int =
  # but I want to accept every possible int including 0 for `optionalArgWithTypeInt`. This is unsolvable.
  1

with std/options you could express optional values like this:

proc foo(optionalArgWithTypeInt: Option[int]): int =
  1
# or like this
proc foo(optionalArgWithTypeInt = none int): int =
  1
c-blake commented 2 years ago

You're welcome. In my experience, good default values do "usually" exist and Nim already has the simpler routine(a=b) abilities and the Number 1 rule of cligen is to not burden common case users with concerns of the perfectly general. Various feedback has indicated that this is the core driver of its popularity/user-base.

If you just want a very smart/fancy CLI that has the abilities to solve your "unsolvables" then you may want to take a good look at test/ParseOnly.nim. This is much less convenient to use/requires more work. However, it gives a few superpowers that ordinary Nim proc invocations do not have (whether the user passed CL-arguments that happen to equal default values and even in what order they were issued, if that winds up mattering).

If you are ok with your final = none int syntax then it might be/(should be?) already feasible to also just do your own argcvt.nim-like converters for Option[T] types. I don't personally use these much/ever, but if you come up with some good new argcvt material then I am happy to merge a PR.

I am closing this since I am sure that at least parseOnly can solve the concrete use cases you mentioned (at some CLauthor work), but I am very open to discussing adding Option[T] converters to cligen/argcvt.nim.

c-blake commented 2 years ago

I think by doing things at the argcvt-level, you also allow CLauthors to decide (perhaps on a per wrapped T type Option[T] basis) if a parse error of CL input yields a visible user exception/error message or a silent "as if none" follow-on. { Yes, that may not often be the best idea, but its possibility might be desired flexibility for someone else..Maybe "warn & continue". }

I should also have said that if you do figure out an argcvt-style way to make your Option[T] types work, and for whatever reason do not want to submit a PR, then we can put it on the github Wiki or if you just tell me with some examples I can try to do it. I'm just in the middle of something else at this very moment.

c-blake commented 2 years ago

So, for example, this (sort of) works:

import std/options

# proc foo*(optionalInt = option(1)): int = # also works
proc foo*(optionalInt = none int, neededFloat: float): int =
  echo optionalInt

when isMainModule:
  import cligen, cligen/argcvt
  proc argParse[T](dst: var Option[T], dfl: Option[T],
                   a: var ArgcvtParams): bool =
    var uw: T           # An unwrapped value
    if argParse(uw, (if dfl.isSome: dfl.get else: uw), a):
      dst = option(uw)
      return true

  proc argHelp[T](dfl: Option[T], a: var ArgcvtParams): seq[string] =
    @[a.argKeys, $T, (if dfl.isSome: $dfl.get else: "NONE")]

  dispatch foo

It probably has some trouble with parameters that are other special wrapped generics like set[T] at least in terms of help message consistency.

Also, possibly simpler for you depending upon your use cases, as in the above example with neededFloat, if you do not syntactically put a default value (with "= something") in your proc signature then it becomes a required parameter. This means the user must enter something for that parameter key to get a CL to parse (though it could be the short/1-letter variant). So, it is also sort of a solution to "no way to assign a meaningful default" (in both Nim and at the command-line, I believe).

c-blake commented 2 years ago

Oh, and one other way to "detect setting by the user" that is simpler than my first mentioned parseOnly is your own argParse that references a global variable defined before your wrapped proc like this:

import cligen, cligen/argcvt

var setXyz = false

proc argParse(dst: var int, dfl: int, a: var ArgcvtParams): bool =
  if a.parNm == "xyz": setXyz = true
  argcvt.argParse dst, dfl, a # module qualify to not recurse

proc foo(xyz=0, abc=2) =
  if setXyz: echo "user set Xyz"
  else     : echo "user let Xyz default"

dispatch foo

test/FancyRepeats.nim and test/FancyRepeats2.nim have some other use cases of that. So, you really have a few choices.

aMOPel commented 2 years ago

Thank you. I will look into it. I didn't dive into the argcvt API yet, but the snippet looks very promising.

aMOPel commented 2 years ago
  import cligen, cligen/argcvt
  proc argParse[T](dst: var Option[T], dfl: Option[T],
                   a: var ArgcvtParams): bool =
    var uw: T           # An unwrapped value
    if argParse(uw, (if dfl.isSome: dfl.get else: uw), a):
      dst = option(uw)
      return true

  proc argHelp[T](dfl: Option[T], a: var ArgcvtParams): seq[string] =
    @[a.argKeys, $T, (if dfl.isSome: $dfl.get else: "NONE")]

This works pretty well. The help messages can get a little inconsistent, when wrapping a wrapper type, as you pointed out. I tried it with none seq[string] and got -n=, --newValue= seq[string] NONE set newValue compared to just seq[string] with default @[""] -n=, --newValue= strings "" append 1 val to newValue

It's good enough for my purposes, though. Thank you again.

c-blake commented 2 years ago

You're welcome. Much of cligen makes "good enough - given how automatic" trade offs.

The way you are using it, the default value is not really @[""] anyway, right? The sense of this Option[T] construction is to "hide the real default" from cligen by putting it in the logic of the proc body instead of the signature. The way to tell CLusers what "unspecified" means would be in the help string for such parameters instead of the default value column. Maybe "?" would have been a better choice than "NONE" to emphasize a "hidden if"? { In theory, we could issue a compile-time warning on not providing a help["thatParam"].. }

I did just try it out with an Option[seq[T]] and the "incremental" operators break. I.e., in:

import std/options
proc foo*(sq = none seq[int]): int = echo sq
when isMainModule:
  import cligen, cligen/argcvt
  proc argParse(dst: var Option[seq[int]], dfl: Option[seq[int]],
                a: var ArgcvtParams): bool =
    var uw: seq[int]
    if argParse(uw, (if dfl.isSome: dfl.get else: uw), a):
      dst = option(uw); return true
  proc argHelp(dfl: Option[seq[int]], a: var ArgcvtParams): seq[string] =
    @[a.argKeys, "seq[int]", (if dfl.isSome: $dfl.get else: "?")]
  dispatch foo

in a file topt.nim you see:

nim r topt -s,+=,2,3 -s,+=,4

report some(@["4"]) instead of some(@["1","2","3","4"]). Those are kind of advanced CLuse cases, though other things may also break. Any such breakage can surely be fixed in more type specific overloads like Option[seq[T]] or possibly fixed in general with more CT magic in cligen/argcvt.nim or maybe a lot of whens everywhere. Such details should be worked out before we add it to cligen/argcvt, but I'd reiterate that I am open to PRs on this.

For now, if you find yourself using this a lot, you can put those general Option[T] definitions in some cgOpts.nim file and then just import cligen; include cgOpts. You should also be able to replace the whole argcvt module with a subdir in your project called cligen/ & a module there called argcvt.nim. (Used to work, but haven't tested it lately.)

This is complex enough that it bears reiterating that I suspect, for most users most of the time, a required parameter with no default value at all or a global var with a specific test is enough - what I might call "one-level optionality" -- which has no helpGen/prog.lang complexity. This "two-level optional" idea with Option[T] default values may "over model optionality" { but, sure, I can imagine times such might be nice, e.g. when you want the Nim API part to use Option[T] - Nim Is Choice! :-) }.