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.
The distinctive feature of confutils is probably that it treats the CLI as "one more" source of application configuration, alongside env variables, conf files and custom sources (like the windows registry) - ie the idea is that you use one "object" to gather configuration options from all sources and then run your app accordingly.
It also has a proc-signature-to-conf-object macro, which has some similar functionality:
import confutils
# macro style
cli do(
name {.name: "name", desc: "Name to greet".}: string,
count {.name: "count", defaultValue: 1, desc: "Number of greetings".}: int
):
for x in 0 ..< count:
echo "Hello ", name
# proc style
proc hello(name: string, count: int) =
for x in 0 ..< count:
echo "Hello ", name
dispatch(hello)@arnetheduck
Yeah, I have no doubts your (Status) library is ergonomic, featureful and well thought-out. That's why I've recommended it already in this thread.
@Araq
While we're at it -- the library could define REST entry points and offer a CLI for easy testing.
I don't think we're "at it" at all. Did you just suggest to plug a server into an iterator-emitting macro?
Actually, to correct another possible misimpression, conf-file-environ-command or any other FFI-like source of seq[string] -> CL params is not only present in cligen, it is present at arnetheduck's suggestion with a pluggable API also at his suggestion a couple weeks before confutils even had its initial git commit (not to suggest such multi-source CLparams is not ancient tradition going back to at least less in the 1980s, but I'd guess it goes back to the 70s).
The set up was/is flexible enough that CLauthors could use std/parsecfg and TOML or any other syntax they liked which relates to Zoom's recent Issue and related PR. That approach is all very "low code" by leveraging the way Nim's scoping rules work/include, but how low to go and in which ways is always a point of dispute. E.g., I picked a new name mergeParams() while it seems Zoom is using the same commandLineParams() name. Pros & cons. Bike shedding on names/design should be on those Issue/PR threads not here.
Meanwhile, I'm successively using the lib and have an update:
Now the auto-generated help, while still being const-evaluated, supports interpolation with run-time generated values, via a hook that affects both display and string convertion ($).
Using multireplace makes it simple.
BTW, you could easily use this functionality for CLI localization.
Details: https://indiscipline.github.io/cozycliparser/#accessing-help-help-string-interpolation
Example:
import cozycliparser
import std/strutils
buildParser(parserConfig(helpPrefix = "MyProg $ver"), "myprog", "Cli", GnuMode):
opt('d', "dir", "Target directory (default: $dir)", "PATH") do (_: string):
discard
let currentDir = "/tmp" # simulates the 'os.getCurrentDir' call
setHelpInterpolator():
s.multiReplace(
("$ver", "v1.2.3"), # literal value
("$dir", currentDir) # captured variable avoids repeated lazy evaluation
)
doAssert $Cli.help == """
MyProg v1.2.3
Usage: myprog [options]
Options:
-d, --dir=PATH Target directory (default: /tmp)
-h, --help Show this help and exit"""
Release on GH: https://github.com/indiscipline/cozycliparser/releases/tag/v0.2.0
Release on GH: https://github.com/indiscipline/cozycliparser/releases/tag/v0.3.0
This is an interesting one. The user wants this form:
somebin <service> <action> <action-opts-and-flags>
Which means the parsing tree is somewhat inverted, the earlier commands vary and later stages are the same and reused. This raises the question of how could we reuse the "actions" (subcommands) and their handling closures without code duplication, but still being able to pass the context in, so handler actually do different thing depending on the parent subcommand (service above). Here's my explanation of the problem:
Unfortunately, I don't see an easy way of providing this without code duplication. The current architecture builds the parser tree and registers handlers in a single parallel pass. Sharing parser's sub-tree across multiple parent commands would require delegating "parse the rest from here" to the same dispatcher proc from multiple call sites, but each dispatcher is tied to a specific slice of the handler index storage, so it cannot be easily shared.
Moreover, registering unique handlers via closures allows them to do exactly what's necessary depending on the context (like, knowing which service/subcommand is registering this handler at the moment). If we try to reuse the same handling closures for multiple commands when reusing a subparser, we need a way of providing that context other than enclosing it, otherwise we would still be multiplying handlers and reusing just the outer scope of their registration, which is negligible and is the same as currently available by using the proc, as described in the docs linked above.
Code reuse at the declaration level (via a template or proc) is supported, reuse of the generated runtime structure is not.
Any ideas?
Any ideas?
Allow for "<service> <action>" keys aka "keys with spaces" and treat it as "service_action" internally.
With the Nim release v2.2.10 the parseopt improvements cozycliparser relies on are finally fully in and we now support stable!
https://github.com/indiscipline/cozycliparser/releases/tag/v0.3.3