Software developers, infrastructure & DevOps engineers, and operational IT staff are heavy users of the command line. The benefits of the command line are clear:
- Commands are / should be readable
- Commands are repeatable
- Commands can be shared amongst a team (eg. via source control / chat / etc.)
- Automation is scalable across many widgets
Unfortunately, there is significant disparity between how different commands work, especially in the UNIX ecosystem. While the Microsoft Windows platform isn’t perfect, it has enjoyed the consistency provided by the Windows PowerShell framework for more than a decade, as of this writing. The non-PowerShell command line tools, that are built into Windows, suffer from similar design inconsistencies as their UNIX counterparts. Some commands use arguments with double dashes, while others use single dashes. Some commands use a forward slash to specify an argument, while others require dashes, or provide the option of a slash or a dash. Some commands spell out their arguments, while others require short-hand form (eg. –force vs. -f).
As an end user of any particular program, these inconsistencies can be frustrating to deal with day-in and day-out. Sometimes, as an end user of <tool>, I just want to get my work done, so I can enjoy a beer on the beachfront. 🙂
Are these UX design inconsistencies the end of the world, or are they glaring flaws that inhibit adoption of software? Not really, but when you consider the fact that millions of developers across the world, at varying skill levels, are dealing with hundreds of dev tools day-in and day-out, having consistency across these types of things is important. Every time I go back to <tool> to perform <operation>, as a software developer and end user of that tool, I shouldn’t have to remember each tools’ nuances.
Command Line Design Challenges
As we continue this discussion about command line tool design, below are some pointers that I’ve noticed, with regard to [in]consistency in command line design. For the purposes of this article, we will consider the term “argument” and “parameter” to mean the same thing.
- High-level command structure
- Consistency with argument positions in a command
- Consistency with argument declarations (single dash vs. double dash)
- Consistency with providing argument aliases
- Combining multiple arguments together
- How to discover additional help
Now that we’ve explored some of the challenges in designing command line user interfaces, let’s take a look at some real-world examples of how things currently are, what existing tools do well, and how the user experience could be improved.
Scenario: Docker Command Line
The Docker command line tool shares a somewhat similar structure to many other modern tools. The top-level docker command exposes functionality via an array of child contexts, each of which is broken into yet more specific commands, with their own arguments. However, this is roughly where the similarities end. All things considered, docker is pretty easy to use. However, it shows some inconsistencies with regard to its high-level command structure, which is something I’ve noticed in many other tools. One of the more common docker commands you might use is called docker inspect. The docker inspect command enables you to discover detailed information about Docker images, containers, and SwarmKit tasks. Okay that’s great, but what if you wanted details about a docker network or SwarmKit service? Well, in that case, you have to go to the network or service child context, each of which has an inspect sub-command.
This isn’t really a huge deal, but going back to the original intent of the article, when developers have to work with many tools on a daily basis, these kinds of small design choices can increase friction to the user. Thankfully, the Docker team is addressing these user experience concerns, thanks to the feedback from many folks on GitHub! Great job, Docker team!
Scenario: Microsoft Azure Command Line
The Microsoft Azure command line interface (CLI) is a cross-platform (Windows, Linux, Mac OS X) Node.js application that enables provisioning and management of cloud resources in your Microsoft Azure subscription (account). This tool is actually quite well designed, and generally offers consistency across its various child contexts. For the most part, Azure CLI consistently uses the -h and –help arguments, enabling the user to avoid remembering exactly which one they have to use. User flexibility is a key, positive trait of the Azure CLI here. This tool doesn’t force you to remember -h or –help, both of which are common UNIX paradigms. Instead, it gives you both options, and even if you forget which one to use, it doesn’t matter, because both of them work equivalently!
This principle of flexibility extends to many of the child commands in Azure CLI. Let’s take a look at azure network vnet create –help, which enables you to create a new Virtual Network. You’ll notice, from the built-in help, that you can specify a short-hand parameter, or a long-form parameter. It’s up to the user to decide, because both options are presented.
PS C:\windows\system32> azure network vnet create --help help: Create a virtual network help: help: Usage: network vnet create [options] <resource-group> <name> <location> help: help: Options: help: -h, --help output usage information help: -v, --verbose use verbose output help: -vv more verbose with debug output help: --json use json output help: -g, --resource-group <resource-group> the name of the resource group help: -n, --name <name> the name of the virtual network help: -l, --location <location> the location help: -a, --address-prefixes <address-prefixes> the comma separated list of address prefixes for this virtual network. help: For example, -a "10.0.0.0/24,10.0.1.0/24" help: Default value is 10.0.0.0/8 help: -d, --dns-servers <dns-servers> the comma separated list of DNS servers IP addresses help: -t, --tags <tags> the list of tags. help: Can be multiple. In the format of "name=value". help: Name is required and value is optional. help: For example, -t "tag1=value1;tag2" help: -s, --subscription <subscription> the subscription identifier help: help: Current Mode: arm (Azure Resource Management)
The Azure CLI is a great example of how to ensure consistency across multiple child contexts / commands, and give the user flexibility in how to specify command line arguments. While the Azure CLI does have its own set of nuances, the team has actually done a really good job of ensuring that the UX is consistent with itself, as well as other command line tools in general.
Scenario: Amazon Web Services Command Line
The AWS command line interfaces is fairly similar to the Microsoft Azure CLI, in the sense that it has a series of child contexts for each AWS cloud platform service (eg. S3, EC2, SQS, etc.). Unfortunately, the beginner user experience with this tool is very poor. There’s nothing in the tool’s built-in help that seems to provide any information about how to utilize it. If you run the tool with no arguments, then it tells you to run aws help, instead of simply presenting the help to the user, the least friction possible.
usage: aws [options]
[ ...] [parameters] To see help text, you can run: aws help aws <command> help aws <command> <subcommand> help aws.exe: error: too few arguments
The AWS CLI’s built-in help also wastes a lot of whitespace that could be used to improve the readability of the documentation, when you follow the directions and run aws help.
As a beginner with this tool, you’d have to go to the online documentation for AWS CLI, and find out that you actually need to run aws configure before you do anything else. Thankfully, when you run aws configure help, the documentation provided there is fairly comprehensive and helpful. Contrasting this experience with the Azure CLI, you’ll notice that the Azure CLI tool provides introductory help by simply running azure, and nothing more. Furthermore, the azure command prioritizes the essential login command towards the top, so you know exactly what your first steps are.
Scenario: Apt Package Manager Command Line
Let’s say that I want to use the apt package management utility to list out packages on an Ubuntu Linux system. If I examine the man apt page, I can see that the apt list sub-command supports a few different arguments: –installed, –upgradable, and –all-versions. When I run the apt list command with the -h or –help arguments however, I don’t receive the detailed help that was provided in the man apt page. In fact, the output from this simply shows all of the top-level apt commands, even though I have explicitly specified a sub-command — or perhaps you prefer the term, child context. As an end user of the apt utility, this complicates my interaction with the utility, because rather than working with apt directly, I now have to erase my apt command and run the man command instead, to find out how to use the command that I’m working with. This user experience is disruptive, and doesn’t ultimately serve the needs of the end user.
Furthermore, why not have aliases for –installed = -i, –upgradable = -u, and –all-versions = -a? Most people seem to advocate for shorter parameters, to “save key strokes,” which is a valid concern, but there’s also the element of “readability” in script files that are shared among a team of developers or automation engineers. Not to mention, if you read your own code 2-3 months down the road, you’ll probably completely forget why you specified those parameters. If there were a user choice about short-form and long-form parameters, it would be much easier to read in the long run.
As we’ve observed in this article, there are varieties of inconsistencies in command line interfaces. While I strongly believe that application developers should not be forced into developing around a specific structure, I think that there is significant value in providing user consistency across different applications. There are tons more examples that I could have listed here, but that would take thousands upon thousands of words — and probably hours — to comprehensively discuss. This list should suffice for the purposes of examples.
The Windows PowerShell framework provides a standardized structure for authoring commands, and therefore, a (generally speaking) consistent user experience. In PowerShell, you have:
- A standardized documentation / help system
- Optional command and argument / parameter aliases
- A single command that “discovers” other commands on the system
- A modular architecture to group related commands
Am I suggesting that PowerShell is the only solution? Absolutely not. What I’m suggesting is that having a consistent user experience across various command line tools is important to ensuring productivity, and even happy, end users. How exactly that principle manifests itself in software development is up to the community at large. Windows PowerShell provides an example of how this element of software design can be highly successful, and it would be great if there were an equivalent type of framework for command line developers in the non-Windows world.
As a global community, we have rallied around common design goals that improve user experiences in other, more specific ecosystems. Why not invest some effort into standardizing some of the most commonly used utilities on non-Windows operating systems? Sure, it might result in some change, but I think that it’s a change for the better, and would help to naturally invite beginner users into the IT sector at large, by lowering the barrier to entry. Although you — the person currently reading this article — might scoff at these design challenges, there are a lot of people who may not be currently at your skill level, across the world, who would just like things to be a bit easier at first. Let’s work together to make the IT industry a more user-friendly and user experience-centric space.