Command-line applications the most utilitarian of programs, but the most utilitarian machinery can be beautiful.
During the first week of a new job several years ago, I was given a spreadsheet that contained SQL queries used in a manual testing process that I was to carry out. Being a good developer, I converted the spreadsheet into a command-line application that ran the queries automatically. Being new, I did it as quickly as I possibly could, to show my new employers how valuable I was.
The script was horrible. It didn’t take normal options, had almost no error handling, output a hodge-podge of debug messages, and broke in mysterious ways if the environment wasn’t set up just right. But I could run it, and it worked. I moved on to other projects, and six months later, another developer came over to ask me to fix a bug in this script.
It was still being used, and it was this poor sap’s job to run it. But my name was on it, so it was my job to fix it. And add features to it. I decided that, instead of hacking out a fix, I would make this application the best automation script I possibly could. It would be easy to use, helpful, well-documented, and easy to enhance. It would be awesome—and if I did things right, I’d never have to touch it again.
Two days later, I no longer had a script, but an application, just as I’d planned. It was in regular use for over a year after that, during which time I never had to help anyone debug it, fix it, or enhance it.
My book Build Awesome Command-Line Applications with Ruby is all about how to do this, and how to do it in Ruby, which is a great language to use for writing command-line apps. While the book goes into great detail, we’ll hit the high points here, focusing on three steps you can take on the path to making awesome command-line apps.
Step 1—Decide to Care
Calling something a script can be an excuse not to care about writing good code. Stop writing scripts and start writing applications. Everything you know about writing good software should be applied to your command-line apps. Why is it OK to skip error handling or duplicate code in a “quick script,” but it’s not OK in the “real” application you work on?
Before you write your next command-line app, take a minute to adjust your mindset. Decide that you’re not going to hack something together, you’re writing a new application, the same as any other application. You’ll find that with this simple change in attitude, you’ll naturally create a more polished app without spending significantly more time than you might have otherwise.
Step 2—Be Helpful
You’ve decided to craft an application instead of hacking together a script. The most important thing to do from this point on is to make your app helpful. This means making it easy for the user to understand how to use your app, and it means not letting them do something wrong and punishing them for it.
To see what I mean, type this into your terminal:
Did it delete your entire hard drive? No. It showed you a very brief usage statement. Your app should behave similarly. It should not do anything destructive or annoying when invoked without any arguments.
It should also accept both -h and --help as options to show more detailed help. There are tools that automate this almost entirely in most languages. In Ruby, for example, OptionParser makes this dead simple.
Also, give the user actionable error messages. You wouldn’t show a user of your web app an error message from MySQL when they try to choose a user name for themselves that already exists, would you? Treat command-line users the same way. Most languages have plenty of if statements to help you get things just right.
If you want to be really helpful, make a man page. A man page (so-named for being used by the UNIX man command, short for manual) is a place to put all the gory details about how your app works. If you are writing a Ruby app and are distributing your app via RubyGems (both excellent ideas), the gem-man gem will install a man page with your gem. Even if you aren’t using Ruby, the Ruby app ronn allows you to write a man page in Markdown, which is a lot easier to use than nroff.
Step 3—Play Well with Others
Being helpful is about interfacing with humans, but it’s also important to interface well with other programs. The tools available on a typical UNIX system are all small and single-purpose, but they are all designed to interface with each other in a way that allows complex operations to be performed. For example, suppose I’m using Subversion and I’ve just done an update that generated conflicts. I need to resolve them, and I’d like to write one command to edit all the files in conflict.
Here is that command:
I can then mark them as resolved similarly:
If you aren’t familiar with awk or xargs, don’t worry. My point is that the authors of Subversion never sat down and designed the edit all files in conflict command. In fact, if you were working alongside them and asked them to add a feature to edit the files in conflict, they would rightfully refuse, because they’ve designed their app to play well with others.
This playing-well-with-others thing works because of a few conventions adopted by good command-line apps. The first of these is the proper use of streams. Every command-line app has access to three streams: the standard input stream, the standard output stream, and the standard error stream. When you use a pipe in a shell command, the shell pipes the standard output stream of one app to the standard input stream of the other app.
The standard error stream is printed to the terminal. This means you can provide error messages to the user without interrupting the output of your app. You should respect this convention and only message the user on the standard error stream. Your program’s output should go to the standard output stream.
Further, you should accept input on the standard input stream, at least as an option. Most command-line apps operate on files. If your app does this, it should look for its data on the standard input stream if no files were provided. This allows it to operate in a pipeline just like all other apps. In Ruby, the awesome ARGF library makes this completely transparent.
The second way in which apps can communicate is via exit codes. The idea is this: If an app does what it was asked, it exits with zero. If some error occurs, it exits with nonzero. The use of exit codes allows you to do things conditionally on the success of actions. For example, when I’m ready to deploy something, I want to push my changes to the remote git repository, and then use Capistrano to deploy. If the push fails for some reason, I obviously would not want to run Capistrano. Because git exits nonzero when a push fails, I can do this:
The shell treats && as a “short-circuit” operator, like most programming languages.
Your app can examine these exit codes for itself. The UNIX standard function system returns the exit code, and most programming languages have a similar construct. In Ruby, you could do it like this:
The final way apps can communicate is via signals. A signal is a value that is given to a process that will interrupt whatever the process is doing. The process can then examine the value of the signal and respond to it. Have you ever used the kill command? All kill does is to signal a process. By default, kill sends the signal TERM, and by default, applications, when receiving TERM, exit. Thus, kill tends to kill processes.
You don’t need to exit when you receive this signal, though; you can do anything you want, such as clean up open resources. You can also respond to other signals. Some applications, for example, will reload their configuration when they receive the HUP signal. In Ruby, you can catch signals with Signal.trap:
Hopefully you’ve seen the value in taking these three tiny steps toward a better command-line app. By having a helpful and easy-to-use command-line syntax, and by making proper use of I/O streams, signals, and exit codes, your apps will be more flexible, easy to use, and easy to maintain. And that will make your life easier, not to mention help you “level up” as a developer.
David Bryant Copeland, author of Build Awesome Command-Line Applications in Ruby, is a veteran professional software developer who lives on the command line. He speaks frequently at national and regional conferences, and spends his working hours as a developer for LivingSocial in Washington, DC. He’s written applications in C, C++, Perl, Java, Ruby, and Scala, believes in clean code, doing it right, providing a great user experience, and using the right tool for the job.