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.


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:

  1. 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.
  2. 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.

comments powered by Disqus