This approach to CLIs comes from Andrey Mikhaylenko's nice Python module 'argh'.
Much as with Python, an intuitive subset of ordinary Nim calls maps cleanly onto
command calls, syntactically and semantically. For such procs, cligen can
automatically generate a command-line interface complete with long and short
options and a nice help message. In Nim terms, adding a CLI can be as easy as
adding a single line of code:
proc foobar(foo=1, bar=2.0, baz="hi", verb=false, paths: seq[string]): int =
##Some existing API call
result = 1 # Of course, real code would have real logic here
when isMainModule: import cligen; dispatch(foobar) # Whoa...Just one line??Compile it to foobar (assuming nim c foobar.nim is appropriate, say) and
then run ./foobar --help to get a minimal (but not so useless) help message:
Usage:
foobar [optional-params] [paths]
Some existing API call
Options (opt&arg sep by :,=,spc):
-h, --help print this help message
-f=, --foo= int 1 set foo
-b=, --bar= float 2.0 set bar
--baz= string "hi" set baz
-v, --verb toggle false set verb
Other invocations (foobar --foo=2 --bar=2.7 ...) all work as expected.
When you want to produce a better help string, tack on some parameter-keyed metadata with Nim's association-list literals:
dispatch(foobar, help = { "foo" : "the beginning", "bar" : "the rate" })Often this is all that is required to have a capable and user-friendly CLI, but some more controls are provided for more subtle use cases.
If you want to manually control the short option for a parameter, you can
just override it with the 5th|short= macro parameter:
dispatch(foobar, short = { "bar" : 'r' }))With that, "bar" gets 'r' while "baz" gets 'b' as short options.
To suppress some long option getting a short option at all, specify '\0' for
its short key. To suppress all short options, give short a key of "".
By default, dispatchGen has requireSeparator=false making -abcdBar,
-abcd Bar, --delta Bar or --delta=Bar all acceptable syntax for
command options. Additionally, long option keys can be spelled flexibly, e.g.
--dry-run or --dryRun, much like Nim's style-insensitive identifiers.
The same basic string-to-native type converters used for option values will be applied to convert optional positional arguments to seq[T] values or mandatory positional arguments to values of their types:
proc foobar(myMandatory: int, mynums: seq[int], foo=1, verb=false): int =
##Some API call
result = 1 # Of course, real code would have real logic here
when isMainModule:
import cligen; dispatch(foobar)If it makes more sense to echo the result of the proc than convert its result to
an 8-bit exit code, just pass echoResult=true:
import cligen, strutils # generate a CLI for Nim stdlib's editDistance
dispatch(editDistance, echoResult=true)If you want to expose two or more procs into a command with subcommands a la
git or nimble, just use dispatchMulti in, say, a cmd.nim file. Each []
list in dispatchMulti is the argument list for each sub-dispatch. Tune
command syntax and help strings in the same way as dispatch as in:
proc foo(myMandatory: int, mynums: seq[int], foo=1, verb=false) =
##Some API call
proc bar(myHiHo: int, myfloats: seq[float], verb=false) =
##Some other API call
when isMainModule:
import cligen; dispatchMulti([foo, short={"verb": 'v'}], [bar])With that, a user can run ./cmd foo -vm1 or ./cmd bar -m10 1.0 2.0.
./cmd --help will emit a brief help message and ./cmd help emits a more
comprehensive message, while ./cmd subcommand --help emits just the message
for subcommand.
That's basically it. Many users who have read this far can start using cligen
without further delay, simply entering illegal commands or --help to get help
messages that exhibit the basic mappings. Default help tables play well with
automated "help to X" tools such as complete -F _longopt in bash, compdef _gnu_generic in zsh, or the GNU help2man package. Many simple examples
are at test/. Here are
some more DETAILS,
and even more is in the module documentations (
parseopt3
argcvt
cligen )
and RELEASE-NOTES.
There are so many CLI parser frameworks out there...Why do we need yet another?
This approach to command-line interfaces has both great Don't Repeat Yourself
("DRY", or relatedly "a few points of edit") properties and also has nice
"loose coupling" properties. cligen need not even be present on the system
unless you are compiling a CLI executable. Similarly, wrapped routines need
not be in the same module, modifiable, or know anything about cligen. This
approach is great when you want to maintain both an API and a CLI in parallel.
Easy dual API/CLI maintenance encourages preserving access to functionality
via API/"Nim import". When so preserved, this then eases complex uses being
driven by other Nim programs rather than by shell scripts (once usage complexity
makes scripting language limitations annoying). Finally, and perhaps most
importantly, the learning curve/cognitive load and even the extra program text
for a CLI is all about as painless as possible - mostly learning what kind of
proc is "command-like" enough, various minor controls/arguments to dispatch to
enhance the help message, and the "binding/translation" between proc and command
parameters. The last is helped a lot by the auto-generated help message.