Subcommand-based interfaces are common: the majority of the CLI tools that provide more than one operation within them expose their features in this manner. Examples of such interfaces include svn, git, ifconfig, yum, apt-get and many, many more.

Designing the interface of these applications is quite straightforward once you have the concepts clear, but there are quite a few common pitfalls that you need to be aware of to prevent falling into them. In this post, I will guide you through what the general design of such interfaces is and I will cover a bunch of the common problems. Let's get started.

General syntax

The general syntax of a subcommand-based interface is simple:

tool [general options] command [command options] [command arguments]

where square brackets denote optionality as usual.

There are two extremely important takeaways from this definition:

  • Anything that comes before command must be applicable to all the commands and cannot change the behavior of any individual command without also changing the others.
  • Anything that comes after command is specific to that command and that command only.

It is your responsibility to make this distinction explicit in the implementation of your interface. In general, this is easy as most command-line parsing libraries will let you chunk your command-line parsing in the two distinct blocks.

However, I must call your attention to GNU Getopt's odd behavior in that, by default, it does not stop searching for options once it encounters the first non-option argument. This makes the chunking of generic options vs. command-specific options trickier, but not impossible. To get GNU Getopt to behave as expected, you need to select POSIX-compliant behavior as described in the GNU Getopt manual. Unfortunately, doing so causes incompatibilities with other implementations so you may need a nasty Autoconf check to detect this situation if you want to remain portable.

Repetition is OK

In a subcommand-based application, it is common for a group of —but not all!— subcommands to require similar pieces of information. In this situation, it is perfectly fine the implement the same option as a command-specific option in all the subcommands that take it.

Let's consider the interface of Kyua as an example. In this interface, we have generic commands like help and config that do not access the test database, and commands like test and report that do access it. All of the subcommands that read from or write to the tests database need a path to the database, and because this path has a reasonable default, it makes sense to let the user tune the path via a flag.

A common temptation would be to offer the interface just described as:

kyua [--store=file] config|debug|help|test

but that would be a really poor choice. This interface definition implies that the --store flag applies to all of the subcommands, which is not true. help and config wouldn't care less about the path to the database file and, effectively, providing that flag to these commands would have no visible effect.

A better definition would be the following:

kyua config|help
kyua debug|test [--store=file]

which very clearly denotes what each subcommand is affected by. It is therefore OK to expose a subset of flags across various subcommands as long as those flags are specific to the subcommands.

So why do you often see invalid interfaces like the former? Because it is much easier to implement them: calling getopt twice is not as easy as it may seem.

Lastly, there are two additional details to keep in mind: first, make sure that the behavior of such options is exactly the same in all of the commands in which it exists; and, second, reuse the parsing code as much as possible.

Requesting help

In a previous post, we learned that the user should only be shown help when he explicitly requests it. So how do we expose this in a subcommand-based interface?

Easy: asking the program to show help is a completely different behavior than that of any other command. For this reason, the only reasonable approach to fit the request for help in the design of a subcommand-based interface is to offer the help as a separate subcommand which you can appropriately call help.

Following this rationale, the simplistic form of the help request would be:

tool help

It is easy to imagine that we could extend this syntax to support additional functionality and thus offer a more complex builtin help system. For example:

tool help [command name]

With this syntax, we could allow the user to request help on any individual subcommand.

Be aware, however, that all of the following are bad choices yet they are quite widespread:

tool --help  # Abuses flags.
tool command --help # Abuses flags.
tool --help command # Abuses flags and their ordering.
tool command help # help is not a subcommand of the command!

Requesting the version number

Similarly to the previous point on how to request help, requesting the version number of the application is also a completely different behavior than anything else. Consequently, this feature must be implemented as a separate subcommand.

The only way you can fit the request for the version number in your subcommand-based application without break consistency is by implementing a syntax like this:

tool version

Or, if you want to generalize this, you could expose the version and other useful information (like license text, authors, etc.) in an about subcommand just like Kyua does.

But yes: exposing the request of the version number via a --version flag is wrong in this type of interfaces.

Subcommand-based interfaces are the most complex of CLI styles given that, conceptually, they bundle many tools under a single roof. However, due to the flexibility in their design, it is possible to achieve a very straightforward and orthogonal interface with which users are already familiar with.

In the next post, I will cover the simpler style of a CLI in which the tool only exposes a single command. Things get slightly trickier there due to the additional implementation constraints.

Go to posts index

Comments from the original Blogger-hosted post: