New Project: adopt-subcommands
Tagged as blog, common-lisp
Written on 2021-04-22 11:00:00
I have just released a new project: adopt-subcommands. This project extends the excellent Adopt library with support for arbitrarily nested subcommands. See the README for more information.
I have just asked that it be included in Quicklisp, so hopefully it will be present in the next QL release.
History
After bouncing around between CL command line processing libraries for a while (including CLON, unix-opts, and another I forget), I tried Adopt shortly after it was released and immediately got hooked. It was just super easy to use and it used functions as the default way to define interfaces (which encouraged reuse and programatic generation). To be fair, other libraries have similar features, but there's just something about Adopt that clicked with me.
The big thing missing for me was easy support for subcommands. Libraries like CLON support that out of the box, but (at least in CLON's case) required that you completely specify every option at the terminal nodes. I wanted to define a folder-like hierarchy where options defined at some level get automatically applied to everything below it as well.
I was able to hack together a solution using Adopt, but I built it in a hurry
and it was definitely not fit for general consumption. Since then, I was
inspired by Steve
Losh's
Reddit comment giving
an example of how he'd make a simple subcommand CLI parser using Adopt. His
post made me realize I missed the existence of the adopt:treat-as-argument
restart (d'oh!) and after that, all the pieces fell into place on how to
cleanly rewrite my solution. This library is the result!
Nifty Features
I work with a number of programs written in golang that (IMO) have atrocious CLI handling (like helmfile and Kaniko). Maybe it's the individual program's fault, but it's endemic enough that I suspect whatever CLI parser the golang community has landed on is just terrible.^1
For instance, position of the options matters. "Global" options have to come
before the subcommand is even specified. So foo --filter=hi run
can have a
completely different meaning than foo run --filter=hi
. Additionally, some of
the subcommand style programs I work with don't print all the options if you
ask for help, they only print the options associated with the most recent
subcommand.
Needless to say, I made sure adopt-subcommands
didn't exhibit any of these
behaviors. As this library is parsing the command line, it builds up a path of
the folders (and eventually the terminal command) it passes through. This path
can be passed to adopt-subcommands:print-help
to print a help string that
includes all the associated options. Additionally, options can come at any
point after the subcommand that defines them.^2
There are two major difference between Adopt and this library:
- You need to provide functions when you define a terminal subcommand. This
function will be called with the results of the parsing when you
dispatch
. - The
dispatch
function has a keyword argument:print-help-and-exit
. If you provide the symbol naming your help option, then this library will automatically print the help and exit if that option is specified, and after doing as much parsing as possible.
Give it a try and let me know of any issues that you find!
1: although, it wouldn't surprise me if some gophers started arguing that it's totally on purpose, is actually quite elegant, blah blah blah. I kid. I'm just salty about golang's lack of conditions and insistence on using tabs and let that color my take on the entire language.
2: It would be possible to let them come before as well, but at the risk of introducing ambiguity. It's not clear to me that it's worth it.