Good CLIs are composable and extensible blog home

Posted Thursday, 21-Nov-2024 by Ingo Karkat

CLI

Many complex applications expose their functionality via command-line interfaces. These can be used interactively in the shell, or combined with other commands in scripts or other programs that invoke them and process their outputs. There's a tension here: We both want the interface to be simple and easy to use, but on the other hand want to have access to all underlying functionality and be able to tweak any aspect of its behavior.

The big advantage of command-line interfaces is that you start with simple commands for simple tasks, gradually discover more nuances with more advanced use cases, learn about flags that help you obtain the desired result. Soon you'll realize that the same set of flags and parameters are passed, and that recalling those from memory or the shell's history becomes tedious. The first shell aliases are born. But those aren't very flexible, just canned invocations that cannot take flags on their own. And shell aliases cannot be easily invoked from shell scripts (which is a blessing, as the short and often cryptic aliases wouldn't be very readable, and they're also prone to change as you rename them to avoid name clashes).

require­ments

What you really want is custom commands that look and behave just like the original ones. Ideally, it's even possible to override existing commands without having to use a different syntax. From this, these two requirements follow:

  1. composable: If subcommands cannot be combined, if one simply query cannot be fed into another, more complex one, then scripting is severely hampered. This also includes good error handling. If problems aren't indicated by a non-zero exit status, outputs have to be parsed manually (or even worse error handling thrown out altogether), and this makes the writing of pipelines where data from one command is fed into the next one a huge mess. Custom output formatting greatly helps composability, too: For the user, tabular output with aligned columns and a header work well, but are difficult to parse programmatically; tab-separation or highly structured output (e.g. as JSON) is easier to consume.
  2. extensible: Many applications today use a program [global-options ...] sub-command [command-args ...] syntax. It should be possible to provide our own custom-sub-command. In Git, most subcommands initially were implemented as git-subcommand shell scripts (for performance reasons, many were later rewritten as binaries). This design enables the addition of a git-whatever command somewhere to the PATH, and it can be immediately invoked as $ git whatever --custom-flag --parameter foo, completely hiding the fact that this is not a native subcommand.

When these conditions aren't fulfilled, or if you aren't aware of the correct approach to work with CLIs, usability will suffer, and you'll likely throw in the towel sooner or later.

illustration

This screencast on Git Commit Fixup by Brooke Kuhlmann reminded me of those requirements. The subject is an intermediate-level use of Git to add a fixup commit and then rebase to have it combined with the original, faulty one. It starts with a custom gl alias, which is explained in another screencast.

alias gl='git log --graph --pretty=format:"%C(yellow)%h%C(reset) %G? %C(bold blue)%an%C(reset) %s%C(bold cyan)%d%C(reset) %C(green)%cr.%C(reset)"'

The custom log format might have advantages, but it's irrelevant for the task at hand. Didactically, it might have been better to omit it (or just mention it at the end as a bonus tip and segue into the other screencast). The core command of the fixup workflow is then given as:

GIT_EDITOR=true git rebase --interactive

With all due respect, this verbatim command is unusable for the task at hand! The GIT_EDITOR=true trick (though well explained here) isn't memorable and with the uppercase variable cumbersome to type, and way too long for a common task like rebasing fixups. This needs to be a short and memorable alias or custom command, much more like a customized log format! In fact, I found grbi and grbq aliases in the author's dotfiles, so Brooke doesn't type this all the time, neither.

Strictly speaking, the --autosquash flag would be required as well, although both the author and likely any user would rather set the corresponding rebase.autoSquash config flag. In fact, with --autosquash as a command-line flag, no --interactive needs to be given and the trick with GIT_EDITOR wouldn't be necessary at all. But that's hard to understand as GIT_EDITOR=true git could also be written as git -c core.editor=true, and a more targeted config override would be sequence.editor (or GIT_SEQUENCE_EDITOR) which just applies to the interactive rebase editing (but not things like editing commit messages). Git's dominant position and long history have led to a very powerful API, but also to a lot of cruft and TIMTOWTDI. I'll rant about that in a future post…

summary

Unfortunately, good shell setup skills aren't widely taught. Hopefully you'll pick up some good habits from master programmers (today, many YouTubers have channels dedicated to programming tips, although the good ones typically are drowned out by the sheer mass of them), and learn the rest (and what your personal best setup is) bit by bit as you progress. I fear that those people who struggle with their first steps in the command-line are then permanently scarred and instead gravitate towards IDEs and GUI applications. There's nothing wrong with that, but you'll lose the opportunity to become a true master, push the boundaries of what's possible, and enjoy almost complete automation of repetitive and tedious tasks.

For things that you do often, working in the command-line is a worthwhile endeavor. Good CLI design opens the door to it.

Ingo Karkat, 21-Nov-2024

ingo's blog is licensed under Attribution-ShareAlike 4.0 International

blog comments powered by Disqus