Build Awesome Command-Line Applications in Ruby 2: Control Your Computer, Simplify Your Life (2013)
Chapter 5. Delight Casual Users
So far, we’ve learned how to write easy-to-use, helpful command-line apps that interoperate well with other apps and systems. But in the previous chapter, we saw how subjective design decisions can be when we had to decide which output format of our to-do app would be the default. Choosing default values and behavior—as well as naming our commands, options, and arguments—is not as straightforward as using exit codes or sending output to the right place. These choices are design decisions that can profoundly affect how a user interacts with your app. Each decision nudges the user in one direction or the other, making some tasks simpler for the user to execute and some more difficult. In short, the right design decision can be the difference between a mediocre app and an awesome one.
It can be very difficult to know how to make these decisions and to understand the impact they will have on an app’s behavior. Command-line applications, however, operate in a more constrained environment; the user interacts with our apps in limited ways, and the output that can be produced is similarly limited. This allows us to articulate some simple guiding principles and rules of thumb to help make these design decisions. In this chapter, we’ll start to understand these principles and rules, all in the name of creating command-line applications that have an awesome user experience.
To help guide us in making design decisions, such as the names of options, the default values of arguments, or the default behavior of our app, we need a small set of principles we can refer to. Here are three guiding principles for designing command-line applications that we’ll explore in this chapter:
· Make common tasks easy to accomplish.
· Make uncommon tasks possible (but not easy).
· Make default behavior nondestructive.
From these principles, we’ll find there are many rules of thumb that we can apply to our designs. To discover what these are and see how to apply them, we’ll examine the three main design elements of a command-line application: the names of the command-line options, the default values for flags and arguments, and the default behavior of the app itself.
We’ll do this by refining our two running examples: db_backup.rb, the MySQL backup app, and todo, our simple to-do list manager. Let’s get started with the first design decisions we’ll face: the names of our options and commands.
5.1 Choosing Names for Options and Commands
In Chapter 2, Be Easy to Use, we learned that OptionParser allows us to create multiple options that mean the same thing. We used this feature in our database backup script, db_backup.rb, by allowing both -i and --iteration to signify an “end-of-iteration” backup. Why does OptionParser have this feature, and why did we use it?
Naming Options
This question is better posed in two parts: “Why did we provide a short-form option?” and “Why did we provide a long-form option?” Short-form options allow frequent users who use the app on the command line to quickly specify things without a lot of typing. Long-form options allow maintainers of systems that use our app to easily understand what the options do without having to go to the documentation. Let’s look at an example.
Suppose we’ve set up db_backup.rb to run nightly at 2 a.m. We’ve also set up our “end-of-iteration” backup to run on the first of the month at 2:30 a.m. We accomplish this by using cron, which is a common UNIX utility for running regularly scheduled commands. Suppose that Bob, a sysadmin who maintains the servers where we run our backups, wants to configure the system to perform automated maintenance on the first of the month. The first thing he’ll do is look at cron’s configuration to see what else is going on at the first of the month. He’ll need to get a complete picture of what’s been configured so he can decide how to get his job done. He’ll see something like this:
|
00 02 * * 1-5 db_backup.rb -u dave.c -p P455w0rd small_client |
|
30 02 1 * * db_backup.rb -i -u dave.c -p P455w0rd small_client |
If you aren’t already familiar with cron, here is a quick description of the format used by crontab in the earlier example: the first five values tell cron when the command should run. The numbers represent, in order, the minute, hour, day of the month, month, and day of week. An asterisk is the symbol for “all,” so the first line tells db_backup.rb to run every weekday (1-5 as the fifth value, or the days of the week) at 2 a.m. (the 00 and 02 as the first and second values, representing the minutes and hour, respectively). The second line tells cron to run our “end-of-iteration” backup at 2:30 a.m. on the first of the month.
Bob has never run db_backup.rb, and while he does understand that our dev team runs two types of backups (daily and “end of iteration”), the -i isn’t going to mean anything to him. He’ll have to find the documentation for db_backup.rb or go to the command line and run db_backup.rb --help. While we could have added a comment to the crontab entry, it’s actually much clearer to use the long-form option:
|
00 02 * * 1-5 db_backup.rb -u dave.c -p P455w0rd small_client |
*
|
30 02 1 * * db_backup.rb --iteration -u dave.c -p P455w0rd small_client |
Now Bob knows exactly what the second line is doing and why it’s there. We could be even more conscientious and turn the long-form option into --end-of-iteration. Of course, we wouldn’t change -i to -e; i is a good mnemonic for “iteration,” which makes it a good name for the short-form version of the option.
This example illustrates the importance of good naming as well as the form of those names. This leads us to the following rules of thumb regarding naming your options:
· For short-form options, use a mnemonic that frequent users will easily remember. Mnemonics are a well-known learning technique that is common in command-line application user interfaces.
· Always provide a long-form option and use it in configuration or other scripts. This allows us to create very specific and readable command-line invocations inside configuration files or other apps. We saw how it helped Bob understand what was going on in cron’s configuration; we want everyone to have this experience maintaining systems that use our apps.
· Name long-form options explicitly and completely; they are designed to be read more so than written. Users aren’t going to frequently type out the long-form options, so it’s best to err on the side of clarity.
Let’s follow these guidelines and enhance db_backup.rb by renaming --iteration and adding long-form options for -u and -p:
make_easy_possible/db_backup/bin/db_backup.rb |
|
|
opts.on("-i", |
*
|
"--end-of-iteration", |
|
'Indicate that this backup is an "iteration" backup') do |
|
options[:iteration] = true |
|
end |
|
opts.on("-u USER", |
*
|
"--username", |
|
"Database username, in first.last format") do |user| |
|
options[:user] = user |
|
end |
|
|
|
opts.on("-p PASSWORD", |
*
|
"--password", |
|
"Database password") do |password| |
|
options[:password] = password |
|
end |
Now our crontab is easy to read by just about anyone who sees it:
|
00 02 * * 1-5 db_backup.rb --username=dave.c \ |
|
--password=P455w0rd small_client |
|
30 02 1 * * db_backup.rb --end-of-iteration \ |
|
--username=dave.c \ |
|
--password=P455w0rd small_client |
While we should always provide a long-form option, the converse isn’t true; some options should have long-form names only and not short-form versions. The reason for this is to support our second guiding principle: while we want uncommon tasks or features to be possible, we don’t want to make them easy.
The reason for this is twofold. First, there’s the practical limitation of having only twenty-six letters and ten digits available for short-form option names (or fifty-two if you include uppercase, although using short-form options as mnemonics makes it hard to have both an -a and an -A that the user will remember). Any new short-form option “uses up” one of these characters. Since we want our short-form options to be mnemonics, we have to ask ourselves, “Is this new option worthy of using one of those letters?”
Second, there is a usability concern with using short-form options. The existence of a short-form option signals to the user that that option is common and encouraged. The absence of a short-form option signals the opposite—that using it is unusual and possibly dangerous. You might think that unusual or dangerous options should simply be omitted, but we want our application to be as flexible as is reasonable. We want to guide our users to do things safely and correctly, but we also want to respect that they know what they’re doing if they want to do something unusual or dangerous.
Let’s put this to use. db_backup.rb compresses the database backup file, but suppose a user didn’t want to perform the compression? Currently, they have no way to do that. We’re happy to add this feature, but it’s not something we want to encourage; database backup files are quite large and can quickly fill the disk. So we allow this feature to be enabled with a long-form option only.
Let’s add a new switch, using only a long-form name, and see how the app’s help output affects the user experience:
make_easy_possible/db_backup/bin/db_backup.rb |
|
|
options = { |
|
:gzip => true |
|
} |
|
option_parser = OptionParser.new do |opts| |
|
# ... |
|
opts.on("--no-gzip","Do not compress the backup file") do |
|
options[:gzip] = false |
|
end |
|
end |
|
$ ./db_backup.rb --help |
|
Backup one or more MySQL databases |
|
|
|
Usage: db_backup.rb [options] database_name |
|
|
|
-i, --end-of-iteration Indicate that this backup is an "iteration" backup |
|
-u, --username USER Database username, in first.last format |
|
-p, --password PASSWORD Database password |
*
|
--no-gzip Do not compress the backup file |
Notice how the documentation for --no-gzip is set apart visually from the other options? This is a subtle clue to the user that this option is not to be frequently used. For apps with a lot of options, this visual distinction is a great way for users to quickly scan the output of --help to see which common options they might need: those with a short-form name.
Naming Commands in a Command Suite
For command suites, the names of commands should follow the same guidelines: all commands should have a clear, concise name. Common commands can have shorter mnemonics if that makes sense. For example, many command-line users are familiar with the ls command, and it is a mnemonic of sorts for “list.” We can take advantage of this in our task-management app todo and provide ls as an alias for the list command. Since todo is a GLI-based app, we simply pass an Array of Symbol to command instead of just a Symbol:
make_easy_possible/todo/bin/todo |
|
*
|
command [:list,:ls] do |c| |
|
|
|
# ... |
|
|
|
end |
Now frequent users can do todo ls:
|
$ todo help |
|
NAME |
|
todo - |
|
|
|
SYNOPSIS |
|
todo [global options] command [command options] [arguments...] |
|
|
|
GLOBAL OPTIONS |
|
-f, --filename=todo_file - Path to the todo file (default: ~/.todo.txt) |
|
--help - Show this message |
|
|
|
COMMANDS |
|
done - Complete a task |
|
help - Shows a list of commands or help for one command |
*
|
list, ls - List tasks |
|
new - Create a new task in the task list |
Naming can be difficult, but our guidelines around mnemonics, descriptive long-form options, and judicious use of short-form names can help. Now it’s time to go one level deeper into a command-line app and talk about the default values for flags and arguments. An example of what we mean is the --filename global option to our to-do list management app, todo. Why did we choose ~/.todo.txt as a default; should we have chosen a default, and is that the best default value we could’ve chosen?
5.2 Choosing Default Values for Flags and Arguments
The existence of flags in an app’s user interface serves our guiding principle of making uncommon things possible, and providing good defaults helps make common things easy. A sensible default communicates your intention of how to use your app, since the default value makes a particular usage of your app very simple (we’ll see how to allow users to customize defaults in Chapter 6, Make Configuration Easy). Let’s see how to decide on good defaults both for the arguments given to flags and for the arguments to our app (see Chapter 2, Be Easy to Use if you need a quick review of the different parts of the command line).
Default Values for Flags
First, you should almost always have a default for flags; a flag that is required is not user-friendly and is burdensome for users to have to include on the command line every time they run your app. It’s possible that your app is complex enough that it needs information from the user for which there is no good default. This is ideally rare, and we’ll see some techniques to allow a user to set defaults in Chapter 6, Make Configuration Easy, but, in general, consider a good default for every flag.
The default you choose is, obviously, dependent on what your app does and how the flag’s value affects it. In other words, it is a design decision you’ll have to make. An easy way to decide on a default value is to ask yourself “What default value would I prefer?” or “What default enables the most common behavior?”
You’re presumably writing a command-line app for yourself or others like you, so this is as good a place to start as any. That being said, there are conventions for certain types of flags, such as a flag that takes a filename as an argument. Another common example is a flag that controls output formatting. Let’s look at these two types of flags in more detail.
Flag Arguments That Represent Filenames
Many times, a flag’s argument is a filename. An example of this is our to-do list app’s global option --filename. We chose ~/.todo.txt as our default, meaning the file named .todo.txt, located in the user’s home directory.
In general, files that represent data that persists across invocations of the app, such as a database or configuration file, should live in the user’s home directory by default. This allows the app to be used by many users on a system without any chance of collisions (imagine if the default was the same file for all users; everyone’s tasks would be mixed together!).
It’s also common practice to use a leading period (.) in the filename so that the file is hidden by default in directory listings. This prevents the user from being distracted by your app’s data and also reinforces that this file shouldn’t be tampered with. There’s less convention around the filename for databases, but for configuration files, it’s customary to use the suffix rc (see The .rc Suffix for the etymology of this suffix).
The .rc Suffix
Most UNIX commands use the suffix rc for the name of configuration files. According to Wikipedia,[33] this extension isn’t an acronym for “resource configuration” or “runtime configuration” but comes from a command called RUNCOM.
RUNCOM was created by Louis Pouzin for the Compatible Time-Sharing System (CTSS), which was one of the first time-sharing operating systems. RUNCOM was used to execute a series of commands in succession—a precursor to what we now call a shell script.
He is even credited with coining the term shell, as he describes in a post on the Internet from 2000[34] on the origins of the shell:
After having written dozens of commands for CTSS, I reached the stage where I felt that commands should be usable as building blocks for writing more commands, just like subroutine libraries. Hence, I wrote “RUNCOM”, a sort of shell driving the execution of command scripts, with argument substitution. The tool became instantly most popular [sic], as it became possible to go home in the evening while leaving behind long runcoms executing overnight.
…
Without being invited on the subject, I wrote a paper explaining how the Multics command language could be designed with [the] objective [of using commands somehow like a programming language]. And I coined the word “shell” to name it.
Although RUNCOM (and CTSS) has long-been retired from regular use, its legacy lives on both as the name for user-specific configuration files and as the basis for the UNIX start-up scripts, typically located in /etc/rc.d.
If a flag’s argument is a filename but not a database or configuration file, you’ll have to use your best judgment; however, keep in mind that many different users might use this app on the same system, so choose a default that is most appropriate or convenient for most users that won’t cause collisions. For example, if your application produces a log file, then a location like /var/log/my_app.log would not be a good default; multiple users might contend for that common location. A better value would be ~/tmp/my_app.log. Another option would be to name the file using the current process identifier, available via $$ in any Ruby app: log_file = "/tmp/my_app_#{$$}.log". Note that if you are requireing English, as we did in Chapter 4, Play Well with Others, you can use the more readable variable name $PROCESS_ID.
Flag Arguments That Control Output Formatting
We saw in Chapter 4, Play Well with Others that todo’s list command takes the flag --format to control the output format. This is a pretty common type of flag and can be seen in several popular Ruby applications, such as Cucumber[35] and RSpec.[36] Choosing the default format can be tricky, especially when you have support for more than just a basic format and a pretty format.
In the case of todo, we chose “pretty” as the default, because this makes the simplest and most common case of using the app very easy: a user is using their terminal and wants to manage a to-do list. The pretty format is easy for human eyes, and we don’t expect the app to be called from another system (like cron).
What if your app supports multiple formats, like Cucumber does? At the time of this writing, Cucumber supports eleven different output formats. The default is a colored, indented format that shows passing, pending, and failing tests in green, yellow, and red, respectively. Why is that the default?
This default represents a design decision by the creators of Cucumber to motivate a certain type of behavior and use of the app. Cucumber wants you to do test-driven development, where you write a (failing) test first and, knowing it will fail, run it. With the default output format, this produces some red text. You then write code to make the tests pass, gradually turning the red output into green output. Once all your output is green (“like a cuke”), your job is done.
Cucumber’s choice of default output format is very powerful; it makes it very obvious what the tool is doing and how you “should” use it (this isn’t to say that you should always use colorful output; in general, you shouldn’t, but we’ll see in Chapter 10, Add Color, Formatting, and Interactivity when to do so and how). This makes the common way of using the app simple. Cucumber makes the uncommon things possible, however, by including “machine-friendly” formats, such as JSON.[37] What you should do, when faced with choosing a default output format, is to think about what your app does and how you want users to use it. Choose the output format that closely matches that use case.
We’ve talked about default values for flags, but what about default values for our app’s arguments?
Default Values for the App’s Arguments
Although many command-line apps’ arguments are a list of files, an app’s arguments really represent some sort of input. In the common case of arguments-as-filenames, these names represent sources of input. In the case of our to-do list app, todo, the argument to the new command is the input (the name of a new task). In both of these cases, the best default value for this input is the content of the standard input stream .
ARGF: Automatically Read from Files or Standard Input
Many command-line apps take their input from a list of files and use the standard input as the default if no files are provided on the command line. Almost every standard UNIX command works this way, and because it is so common, Ruby provides ARGF to make this easier.
Let’s take a simple app to sort the lines of any files, or the standard input, much like the UNIX command sort:
make_easy_possible/sort.rb |
|
|
#!/usr/bin/env ruby |
|
def read_file(io,lines) |
|
io.readlines.each { |line| lines << line.chomp } |
|
end |
|
lines = [] |
|
if ARGV.empty? |
|
read_file(STDIN,lines) |
|
else |
|
ARGV.each { |file| read_file(File.open(file),lines) } |
|
end |
|
puts lines.sort.join("\n") |
With ARGF, we can eliminate both the check for files on the command and the iteration over those files if they are included.
make_easy_possible/sort_argf.rb |
|
|
#!/usr/bin/env ruby |
|
|
|
lines = [] |
|
|
|
ARGF.readlines.each { |line| lines << line.chomp } |
|
|
|
puts lines.sort.join("\n") |
ARGF includes all the logic to figure out where to get input from and will iterate over every file provided on the command line using the standard input if none was provided. ARGF even provides methods to know where you are in the list of files. filename returns the name of the file currently being processed, and lineno returns the line number within that file. This means you can provide good error messaging to the user when processing files in this manner.
To learn more about ARGF, consult its documentation page at http://www.ruby-doc.org/core-2.0.0/ARGF.html .
Much like the standard output and standard error streams, the standard input stream is available to all applications and is used to access any input piped into the app. When we piped the output of ls into sort, sort read this information from its standard input stream. Note that if there is no input piped into the command, the terminal will allow the user to enter any text and treat that as the app’s standard input. In Ruby, the standard input is available via the constant STDIN.
For apps that use a list of files as their source of input, Ruby provides the class ARGF in its standard library, which implements the exact behavior described here. See ARGF: Automatically Read from Files or Standard Input for more on how to use this.
How could todo use the standard input stream as a default source of input? Consider setting up your to-do list for the first time. You probably have a lot of tasks to input initially, and given the way todo is implemented, you’d have to call todo new for each one of them. If, on the other hand, todo new accepted task names from the standard input, you could enter them much more quickly, like so:
|
$ todo new |
|
Rake Leaves |
|
Take out trash |
|
Clean garage |
|
Put away Dishes |
|
^D |
|
$ todo list |
|
1 - Rake Leaves |
|
Created: Mon Aug 15 21:01:35 EDT 2011 |
|
2 - Take out trash |
|
Created: Mon Aug 15 21:01:35 EDT 2011 |
|
3 - Clean garage |
|
Created: Mon Aug 15 21:01:35 EDT 2011 |
|
4 - Put away Dishes |
|
Created: Mon Aug 15 21:01:35 EDT 2011 |
To implement this, we simply check whether the argument list given to the new command is empty and, if so, use the readlines method on STDIN to read each new task, one at a time. We’ll let the user know that we’re doing this so they don’t get confused when the app patiently waits for input. In our continued desire to be helpful, we’ll add a check to see whether the standard input contained tasks and send a message to the user if no tasks were created.
make_easy_possible/todo/bin/todo |
|
|
c.action do |global_options,options,task_names| |
|
File.open(global_options[:filename],'a+') do |todo_file| |
*
|
if task_names.empty? |
*
|
puts "Reading new tasks from stdin..." |
*
|
task_names = STDIN.readlines.map { |a| a.chomp } |
*
|
end |
*
|
|
|
tasks = 0 |
|
task_names.each do |task| |
|
todo_file.puts [task,Time.now].join(',') |
|
tasks += 1 |
|
end |
|
|
*
|
if tasks == 0 |
*
|
raise "You must provide tasks on the command-line or standard input" |
*
|
end |
|
end |
The first block of highlighted lines checks whether task_names is empty and, if so, assigns it to each line of the standard input stream. Since these strings contain newlines, we use one of Ruby’s famous one-liners to remove them by combining map , which maps array elements, with chomp , which removes newlines from the ends of strings.
The final highlighted bit of code shows our helpful error handling. Recall that in a GLI-based app, we can safely raise an exception; our app will exit nonzero, and the exception’s message will be shown to the user (without the nasty backtrace).
If your app’s arguments don’t represent input or don’t locate input for your app to process, a default value for its arguments might not make sense, though it’s still worth considering. ls’s arguments could be thought of as having a default of “the current directory.” That default makes a lot of sense for ls because it’s nondestructive and represents the normal, expected use case of ls: list the files in the current directory. There’s no better example of making the common things easy. It might not be this straightforward for your app, but ideally we’ve given you some things to think about.
Now that we know how to name and set defaults for options, flags, and arguments, let’s take our final step in understanding our guiding principles: default behavior.
5.3 Deciding Default Behavior
The default behavior is the behavior of our app in the absence of options; for example, db_backup.rb’s default behavior is to compress database backups. Choosing the best default behavior is highly dependent on what your app does; however, there are two behaviors common to many command-line apps: modifying the system and producing output. Let’s look at these two common behaviors to see how we apply our guiding principles to choosing default behavior.
We’ll tackle modifying the system first, because it’s the most common and the most important. By “modifying the system,” we mean the creation, modification, and removal of files. This is where we finally apply our third guiding principle: “Don’t be destructive by default.”
Preventing Destructive Actions by Default
One of the most destructive commands on any UNIX system is rm; it deletes files. When you just type rm on the command line, however, nothing bad happens:
|
$ rm |
|
usage: rm [-f | -i] [-dPRrvW] file ... |
|
unlink file |
|
# => Hard drive NOT deleted |
rm is not only helpful (as we’ve come to expect from great applications) but also nondestructive by default; nothing changed in our environment or on our machine from having run rm without any arguments. rm will even go so far as to ask permission before deleting a file that you own but that doesn’t have write permissions. Our apps need to take the same care with their users’ data as rm and prevent destructive behavior by default; however, we do want to allow such behavior in our continued effort to making uncommon or dangerous things possible.
Unless you’re writing an app that deletes files, it might not be clear what’s destructive and what isn’t. A good rule of thumb is to think of destructive behavior as any irreversible action that occurs outside of the normal operations of the application. For example, adding a new task using todo new is not destructive; adding a task is the entire point of the new command. If db_backup.rb overwrote an existing backup without asking permission, however, that would be destructive.
Thinking about which behavior of an app is destructive is a great way to differentiate the common things from the uncommon things and thus drive some of your design decisions. Any feature that does something destructive shouldn’t be a feature we make easy to use, but we should make it possible.
Let’s change db_backup.rb to avoid, but allow, its currently destructive behavior. First, we’ll check for an existing file before we start backing up and exit nonzero with an error message in that case. Second, we’ll add a new flag, --force, that will allow skipping this check. This requires three distinct changes to our app.
make_easy_possible/db_backup/bin/db_backup.rb |
|
|
option_parser = OptionParser.new do |opts| |
|
# ... |
① |
opts.on("--[no-]force","Overwrite existing files") do |force| |
|
options[:force] = force |
|
end |
|
|
|
end |
|
auth = "" |
|
auth += "-u#{options[:user]} " if options[:user] |
|
auth += "-p#{options[:password]} " if options[:password] |
|
|
|
database_name = ARGV[0] |
|
output_file = "#{database_name}.sql" |
|
command = "/usr/local/mysql/bin/mysqldump " + |
|
"#{auth}#{database_name} > #{output_file}" |
|
|
② |
if File.exists? output_file |
|
if options[:force] |
③ |
STDERR.puts "Overwriting #{output_file}" |
|
else |
|
STDERR.puts "error: #{output_file} exists, use --force to overwrite" |
|
exit 1 |
|
end |
|
end |
There’s a lot going on here to make db_backup.rb nondestructive by default but without sacrificing the improvements we’ve made to make it an awesome app.
①
Here, we add the --force option, but we also allow for --no-force to explicitly call out the default behavior on the command line. In a similar vein to accepting long-form options for readability in automation scripts, adding “negatable” switches can be advantageous. When someone sees --no-force as a switch to db_backup.rb in an automation script, they’ll instantly know that the app is going to avoid being destructive without having to check the documentation about what the default behavior is.
②
Here, we implement the basic logic to check for the file’s existence and either exit with an error or allow the file to be overwritten, based on the value of options[:force].
③
Notice here how we still message the user (using the standard error stream, where such messages should go) that we’re overwriting a file. It’s always a good idea for an app to let the user know something destructive is happening, even when the user explicitly requested it.
Preventing destructive behavior is the most important default behavior your app can have. The second common behavior we want to look at is how it produces output. If your app produces output in a user-selectable format, choosing the default is important, and you should make that choice based on where that output is going.
Choosing the Best Default Output Format Based on Context
In Chapter 4, Play Well with Others, we chose the “pretty” output format as the default. It turns out, we can be smarter about the format of our output by taking into account the way in which our app was invoked. ls does this; we saw previously that the -1 option tells ls to format its output one file per line, which we used to put ls into a pipeline. ls will use this format by default if its output is sent to a file or another application. This is an excellent example of how ls makes two common things simple through default ls at the terminal, we see a nicely formatted output format. When using it as part of a pipeline, it uses a more machine-friendly format.
We want our apps to do this, as well. In general, we don’t want formatted or colorful output going to files or to the input of another application by default; we want to use the machine-friendly format in these cases. Let’s enhance todo to choose its default format based on where its output is going.
To determine where the output is going, we can use the method tty? of the IO class to see whether the output is a terminal or not (TTY is an abbreviation for a teletypewriter[38] but is used in UNIX systems to refer to the terminal). Since STDOUT is an instance of IO, we can call tty? on it to find out where our output is going. Based on that, we can choose the default format.
make_easy_possible/todo/bin/todo |
|
|
desc 'List tasks' |
|
command [:list,:ls] do |c| |
|
|
|
c.desc 'Format of the output (pretty for TTY, csv otherwise)' |
|
c.arg_name 'csv|pretty' |
*
|
# explicit default removed |
|
c.flag :format |
|
|
|
c.action do |global_options,options,args| |
*
|
if options[:format].nil? |
*
|
if STDOUT.tty? |
|
options[:format] = 'pretty' |
|
else |
|
options[:format] = 'csv' |
|
end |
|
end |
|
|
|
# ... |
|
|
|
end |
|
end |
Note that we removed the explicit default value for the --format flag. This way, we can check whether it’s nil (meaning the user didn’t provide a value for it on the command line), and then check whether STDOUT is a terminal. Also note that since we removed the call to default_value , we had to augment the help text to explain how the default value is chosen.
Avoiding destructive behavior and sensibly choosing output formats are the most common types of behavior you’ll need to consider for any given command-line app, but your app will do many other things. Choosing sensible defaults for them is dependent on the specifics of your app; these are design decisions you’ll have to make. They will communicate to your users how your app should be used. As with the other issues we’ve discussed in this chapter, an easy way to choose appropriate default behavior is to think about how you want to use the app.
5.4 Moving On
Starting with our guiding principles of making common tasks easy, making uncommon tasks possible, and avoiding destructive behavior, we’ve discovered several guidelines to help us design our apps for an awesome user experience. They’re summarized in the following list. Overall, you want to have an opinion about how your app is supposed to work and reflect that opinion in the names and defaults you choose and how your app behaves.
Use short-form option names only for common and nondestructive options.
Short-form options are easy to use and should affect only common behaviors; this reinforces the desired usage patterns of your app. Options available only in long form indicate to the user that they are uncommon or dangerous.
Short-form options should be mnemonics for the behavior they control.
Mnemonics are a common and easy way to help users remember things.
Always provide a long-form option, and use it when scripting your app.
Long-form options’ verbose nature allows users reading invocations of your app to better understand what’s being configured by the options.
Long-form options should be as clear as possible; don’t skimp on letters.
Long-form options are designed to be well-understood by infrequent users, so they need to be descriptive.
For command suites, use abbreviations or mnemonics as aliases for common commands.
Frequent users will appreciate using two- or three-character aliases for common commands to your command suite, such as ls for list or cp for copy. These short-form aliases also help to reinforce what the common commands are.
Files that configure or drive the behavior of your app and persist across invocations should be located, by default, in the user’s home directory, and they should be hidden (i.e., they should start with a .).
The user’s home directory is a safe sandbox that allows your app to create and manage files without fear of clashing with other users on the system. Hiding them is common practice so the user doesn’t see them in directory listings.
Name files uniquely and in common locations (such as /tmp) to avoid collisions.
Occasionally, you will need to create files in locations outside of the user’s home directory. By using unique information, such as the process identifier, in filenames, you ensure that the app behaves well and doesn’t interfere with other users of the app.
Use the standard input stream as the default source for input.
Accepting input from the standard input stream allows your app to be more easily used in a pipeline and thus allows it to play well with others. It’s also the default that most command-line apps use, so this will be expected by seasoned command-line users.
Do not delete or overwrite files as a side effect of what your app does without explicit instructions from the user (typically via a command-line switch).
Respect users’ data; they will trust and use your app more if it’s well-behaved and doesn’t cause irreversible damage to their environment.
Choose the output form of your app based upon the destination of the output.
Even if your app produces human-friendly output by default, when that output is redirected to a file or to the input of another app, default to a machine-readable format. Using the context of where the output is going optimizes your app, making two common cases very simple.
What if users don’t like our defaults? For example, what if a db_backup.rb user wants the backup file to be overwritten all the time and isn’t happy about typing --force on the command line all the time? What if a todo user doesn’t like new tasks being added with the lowest priority? Is there a way to make the experience of these users just as good as for everyone else?
In the next chapter, we’ll learn how to make our applications configurable in an easy way that will allow users to customize the default behavior of our apps, all without sacrificing ease of use, helpfulness, or interoperability.
Footnotes
[33] |
http://en.wikipedia.org/wiki/Rc_file |
[34] |
http://www.multicians.org/shell.html |
[35] |
http://cukes.info |
[36] |
http://www.rspec.info |
[37] |
http://en.wikipedia.org/wiki/JSON |
[38] |
http://en.wikipedia.org/wiki/Teletypewriter |