I’ve been following the discussion and it’s cool to see how everyone handles CLI parsing(https://forum.nim-lang.org/t/13777). I thought I’d share a macro-based approach I use called Kicker. https://github.com/BLeAm/kicker.nim
It’s pretty straightforward, the macro inspects your proc and maps parameters into five modes: positional, optional, flags, rest args, and repeated options.
It’s been a 'less is more' solution for me, no need to manually build parser objects or configuration tables. You just write your logic and let the macro handle the mapping. Actually, I developed it for personal use but just thought I’d throw it out there as another way to leverage Nim’s macros for CLI tools!
ooooooooh that's wtf I'm talking about! Amazing stuff.
I was thinking about making some thing like this but never did.
To be honest, I might not be the best person to explain exactly how Kicker compares to cligen. Truthfully, I didn't even look at cligen’s details before developing Kicker. Actually, I didn't look at it at all until I was done, and then I just took a quick peek.
But if I had to explain the mindset behind Kicker, it’s really all about "Convention over Configuration." With Kicker, I wanted everything defined right inside the proc block, both the abbreviations and the help messages.
For instance, if you want to customize things in cligen, you’d usually end up with something like this:
import cligen
proc test(input: seq[string], verbose = false) =
discard
proc build(release = false) =
discard
dispatchMultiple(
[test,
short = {"input": 'i', "verbose": 'v'},
help = {
"input": "Input values (repeatable)",
"verbose": "Verbose output"
}
],
[build,
short = {"release": 'r'},
help = {
"release": "Build in release mode"
}
]
But with Kicker, you can keep all those settings bundled within the proc block like this:
import kicker
proc test(Input: seq[string] = @[], Verbose = false) {.kicker.} =
"""
test: Run the test command
input: Input values (repeatable)
verbose: Verbose output
"""
echo "Testing with input: ", input, " (verbose: ", verbose, ")"
proc build(Release = false) {.kicker.} =
"""
Build the project
release: Build in release mode
"""
echo "Building (release mode: ", release, ")"
dispatch("My CLI Tool developed with Kicker") Personally, I feel this approach is cleaner, more modular, and easier to wrap your head around. But hey, that's just a matter of preference!
Another point is that Kicker has a very thin wrapper. It barely modifies the original proc at all, except for normalizing the parameters to lowercase. cligen definitely has a much thicker wrapper, but that’s because it supports a ton of extra features. In my experience, I almost never use those extra bells and whistles. I chose to "keep it simple" because it just matches my mental model better. Just my humble opinion, though! :D
Well, it currently fails if you are missing entirely this Python doc string-like construct with its own private DSL to learn (why isn't it a doc comment like most Nim code?). So, it could be more "incremental" with smarter defaults. This aspect is probably very temporary, if this package has much future life, though.
What is less likely temporary is this macro tag {.kicker.} you have to add to wrapped procs. cligen is careful to not actually require even an import before the wrapped proc or even being defined in the same module. I believe this loose coupling is well motivated, too, although sure - I can see people not appreciating this more system-wide property until they've been burned by the overhead of having to invoke a program 100,000 times rather than a function call via import and having 99% of their run-time in "easily eliminated overhead - 'if only' PLang callability/APIs had been preserved". That has happened to me not once, but many times over the decades.
In general, people should be encouraged to do their own things, though. You just should not be surprised if a single slice through the feature space winds up much simpler than something literally dozens of people asked for features in. Of course, it fits in your head as your only user and primary architect and only adding what you need. Generality costs and familiarity saves (at docs at the least!). I was actually a bit surprised at how low the cost was of the run-time syntax generality recently added (kind of in the theme of Landin's The Next 700 Programming Languages). Anyway, I make no pretense that cligen is the be-all/end-all impl even of the style of API it is, but it is also pretty easily to dismiss things that seem superficial that are actually pretty well motivated.
To correct what might be a slightly tricky communication/impression, cligen also just calls the wrapped procs, but there are some compile-time knobs for massaging their return values because the "calling convention" of commands is more different for "output/return" than it is for "input/call". So, you can echoResult or whatnot, but this is mostly just compile-time complexity, not run-time. Anyway, I don't think many people comment on it, but these tools are all very akin to "FFIs". The config file parsing/colorized help and all that can be more run-time heavy, but can actually be turned off with a couple define's Zoom got me to add, and you can toss these defines in your user-/project/etc. configs (e.g. ~/.config/nim/nim.cfg) and be done.
Yeah, I totally get why you built in all those extra features to support a wider range of users and it's great that the Nim community has cligen as a solid standard.
That said, everyone has different preferences, and it's good that we can all take our own approach.
For me, it’s not about which features are left to add, but rather what not to add, to keep the core concepts clear and focused. If that aligns with your use case and mindset, then I'm glad we’re on the same page. If not, there’s nothing stopping you from using something more feature-rich that fits your needs better, which might very well be cligen. :)
Very nice, congrats.
Some useless questions: