Command line clients are everywhere. Almost everyone, at least in tech, is using them.
There are a lot of successful command line clients out there: the Linux project has git and the Node.js project has npm. We use some of them multiple times per day. Apache CouchDB recently got nmo (speak: nemo), a tool to manage the database cluster. We can learn a lot from successful command line interfaces in order to write better command line clients.
When I started to get interested in command line clients I realised that there are a lot of discussions and informations on the web about writing APIs. The web is full of tutorials to teach you how to build APIs, especially REST-APIs, but almost nothing can be found about writing good CLIs. This book tries to explain what makes a good CLI. In the second part of the book we will build a small command line client to learn how to use Node.js to create great command line clients that people love.
The goal of the book is to show the principles to build a successful command line client. The provided code should give you a good understanding what is important to build successful command line clients and how you could implement them.
Every section has its own code examples. Before you run the code, you have to run
npm install in the folder that belongs to the section.
I hope you enjoy the book – please recommend it in case you like it.
What makes a good CLI?
In this chapter we will take a look at successful command line clients and what they are doing pretty well, which will help us to understand the problems users face using the Terminal. Understanding the problems of our users will help us to build better command line clients with Node later in the book.
Let’s take a look at how people usually use a CLI: most of the time a human sits in front of a keyboard and interacts with a terminal. We want to use simple and recognisable commands for our CLI. Sadly just easy recognisable commands don’t get us very far right now.
Maybe the problem is easier to understand if we take a look at something what I would call a bad CLI:
$ mycli -A -a 16 r foo.html error: undefined is not a function
In my example I have to enter cryptic commands which is answered by a very cryptic error message. What does -A -a 16 and r mean? Why I am getting an error back, am I using it wrong? What does the error mean and how can I get my task done?
So what makes a good CLI? Let’s try it with the following three principles:
you never get stuck
it is simple and supports powerusers
you can use it for all the things!
In short: A successful CLI is successful because its users are successful and happy.
You never get stuck
Nobody likes to be in a traffic jam, stuck, just making a few meters per minute. We want to reach our target destination, that’s all we want! The same applies for our users. Both developers and users are extremely unhappy when the tools they use are standing in their way. They just want to get their task done.
So what does, „You never get stuck“ mean, exactly? It means that we should always offer our users a way to solve their task, a command should never be a dead end. Additionally the developers of the CLI should avoid every source of friction in their tool.
Let’s take a look at me, trying to use git:
$ git poll git: 'poll' is not a git command. See 'git --help'. Did you mean this? pull
In this example I entered a wrong command. git answers friendly: „Hey Robert, it looks like you entered a wrong command, but if you type in
git --help, you can list all the existing commands. And hey, it just looks like you mistyped git pull, did you mean git pull?“
git offers us a way to continue our work and finish the task.
And if we take a look at npm, another successful CLI client, we’ll see the same concept:
$ npm ragrragr Usage: npm <command> where <command> is one of: access, add-user, adduser, apihelp, author, bin, bugs, c, cache, completion, config, ddp, dedupe, deprecate, dist-tag, dist-tags, docs, edit, explore, faq, find, find-dupes, get, help, help-search, home, i, info, init, install, issues, la, link, list, ll, ln, login, logout, ls, outdated, owner, pack, prefix, prune, publish, r, rb, rebuild, remove, repo, restart, rm, root, run-script, s, se, search, set, show, shrinkwrap, star, stars, start, stop, t, tag, test, tst, un, uninstall, unlink, unpublish, unstar, up, update, upgrade, v, verison, version, view, whoami npm <cmd> -h quick help on <cmd> npm -l display full usage info npm faq commonly asked questions npm help <term> search for help on <term> npm help npm involved overview Specify configs in the ini-formatted file: /Users/robert/.npmrc or on the command line via: npm <command> —key value Config info can be viewed via: npm help config email@example.com /Users/robert/.nvm/versions/node/v0.12.2/lib/node_modules/npm
In this example I try to put garbage into npm, so npm answers friendly: „Hey Robert, I don’t know that command, but here are all the commands that would be possible. You can use them like this and get help about them by typing in npm help <command>.“
Like git, npm immediately offers help to enable me to finish my task, even if I have no idea how to use npm at all.
What if I still need help? Maybe I want to get some help before I just try out commands. Turns out there is a quite reliable way to ship documentation on Unix or Linux, man-pages!
Man-pages are quite nice, as you don’t need the internet to open them. You can also stay in the same terminal window to read them and don’t have to switch to another window, e.g. a browser.
But some users don’t know about man-pages or they don’t like to use them. Additionally many of them will be on Windows which can’t handle man-pages natively, so git and npm offer their documentation as webpages, too:
Both git and npm are making use of a trick: they write their documentation once (e.g. in Markdown or Asciidoc) and use the initital source as the base for the different formats of their docs. Later they convert them to different formats, e.g. to html.
If you take a look at the man-pages of git and npm, you will notice that their websites are basically framing the content from the man-page with a header and a sidebar.
Sometimes things go still horribly wrong… Let’s take a look at my example for a bad CLI again:
$ mycli -A -a 16 r foo.py events.js:85 throw er; // Unhandled 'error' event ^ Error: ENOENT, open 'cli.js' at Error (native)
In this case we are getting back a stacktrace without much context. For most people these stacktraces look quite cryptic, especially for people that don’t write Node.js on a daily basis.
And it is even worse: I really can’t tell if I just hit a bug in the command line client or if I am just using the CLI in a wrong way. Looking at that small terminal, with no idea what to do, I get extremely unhappy and so our users will get unhappy.
One thing nmo supports is „usage errors“ — here is what they look like:
$ nmo cluster dsf ERR! Usage: nmo cluster get [<clustername>], [<nodename>] nmo cluster add <nodename>, <url>, <clustername> nmo cluster join <clustername>
If a user tries to use a command in a wrong way, nmo will tell them immediately how they can use the command to get their job done. No need to open the documentation.
nmo also shows stacktraces to a user, if nmo crashes for serious reasons:
$ nmo cluster join anemone ERR! df is not defined ERR! ReferenceError: df is not defined ERR! at /Users/robert/apache/nmo/lib/cluster.js:84:5 ERR! at cli (/Users/robert/apache/nmo/lib/cluster.js:68:27) ERR! at /Users/robert/apache/nmo/bin/nmo-cli.js:32:6 ERR! ERR! nmo: 1.0.1 node: v0.12.2 ERR! please open an issue including this log on https://github.com/robertkowalski/nmo/issues
nmo adds the current nmo and node version to the stacktrace, like npm does. We also ask the user to copy the stacktrace and to open an issue containing the stacktrace.
The reports make it easy for the team to identify the bug, solve it, and release a new version of nmo by seeing the stacktrace.
And again the user is not stuck. The user gets help to solve their task, in the worst case we help them in our issue tracker.