Re: Unsolicited opinions about CLI design

This week is one of those “little things” week where I get to do fun things, work on the little broke things around the house, and just relax. It also means I get more verbose and start doing blog posts because why not?

Over on Gemini, there was a recent post about CLI design by Lark that caught my attention this morning. Well, and one about last names, but that is a much different topic.

As such, I just had a long conversation with one of my developers about our semi-annual goals. They wanted to document our primary CLI and asked me about opinions of their tasks. Ultimately, I suggested that their idea of creating a document that lists every option would be ultimately useless, expanding on the help from inside the CLI would be beneficial.

This also gives me the impetus to talk about some of my own evolving opinions about CLI.

External Documentation

I'll start with external documentation. Our application, call it bob, is a command-line that reuses the same business logic as our front end and services. It is intended to be a “big” one which means it has nested commands for individual tasks.

Many of those tasks are written because we need to solve a problem Right Now™. Other are the tasks because upper management has, in their infinite wisdom, separated our DBAs into a separate team which handles all the database woes of our company and we no longer have the dedicated individual that we've enjoyed for thirteen years. Since there is now a 1-3 hour delay on getting a DBA, they have to follow a strict system of access, and I have to puppet (re, take over their screen and type because I can't have direct access and they don't know the system or type quickly), I've been writing tools for things that I can programmatically do through our normal users.

This means that any external documentation is going to go stale. In a perfect world, I would write formal documentation as I code, but I also make a point of documenting every argument and command line as I go, so I'd rather have the bob handle the documentation instead of trying to also update our internal wiki, which customers don't have access to despite the fact we ship bob with our product also (for the same reasons I need it).

No Arguments

I completely agree with the -long argument name that find and PowerShell uses. It annoys the hell out of me. I also hate the /v of other Windows programs.

Same with exit codes. Different codes are awesome, when they are used properly. I'm looking at you, MadCap Flare.

Discovering

I think it would be obvious, but bob is inspired by git and az (thought more toward Azure's CLI). It uses nested verbs and they can go down reasonably deep.

az account list
bob direct property calculate

In both cases, I want the container verbs (az account, bob direct, bob direct property) to provide a short help, maybe some examples, than do a default behavior. This is because people who don't know the system need to be able to explore, but also know that there is more.

This is contrast to git remote which lists the remote, but git remote add adds remotes. There is no git remote list or git remote ls, so that makes it hard to discover what the tool can do because there is no obvious indicator that there are more nested verbs.

The Gitea CLI, tea, also does this but in a slightly different manner. Both tea repo and tea repo ls both list repositories but tea repo create will create a new one. That means if someone enters tea repo, there is nothing to indicate the create exists but at least there is a way to explicitly list repositories.

Since I value exploration and curiosity, I strongly lean toward container verbs are for discovery, not functionality. Ideally, a command that just gives input should also have a dedicated exit code (253 in most of my programs) for “showed help”.

Breaking Changes

The problem with the above statement is when a command needs to be broken into separate bits. For example, we used to have bob direct export to import data directly into the database (our direct commands are straight to the DB, our api commands use the OpenAPI layer). However, we split export into export data and export tables (bob direct export table).

What about the existing scripts that made the assumption about bob direct export? I don't want to be a versioning layer on our CLI and our customers don't understand semantic versioning even if we used it (business insists on romantic versioning). I also don't want to limit our ability to evolve as our understanding of the tools and how they work changes.

Ultimately, I go with three options:

  • Plan ahead and add the nested verb if I think we're going to need it.
  • Reorganize so we don't have to, which is why we have bob direct criteria export and bob direct property export.
  • Just break it (bump the major version if you can) and document the change.

Levels of Help

I prefer three levels of help: synopsis, option, and verb.

Synopsis is when you just run a program and it doesn't include the requirement arguments. Just give a little summary of what is needed. Depending on the parsing library, I'd rather see most command options, a list of verbs and what they do.

Option is when someone passes in --help (I do not like using -h for help). That should be the list of all verbs, options, and arguments and details.

This is where my opinions different from Lark. I hate when git clone --help opens a pager. I know what I'm doing and either I'm passing it already through a pager of my choice or I'm scanning because PowerShell is slow enough I can read almost as fast as it prints. I want it to dump data, I want to be able to scroll up as I need. Needless to say, I despise that git clone --help opens up a web browser on Windows. Switching programs is the last thing I want to do and it makes me feel like I'm losing control.

Also, if I touch the mouse, I failed. And browsers on Windows almost always require mice touching.

On the other hand, if it is given as a command or verb, such as git clone help, then I'm okay with a novel-length list of help files and throwing it into a pager is fine. I want the help verb to be the full details with examples, discussions, and links. Need more? Then make help have nested verbs that let me discover the detailed topics (but please no interactive exploration).

That means I'm also okay with the example:

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

Though I would have preferred that go get did the synopsis given above, go get --help gave the synopsis and explained what -t, -u, and -v are, and go help get (I'd rather it be go get help though) gave details like modules, examples, etc.

I believe that go get does something by itself, but I'd rather require an option that says “get all” (which is what I assume it does") instead of inferring it, but that is a point I feel strongly about. If go get does something, then skip that but my opinions that go get --help should at least list the verbs and explain the options.

Related to that, it frustrates me when --help does not show help screens. I don't care if I have every argument and option set, --help should always take priority.

Arguments and Options

There is one thing I struggle with the CLI design:

Prefer flags to args. It’s a bit more typing, but it makes it much clearer what is going on. It also makes it easier to make changes to how you accept input in the future. Sometimes when using args, it’s impossible to add new input without breaking existing behavior or creating ambiguity.

There isn't a good way of knowing when an flag is required. No standard convention that says “you must have this” because we are so inconsistent with indicating optional verses not optional. I guess if the synopsis said gary [--verbose] --input FILE maybe?

But this is one that I need to work out in my head because I usually equate positions are required, flags are optional. But, intellectually, I agree with the statement, but it isn't what I'm doing these days.

After thinking about it, I think I want to change to follow this one more.

Data Type of Options and Arguments

This is one of my frustrations, when I don't know if a flag takes a value or not, and what type of value does it take. In our system, we identify files or the type of input.

$ bob direct user list --help
...
--no-color
--table-search REGEX
--output FILENAME
--user-search LOOKUP

(“LOOKUP” has a special meaning for us such as starts:XXX, regex:XXX, id:999, key:XXXX, and contains:XXX verses XXX which is a case-insensitive exact match.)

There are cases when I want to pass in the value, so there is a different between --foo and --foo yes and the help should say that. Also, using a generic value, like tea does is frustrating.'

$ git repo create --help
...
   --gitignores value, --git value    list of gitignore templates (need --init)

Um, what is the value? And the help isn't what it does. It doesn't list the templates, it needs to have a template set but it doesn't tell me where to get a list of the templates.

Terminal Columns

Another frustration is that I don't like fancy tables in my CLI help. It might sound strange, but creating a nicely formatted table is great when you use standard columns:

$ tea repo create --help
...
   --branch value                     use custom default branch (need --init)
   --description value, --desc value  add description to repo

Now, saw you are jamming a narrow column of shell because your editor needs the bulk of the window but you need to handle some sidebar tasks or are writing code to use that CLI:

$ tea repo create --help
...
   --branch value                     use custom def
ault branch (need --init)
   --description value, --desc value  add descriptio
n to repo
   --gitignores value, --git value    list of gitign
ore templates (need --init)

... yeah, that isn't really readable. Wrapping stuff is a problem unless you take into account the number of columns that are currently on the screen. This is somewhere either the default needs to be screen wrapping, or we need a reactive formatting.

In a “perfect” world, I'd rather the narrow one look like:

$ tea repo create --help --faked
--branch value
  use custom default branch (need --init)
--description value, --des value
  add description to repo
--gitignores value, --git value
  list of gitignore templates (need --init)

Also, infrequently options should be longer and or multiple aliases mean there is a bigger chance of word-wrapping. Overall, ripgrep has a nicer default format for what I'm looking for:

$ rg --help
    --no-config
        When set, ripgrep will never read configuration files. When this flag
        is present, ripgrep will not respect the RIPGREP_CONFIG_PATH
        environment variable.

        If ripgrep ever grows a feature to automatically read configuration
        files in pre-defined locations, then this flag will also disable that
        behavior as well.

But even that doesn't handle columns well. I'd love to see a better convention here, even if it comes down to when everything should just be on a single line and let the terminal wrap.

$ bob --help
--log-level LEVEL, -l LEVEL

Sets the logging level for the
console output. Possible optio
ns are: Error, Warning, Info, 
Debug, Verbose. Case-insensiti
vie and shortened values allow
ed. Default: $BOB_LOG_LEVEL, I
nfo.

And since it usually comes up, I can't always zoom out the terminal and make the text smaller because a certain point, the blur radius from my eye surgery causes everything to turn into a muddled mess of colors.

Environment Variables

If an flag/option is driven by an environment variable (then it isn't a argument/positional), then list that in the --help. I believe the Woodpecker CLI does that (but I don't have it installed right now), but seeing something like this is nice:

$ bob direct property export --help
...
--connection DOTNET-SQL-CONNECTION The connection string to connect to
  the primary database. Value comes from $BOB_CONNECTION_STRING,
  "ConnectionString" from bob.config. Required.

That makes it a lot easier to understand, more so when the order of processing is also listed. If I'm using the example of --help verses help, those additional details are better suited for the command instead of the option.

Plural verses Singular

Singular. The same with my REST opinions. tea uses both and it looks wrong to me:

$ tea --help
...
     issues, issue, i                  List, create and update issues

Colors and Emojis

This is a hard one because I love and hate colors. Pretty colors are great, but I use terminals with black background, ones with light ones, and PowerShell with its hideous blue background. Sooner or later, a command with color screws up one of them because after decades of writing CLIs, we haven't come up with a convention to provide color preferences via environment variables.

Also, a decent percentage of the population is color blind. I also find that I stop processing color in certain situations and start seeing things in desaturated colors. So, as much as I want to see all those pretty colors in output, either don't touch my colors so I can use whatever background I want or come up with a standard that everyone follows.

I also love emojis, but there is a disconnect when some are black and white and others have full color. They are also dependant on fonts on the terminal.

Logging

Remarkably, logging flags cause me a lot of stress. As I see it, there are two philosophies when it comes to logging: flags or option.

Flags is when you have --verbose and --quiet. The problem is the conflict. What if you provide both? Should one take priority? Should it blow up with an invalid options selected? Mutually exclusive options for those libraries who handle it? And then there is the -vvvvvvv.

The other is an option, such as a lot of Microsoft tools use (also woodpecker-cli), which is a --log-level LEVEL.

Lately, I've been leaning toward the --log-level approach, but give it options. Since I use Serilog heavily and my users are sloppy, I like the level to be as flexible as possible. So, while I might say “error, warning, info, debug, verbose” (I always think debug and verbose should be reversed with Serilog), I want them to be able to say --log-level e or --log-level ERR because that's how they think.

I also frequently use the same tools in a monitored environment as my local machine, so occasionally I want more details in the log messages. That is also why I want to see more --log-format FORMAT where format can be one-line JSON, plain, with full timestamps, extra details, etc.

Finally, I have a common need to have log files written out to a text file. Usually this is --log-file FILENAME, but that format and level should be independently configurable. --log-file-format and --log-file-level with the same options, defaulting to the top level ones if not provided.

Of course, if --verbose is just an alias for --log-level verbose, that would be fine also. If I had to pick common or uncommon options, I would pick one set for the common.

Consistency

There is a bunch in here. Not all of them make sense for everyone, but they make sense for me. That comes down to the Standards Problem so I can't really say if they are “good” opinions or not, just my opinions and ones that I like and ones that I don't like.

But I like talking about it because it helps refine my opinions and find new ideas that I have never thought about.

Metadata

Categories:

Tags: