💾 Archived View for lark.gay › posts › cli-opinions.gmi captured on 2024-02-05 at 09:32:15. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

Unsolicited opinions about CLI design

There's an excellent reference on the web detailing how to build sensible, ergonomic, and robust CLIs:

Command Line Interface Guidelines

I recommend giving it a read. Once you're done, I'll spend the rest of this post wasting your time giving you my ✨ opinions ✨.

Passing Options

Most tools follow the GNU-style conventions for passing options, where options have a "long" form (e.g. `--version`) and a "short" form (e.g. `-v`). Considering how pervasive it is in the Unix world, I consider tools that don't follow this convention to be violations of the Principle of Least Surprise.

Principle of least astonishment (Wikipedia)

`find` is always a thorn in my side for exactly this reason.

$ find /tmp -name core -type f -print

I also think tools should always allow you to smush together single-character options, so you can do horrible things like `rsync -rlptgoD` (this is actually in the man page). The argument-parsing package in Go's standard library (`flag`) does not allow this, and it makes me sad.

flag (Go)

There are LIMITED cases where I will permit doing something funky with your CLI syntax in the name of ergonomics. I'll give Taskwarrior a pass here because it wouldn't really work otherwise.

Taskwarrior

Exit Codes

Some commands, like `rsync`, provide different error codes for different failure modes and meticulously document them. This makes them very scriptable. Most tools, despite having 255 possible error codes, will only ever return a 0 or a 1.

Synposis

Man pages often have a "Synopsis" section at the top to act as a quick reference for the arguments the tool accepts.

git commit [-a | --interactive | --patch] [-s] [-v] [-u<mode>] [--amend]
          [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>)]
          [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
          [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
          [--date=<date>] [--cleanup=<mode>] [--[no-]status]
          [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
          [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
          [--] [<pathspec>...]

man git-commit

git checkout [-q] [-f] [-m] [<branch>]
git checkout [-q] [-f] [-m] --detach [<branch>]
git checkout [-q] [-f] [-m] [--detach] <commit>
git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>...
git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]
git checkout (-p|--patch) [<tree-ish>] [--] [<pathspec>...]

man git-checkout

a quick reference

Git, my brain is too small for this.

Flags

Here are some common flags you'll see in CLI tools. If I see a flag with one of these names, I'll have opinions about it.

--help

I really need the `--help` text to fit on one page. When commands start dumping their man pages to stdout I get overwhelmed and need to go sit in a dark room. Git is nice enough to open a pager for you at least, but I'm really looking for a Lark-sized nugget of documentation.

Many tools that don't use GNU-style option conventions, such as `dd` and `find`, will often make an exception for `--help`. I appreciate this, since typing `--help` will always be my first reflex.

Some tools, however, will insist that you invoke a `help` subcommand instead:

$ go get --help
usage: go get [-t] [-u] [-v] [build flags] [packages]
Run 'go help get' for details.

Go, you're already handling `--help` specially to give me this error message, why not just print the help text?

--version

Every tool should be able to print its version—mostly so I can figure out why it works on my coworker's machine and not mine.

Some tools decide to print other information—such as their license notice—as well, just in case I was hoping to be able to parse the output.

-f, --force

This flag is supposed to mean, "Are you absolutely sure you want to do this?" The intent is to protect your users from accidentally wiping their data or borking their system (or at least prevent them from deflecting blame when they do).

However, tools that make `--force` too common a use-case cheapen its effectiveness. Too often I'll try to `git push --force` to `main` because I'm used to force-pushing to feature branches.

If you're trying to protect me from doing something particularly dangerous, please make it really difficult for me by making the name of the flag really long and specific. I'll give a special shoutout to `rm --no-preserve-root`.

-r, -R, --recursive

For tools that operate on files, I always assume `-r` or `-R` is reserved as synonyms for `--recursive`, making the tool operate recursively on directories and their descendants.

Naturally, tools vary on whether they accept `-r`, `-R`, or both.

Let's keep score:

If you're going to write a tool that could possibly do something recursively, please don't make `-r` or `-R` do something other than that.

--no-clobber

Some tools that operate on files or data and need an option to prevent overwriting the target will call that option `--no-clobber`. Examples include `cp`, `mv`, and `curl`.

You'll often see the term "clobbering" used to refer to this scenario. Clobber is a very fun word and if you call it `--no-overwrite` instead you hate fun.

--dry-run

Some tools provide a feature that lets you see what the tool *would* do without actually doing it. It's common to call this option `--dry-run`.

If your tool does dangerous things and doesn't have this option, you hate your users.

Libraries

Argument parsing libraries can often be rather opinionated. This is fine, as long as they have good opinions.

Here are two I quite like:

Click (Python)

Clap (Rust)

Some languages provide an argument parsing module as part of their standard library. Some provide more than one! These tend to be worse than the community-maintained ones.

Some examples:

getopt (Python)

argparse (Python, again)

flag (Go)

Conclusion

Send me your CLI opinions and I'll link or quote them here (and judge them if they're not Good Opinions).

─────────

Posts

Home