Hey, after poking around std/parseopt for a while, I've decided it's become ready to build around it.
So I've made cozycliparser.
I suggest heading out straight to Docs, they are pretty detailed and I've tried to include many examples. Here's the repo. A simple example is also listed below.
I won't repeat the docs and the GH readme here, so just a couple of words about the implementation.
I've set a goal to test how far can I go with low-magic, typed macro-based implementation. Think it turned out rather well.
I've just ported a little program with mere 6 options that previously used iffy's argparse which I like a lot and which is comparable in terms of the API. The code gained les than 10 new lines, but the --help output is now colorized and the compiled binary lost ~**37.5 KiB** which in this case is ~14%. I'm pretty sure bugs will be revealed and fixes will expand the code, but it's a nice start.
To be fair, this still the same dumb parser from the standard library. No validation, no typed output, nothing like that. But this is an intentional choice, as from my experience, the requirements catering to all the specific needs a developer can have while parsing arguments just explode the complexity of the task and, which is perhaps even worse, of the APIs that strive to be flexible. Still, flexibility is never enough and in many cases you just ditch the built-in capabilities and write the logic manually. An endless chase.
Also, I've been keeping the #12425 in mind and think this could be it. ;) At least the spirit is stdlib-aligned: write idiomatic nim to use, light-weight, flexible by not doing more than strictly necessary.
import cozycliparser
type Options = object
output: string
input: string
verbose: bool
greetName: string = "world"
var options: Options
buildParser(parseConfig(helpPrefix = "Greeter v0.1\nThis program greets."),
"greeter", "Cli", GnuMode):
opt('\0', "output", "Output file", "FILE") do (val: string):
options.output = val
flag('v', "verbose", "Enable verbose output") do ():
options.verbose = true
arg("INPUT", "Input file") do (val: string):
options.input = val
cmd("greet", "Greets NAME") do ():
arg("NAME", "Name to greet") do (val: string):
if val != "": options.greetName = val
echo "Hello ", options.greetName
cmd("version", "Displays version and quits") do ():
run do ():
quit("v0.42", 0)
# HelpText namespace is automatically built and injected in scope:
doAssert $Cli.help == """Greeter v0.1
This program greets.
Usage: greeter [options] INPUT <greet> <version>
Arguments:
INPUT Input file
Commands:
greet Greets NAME
version Displays version and quits
Options:
--output=FILE Output file
-v, --verbose Enable verbose output
-h, --help Show this help and exit"""
# Display colorized help for the program and subcommands with:
Cli.help.display()
Cli.greet.help.display()
To those with the visceral reaction like "Eww, closures? And you want me to write them by hand?" the module includes templates to free you from typing additional ~5-15 chars per option.
FWIW I would design it this way:
import smartcli
let options = cliapp"""Greeter v0.1
This program greets.
Usage: greeter [options] INPUT <greet> <version>
Arguments:
INPUT Input file
Commands:
greet Greets NAME
version Displays version and quits
Options:
--output=FILE Output file
-v, --verbose Enable verbose output
-h, --help Show this help and exit"""
And then cliapp builds you a validator and an object type with fields like input and output. When the description is --foobar=VAL1|VAL3 there is a field foobar of a generated enum type.
Not necessary easy to write this cliapp macro but it's the one design that gives you something more convenient than "for loop + nested case statements", whereas you replaced the case statement with a table of closures, which is the same thing, just slower and less convenient to write.
I expected you'll say something like that, if anything. Your design more or less exists: http://docopt.org/ There's already a nim implementation, though I never tried it: https://github.com/docopt/docopt.nim. Rust's Clap had the same mode too, IIRC.
I don't like this approach personally, but it certainly works for many people.
whereas you replaced the case statement with a table of closures, which is the same thing, just slower and less convenient to write.
Thanks for this stellar characterization. The case statements are not replaced and closures are just a natural way to define logic in a structured way before it's used in the code.
Moreover, I don't think the "slow" argument works at all. If your case requires handling so many arguments speed becomes a factor and a closure overhead is too much, it's time to roll up the sleeves and write a specialized parser. What we're talking here about is code that runs exactly once during program's lifetime in the overwhelming majority of cases.
Regarding "less convenient to write", I understand that's subjective, but to me it's a surprising point of view. Even if we don't count all the handler registering, just the case statements this macro expands to are much-much longer and require duplication and manual synchronization of data to provide help display and keep it from going stale.
I'd still stand by the opinion this library solves enough to be useful: enables single source of truth for docs, handlers and parsers, provides formatting (opposite to the topsy-turvy and fragile way of docopts) and colorizes output, automates dispatch of commands and "contextual" help for them.
Cool!
Do you have support for shared or grouped flags across different subcommands (besides global flags)? I used to primarily use cligen for more complex binaries I wrote but didn't love the feel of the API the more I wanted to tweak how different flags/opts were handled in that scenario.
When I decided to write my own I also wrapped std/parseopt (but later switched to used cligen/parseopt3 so that I could parse with short flags without separators, and without writing more parsing logic).
As a DSL-heavy counterpoint here is what your example program looks like with my library:
import hwylterm/hwylcli
hwylCLi:
name "greeter"
... "This program greets"
flags:
[global]
v|verbose "enable verbose output"
output:
? "Output file"
T string
input:
? "Input file"
T string
subcommands:
[greet]
... "greets [b yellow]name[/]"
positionals:
name string
run:
echo "Hello " & input
[version]
... "Displays version and quits"
run:
quit("v0.42", 0)
All of the "typing" in my library is handled via overloaded hooks (not dissimilarly called from the closures I imagine) which makes it straightforward to take string parsing logic on flag-by-flag basis:
For instance supporting a multilevel verbosity argument like -v , -vvv, -v=3
type Count = object
val: int
proc parse*(p: OptParser, target: var Count) =
# if value set to that otherwise increment
if p.val != "":
var num: int
parse(p, num)
target.val = num
else:
inc target.valDo you have support for shared or grouped flags across different subcommands (besides global flags)?
Not really. You'll need to repeat the opt/flag/arg registering for each command you want it work with, but you, of course, can reuse the handler if you pass a handling closure prepared beforehand.
The syntax builds a command tree naturally and without some additional syntax you can't really share/reference a node.
If you require custom logic, like prohibiting a global/parent flag for a subcommand, you really can't do it at the parsing time as you need to peek forward. Enabling things like this would create a mess that still won't satisfy some custom logic requirements.
Instead, just keep state while parsing and deal with it afterwards as you see fit.
I already mentioned their limitations in previous posts. In short, they always assume things and even with advanced ones you often need to write some additional logic. Besides that: no DSL, lean code, no logic level above the parser loop in my lib.
Of all people, you're the one using bare std/parseopt regularly and I thought you might appreciate something that builds on top of it without giving up any flexibility or control.
For example, if you need to count repeated flags, unless the library like docopts exposes a counter or some custom hook you can't do it.
There's benefits to staying on a lower abstraction level (as also having option points on the range to the higher ones, but all the links above + Cligen cover it already).