Be Easy to Use - Build Awesome Command-Line Applications in Ruby 2: Control Your Computer, Simplify Your Life (2013)

Build Awesome Command-Line Applications in Ruby 2: Control Your Computer, Simplify Your Life (2013)

Chapter 2. Be Easy to Use

After installing your app, the first experience a user has with it will be the actual command-line interface. If the interface is difficult, counterintuitive, or, well, ugly, it’s not going to inspire a lot of confidence, and your users will have a hard time using it to achieve its clear and concise purpose. Conversely, if it’s easy to use, your interface will give your application an edge with its audience.

Fortunately, it’s easy to get the command-line interface right, once you know the proper tools and techniques. The UNIX command line has a long and storied history, and there are now many conventions and idioms for how to invoke a command-line app. If your app follows these conventions, your users will have an easier time using it. We’ll see that even a highly complex app can have a succinct and memorable interface.

In this chapter, we’ll learn to use standard library and open source community tools that make it incredibly simple to create a conventional, idiomatic command-line interface whether it’s a simple backup script or a complex command-line task management system. We’ll learn how to make a simple command-line interface using Ruby’s OptionParser class and then tackle a more sophisticated command-suite application, which we’ll build using the open source GLI library. But first, we need to get familiar with the proper names of the elements of a typical command-line interface: its options, arguments, and commands.

2.1 Understanding the Command Line: Options, Arguments, and Commands

To tell a command-line application how to do its work, you typically need to enter more than just the name of its executable. For example, we must tell grep which files we want it to search. The database backup app, db_backup.rb, that we introduced in the previous chapter needs a username and password and a database name in order to do its work. The primary way to give an app the information it needs is via options and arguments , as depicted in Figure 1, Basic parts of a command-line app invocation. Note that this format isn’t imposed by the operating system but is based on the GNU standard for command-line apps.[13] Before we learn how to make a command-line interface that can parse and accept options and arguments, we need to delve a bit deeper into their idioms and conventions. We’ll start with options and move on to arguments. After that, we’ll discuss commands , which are a distinguishing feature of command suites.

images/be_easy_to_use/command_line_simple.png


Figure 1. Basic parts of a command-line app invocation

Options

Options are the way in which a user modifies the behavior of your app. Consider the two invocations of ls shown here. In the first, we omit options and see the default behavior. In the second, we use the -l option to modify the listing format.

$ ls

one.jpg two.jpg three.jpg

$ ls -l

-rw-r--r-- 1 davec staff 14005 Jul 13 19:06 one.jpg

-rw-r--r-- 1 davec staff 14005 Jul 11 13:06 two.jpg

-rw-r--r-- 1 davec staff 14005 Jun 10 09:45 three.jpg

Options come in two forms: long and short.

Short-form options

Short-form options are preceded by a dash and are only one character long, for example -l. Short-form options can be combined after a single dash, as in the following example. For example, the following two lines of code produce exactly the same result:

ls -l -a -t

ls -lat

Long-form options

Long-form options are preceded by two dashes and, strictly speaking, consist of two or more characters. However, long-form options are usually complete words (or even several words, separated by dashes). The reason for this is to be explicit about what the option means; with a short-form option, the single letter is often a mnemonic. With long-form options, the convention is to spell the word for what the option does. In the command curl --basic http://www.google.com, for example, --basic is a single, long-form option. Unlike short options, long options cannot be combined; each must be entered separately, separated by spaces on the command line.

Command-line options can be one of two types: switches , which are used to turn options on and off and do not take arguments, and flags , which take arguments, as shown in Figure 2, A command-line invocation with switches and flags. Flags typically require arguments but, strictly speaking, don’t need to do so. They just need to accept them. We’ll talk more about this in Chapter 5, Delight Casual Users.

images/be_easy_to_use/switches_and_flags.png


Figure 2. A command-line invocation with switches and flags

Typically, if a switch is in the long-form (for example --foo), which turns “on” some behavior, there is also another switch preceded with no- (for example --no-foo) that turns “off” the behavior.

Finally, long-form flags take their argument via an equal sign, whereas in the short form of a flag, an equal sign is typically not used. For example, the curl command, which makes HTTP requests, provides both short-form and long-form flags to specify an HTTP request method: -X and --request, respectively. The following example invocations show how to properly pass arguments to those flags:

curl -X POST http://www.google.com

curl --request=POST http://www.google.com

Although some apps do not require an equal sign between a long-form flag and its argument, your apps should always accept an equal sign, because this is the idiomatic way of giving a flag its argument. We’ll see later in this chapter that the tools provided by Ruby and its open source ecosystem make it easy to ensure your app follows this convention.

Arguments

As shown in Figure 1, Basic parts of a command-line app invocation, arguments are the elements of a command line that aren’t options. Rather, arguments represent the objects that the command-line app will operate on. Typically, these objects are file or directory names, but this depends on the app. We might design our database backup app to treat the arguments as the names of the databases to back up.

Not all command-line apps take arguments, while others take an arbitrary number of them. Typically, if your app operates on a file, it’s customary to accept any number of filenames as arguments and to operate on them one at a time.

Commands

Figure 1, Basic parts of a command-line app invocation shows a diagram of a basic command-line invocation with the main elements of the command line labeled.

For simple command-line applications, options and arguments are all you need to create an interface that users will find easy to use. Some apps, however, are a bit more complicated. Consider git, the popular distributed version control system. git packs a lot of functionality. It can add files to a repository, send them to a remote repository, examine a repository, or fetch changes from another user’s repository. Originally, git was packaged as a collection of individual command-line apps. For example, to commit changes, you would execute the git-commit application. To fetch files from a remote repository, you would execute git-fetch. While each command provided its own options and arguments, there was some overlap.

For example, almost every git command provided a --no-pager option, which told git not to send output through a pager like more. Under the covers, there was a lot of shared code as well. Eventually, git was repackaged as a single executable that operated as a command suite . Instead of running git-commit, you run git commit. The single-purpose command-line app git-commit now becomes a command to the new command-suite app, git.

A command in a command-line invocation isn’t like an option or an argument; it has a more specific meaning. A command is how you specify the action to take from among a potentially large or complex set of available actions. If you look around the Ruby ecosystem, you’ll see that the use of command suites is quite common. gem, rails, and bundler are all types of command suites.

Figure 3, Basic parts of a command-suite invocation shows a command-suite invocation, with the command’s position on the command line highlighted.

images/be_easy_to_use/command_line_suite.png


Figure 3. Basic parts of a command-suite invocation

You won’t always design your app as a command suite; only if your app is complex enough that different behaviors are warranted will you use this style of interface. Further, if you do decide to design your app as a command suite, your app should require a command (we’ll talk about how your app should behave when the command is omitted in Chapter 3, Be Helpful).

The command names in your command suite should be short but expressive, with short forms available for commonly used or lengthier commands. For example, Subversion, the version control system used by many developers, accepts the short-form co in place of its checkout command.

A command suite can still accept options; however, their position on the command line affects how they are interpreted.

Global options

Options that you enter before the command are known as global options . Global options affect the global behavior of an app and can be used with any command in the suite. Recall our discussion of the --no-pager option for git? This option affects all of git’s commands. We know this because it comes before the command on the command line, as shown in Figure 3, Basic parts of a command-suite invocation.

Command options

Options that follow a command are known as command-specific options or simply command options. These options have meaning only in the context of their command. Note that they can also have the same names as global options. For example, if our to-do list app took a global option -f to indicate where to find the to-do list’s file, the list command might also take an -f to indicate a “full” listing.

The command-line invocation would be todo -f ~/my_todos.txt list -f. Since the first -f comes before the command and is a global option, we won’t confuse it for the second -f, which is a command option.

Most command-line apps follow the conventions we’ve just discussed. If your app follows them as well, users will have an easier time learning and using your app’s interface. For example, if your app accepts long-form flags but doesn’t allow the use of an equal sign to separate the flag from its argument, users will be frustrated.

The good news is that it’s very easy to create a Ruby app that follows all of the conventions we’ve discussed in this section. We’ll start by enhancing our Chapter 1 database backup app from Chapter 1, Have a Clear and Concise Purpose to demonstrate how to make an easy-to-use, conventional command-line application using OptionParser. After that, we’ll use GLI to enhance our to-do list app, creating an idiomatic command suite that’s easy for our users to use and easy for us to implement.

2.2 Building an Easy-to-Use Command-Line Interface

If you’ve done a lot of shell scripting (or even written a command-line tool in C), you’re probably familiar with getopt,[14] which is a C library for parsing the command line and an obvious choice as a tool for creating your interface. Although Ruby includes a wrapper for getopt, you shouldn’t use it, because there’s a better built-in option: OptionParser. As you’ll see, OptionParser is not only easy to use but is much more sophisticated than getopt and will result in a superior command-line interface for your app. OptionParser code is also easy to read and modify, making enhancements to your app simple to implement.

Before we see how to use OptionParser, let’s first consider the input our application needs to do its job and the command line that will provide it. We’ll use the backup application, db_backup.rb, which we introduced in Chapter 1, Have a Clear and Concise Purpose. What kind of options might our application need?

Right now, it needs the name of a database and some way of knowing when we’re doing an “end-of-iteration” backup instead of a normal, daily backup. The app will also need a way to authenticate users of the database server we’re backing up; this means a way for the user to provide a username and password.

Since our app will mostly be used for making daily backups, we’ll make that its default behavior. This means we can provide a switch to perform an “end-of-iteration” backup. We’ll use -i to name the switch, which provides a nice mnemonic (i for “iteration”). For the database user and password, -u and -p are obvious choices as flags for the username and password, respectively, as arguments.

To specify the database name, our app could use a flag, for example -d, but the database name actually makes more sense as an argument. The reason is that it really is the object that our backup app operates on. Let’s look at a few examples of how users will use our app:

$ db_backup.rb small_client

# => does a daily backup of the "small_client" database

$ db_backup.rb -u davec -p P@55WorD medium_client

# => does a daily backup of the "medium_client" database, using the

# given username and password to login

$ db_backup.rb -i big_client

# => Do an "end of iteration" backup for the database "big_client"

Now that we know what we’re aiming for, let’s see how to build this interface with OptionParser.

Building a Command-Line Interface with OptionParser

To create a simple command-line interface with OptionParser, create an instance of the class and pass it a block. Inside that block, we create the elements of our interface using OptionParser methods. We’ll use on to define each option in our command line.

The on itself takes a block, which is called when the user invokes the option it defines. For flags, the block is given the argument the user provided. The simplest thing to do in this block is to simply store the option used into a Hash, storing “true” for switches and the block argument for flags. Once the options are defined, use the parse! method of our instantiated OptionParser class to do the actual command-line parsing. Here’s the code to implement the iteration switch and username and password flags of our database application:

be_easy_to_use/db_backup/bin/db_backup.rb

#!/usr/bin/env ruby

# Bring OptionParser into the namespace

require 'optparse'

options = {}

option_parser = OptionParser.new do |opts|

# Create a switch

opts.on("-i","--iteration") do

options[:iteration] = true

end

# Create a flag

opts.on("-u USER") do |user|

options[:user] = user

end

opts.on("-p PASSWORD") do |password|

options[:password] = password

end

end

option_parser.parse!

puts options.inspect

As you can see by inspecting the code, each call to on maps to one of the command-line options we want our app to accept. What’s not clear is how OptionParser knows which are switches and which are flags. There is great flexibility in the arguments to on , so the type of the argument, as well as its contents, controls how OptionParser will behave. For example, if a string is passed and it starts with a dash followed by one or more nonspace characters, it’s treated as a switch. If there is a space and another string, it’s treated as a flag. If multiple option names are given (as we do in the line opts.on("-i","--iteration")), then these two options mean the same thing.

Table 1, Overview of OptionParser parameters to on provides an overview of how a parameter to on will be interpreted; you can add as many parameters as you like, in any order. The complete documentation on how these parameters are interpreted is available on the rdoc for the make_switch method.[15]


Table 1. Overview of OptionParser parameters to on

Effect

Example

Meaning

Short-form switch

-v

The switch -v is accepted on the command line. Any number of strings like this may appear in the parameter list and will all cause the given block to be called.

Long-form switch

--verbose

The switch ––verbose is accepted. Any number of strings like this may appear in the parameter list and can be mixed and matched with the shorter form previously.

Negatable long-form switch

--[no-]verbose

Both ––verbose and ––no-verbose are accepted. If the no form is used, the block will be passed false; otherwise, true is passed.

Flag with required argument

-n NAME or --name NAME

The option is a flag , and it requires an argument. All other option strings provided as parameters will require flags as well (for example, if we added the string ––username after the -u USER argument in our code, then --username would also require an argument; we don’t need to repeat the USER in the second string). The value provided on the command line is passed to the block.

Flag with optional argument

-n [NAME] or --name [NAME]

The option is a flag whose argument is optional. If the flag’s argument is omitted, the block will still be called, but nil will be passed.

Documentation

Any other string

This is a documentation string and will be part of the help output.


In the blocks given to on , our code simply sets a value in our options hash. Since it’s just Ruby code, we can do more than that if we’d like. For example, we could sanity check the options and fail early if the argument to a particular flag were invalid.

Validating Arguments to Flags

Suppose we know that the usernames of all the database users in our systems are of the form first.last. To help our users, we can validate the value of the argument to -u before even connecting to the database. Since the block given to an on method call is invoked whenever a user enters the option it defines, we can check within the block for the presence of a period in the username value, as the following code illustrates:

be_easy_to_use/db_backup/bin/db_backup.rb

opts.on("-u USER") do |user|

unless user =~ /^.+\..+$/

raise ArgumentError,"USER must be in 'first.last' format"

end

options[:user] = user

end

Here, we raise an exception if the argument doesn’t match our regular expression; this will cause the entire option-parsing process to stop, and our app will exit with the error message we passed to raise.

You can probably imagine that in a complex command-line app, you might end up with a lot of argument validation. Even though it’s only a few lines of extra code, it can start to add up. Fortunately, OptionParser is far more flexible than what we’ve seen so far. The on method is quite sophisticated and can provide a lot of validations for us. For example, we could replace the code we just wrote with the following to achieve the same result:

be_easy_to_use/db_backup/bin/db_backup.rb

opts.on("-u USER",

*

/^.+\..+$/) do |user|

options[:user] = user

end

The presence of a regular expression as an argument to on indicates to OptionParser that it should validate the user-provided argument against this regular expression. Also note that if you include any capturing groups in your regexp (by using parentheses to delineate sections of the regexp), those values will be extracted and passed to the block as an Array. The raw value from the command line will be at index 0, and the extracted values will fill out the rest of the array.

You don’t have to use regular expressions for validation, however. By including an Array in the argument list to on , you can indicate the complete list of acceptable values. By using a Hash, OptionParser will use the keys as the acceptable values and send the mapped value to the block, like so:

servers = { 'dev' => '127.0.0.1',

'qa' => 'qa001.example.com',

'prod' => 'www.example.com' }

opts.on('--server SERVER',servers) do |address|

# for --server=dev, address would be '127.0.0.1'

# for --server=prod, address would be 'www.example.com'

end

Finally, if you provide a classname in the argument list, OptionParser will attempt to convert the string from the command line into an instance of the given class. For example, if you include the constant Integer in the argument list to on , OptionParser will attempt to parse the flag’s argument into an Integer instance for you. There is support for many conversions. See Type Conversions in OptionParser for the others available and how to make your own using the accept method.

Type Conversions in OptionParser

While strictly speaking it is not a user-facing feature, OptionParser provides a sophisticated facility for automatically converting flag arguments to a type other than String. The most common conversion is to a number, which can be done by including Integer, Float, or Numeric as an argument to on , like so:

ops.on('--verbosity LEVEL',Integer) do |verbosity|

# verbosity is not a string, but an Integer

end

OptionParser provides built-in conversions for the following: Integer, Float, Numeric, DecimalInteger, OctalInteger, DecimalNumeric, FalseClass, and TrueClass. Regexp support is provided, and it looks for a string starting and ending with a slash (/), for example --matches "/^bar/". OptionParser will also parse an Array, treating each comma as an item delimiter; for example, --items "foo,bar,blah" yields the list ["foo","bar","blah"].

You can write your own conversions as well, by passing the object and a block to the accept method on an OptionParser. The object is what you’d also pass to on to trigger the conversion (typically it would be a class). The block takes a string argument and returns the converted type.

You could use it to convert a string into a Hash like so:

opts.accept(Hash) do |string|

hash = {}

string.split(',').each do |pair|

key,value = pair.split(/:/)

hash[key] = value

end

hash

end

opts.on('--custom ATTRS',Hash) do |hash|

custom_attributes = hash

end

A command like foo --custom foo:bar,baz:quux will result in custom_attributes getting the value { ’foo’ => ’bar’, ’baz’ => ’quux’ }.

Automatic conversions like these can be very handy for complex applications.

By using OptionParser, we’ve written very little code but created an idiomatic UNIX-style interface that will be familiar to anyone using our app. We’ve seen how to use this to improve our backup app, but how can we create a similarly idiomatic interface for our to-do list app? Our to-do list app is actually a series of commands: “create a new task,” “list the tasks,” “complete a task.” This sounds like a job for the command-suite pattern.

OptionParser works great for a simple app like our backup app; however, it isn’t a great fit for parsing the command line of a command suite; it can be done, but it requires jumping through a lot more hoops. Fortunately, several open source libraries are available to make this job easy for us. We’ll look at one of them, GLI, in the next section.

2.3 Building an Easy-to-Use Command-Suite Interface

Command suites are more complex by nature than a basic automation or single-purpose command-line app. Since command suites bundle a lot of functionality, it’s even more important that they be easy to use. Helping users navigate the commands and their options is crucial.

Let’s revisit our to-do list app we discussed in Chapter 1, Have a Clear and Concise Purpose. We’ve discussed that the command-suite pattern is the best approach, and we have already identified three commands the app will need: “new,” “list,” and “done” to create a new task, list the existing tasks, and complete a task, respectively.

We also want our app to provide a way to locate the to-do list file we’re operating on. A global option named -f would work well (f being a mnemonic for “file”). It would be handy if our “new” command allowed us to set a priority or place a new task directly at the top of our list. -p is a good name for a flag that accepts a priority as an argument, and we’ll use -f to name a switch that means “first in the list.”

We’ll allow our list command to take a sort option, so it will need a flag named -s. done won’t need any special flags right now. Let’s see a few examples of the interface we want to create:

$ todo new "Rake leaves"

# => Creates a new todo in the default location

$ todo -f /home/davec/work.txt new "Refactor database"

# => Creates a new todo in /home/davec/work.txt instead

# of the default

$ todo -f /home/davec/work.txt new "Do design review" -f

# => Create the task "Do design review" as the first

# task in our task list in /home/davec/work.txt

$ todo list -s name

# => List all of our todos, sorted by name

$ todo done 3

# => Complete task #3

Unfortunately, OptionParser was not built with command suites in mind, and we can’t directly use it to create this sort of interface. To understand why, look at our third invocation of the new command: both the “filename” global flag and the command-specific “first” switch have the same name: -f. If we ask OptionParser to parse that command line, we won’t be able to tell which -f is which.

A command-line interface like this is too complex to do “by hand.” What we need is a tool custom-built for parsing the command line of a command suite.

Building a Command Suite with GLI

Fortunately, many open source tools are available to help us parse the command-suite interface we’ve designed for our to-do list app. Three common ones are commander,[16] thor,[17] and GLI.[18] They are all quite capable, but we’re going to use GLI here. GLI is actively maintained, has extensive documentation, and was special-built for making command-suite apps very easily (not to mention written by the author of this book). Its syntax is similar to commander and thor, with all three being inspired by rake; therefore, much of what we’ll learn here is applicable to the other libraries (we’ll see how to use them in a bit more depth in Appendix 1, Common Command-Line Gems and Libraries).

Rather than modify our existing app with GLI library calls, we’ll take advantage of a feature of GLI called scaffolding. We’ll use it to bootstrap our app’s UI and show us immediately how to declare our user interface.

Building a Skeleton App with GLI’s scaffold

Once we install GLI, we can use it to bootstrap our app. The gli application is itself a command suite, and we’ll use the scaffold command to get started. gli scaffold takes an arbitrary number of arguments, each representing a command for our new command suite. You don’t have to think of all your commands up front. Adding them later is simple, but for now, as the following console session shows, it’s easy to set up the commands you know you will need. For our to-do app, these include new, list, and done.

$ gem install gli

Successfully installed gli-2.8.0

1 gem installed

$ gli scaffold todo new list done

Creating dir ./todo/lib...

Creating dir ./todo/bin...

Creating dir ./todo/test...

Created ./todo/bin/todo

Created ./todo/README.rdoc

Created ./todo/todo.rdoc

Created ./todo/todo.gemspec

Created ./todo/test/default_test.rb

Created ./todo/test/test_helper.rb

Created ./todo/Rakefile

Created ./todo/Gemfile

Created ./todo/features

Created ./todo/lib/todo/version.rb

Created ./todo/lib/todo.rb

Don’t worry about all those files that scaffold creates just yet; we’ll explain them in future chapters. Now, let’s test the new interface before we look more closely at the code:

$ cd todo

$ bundle exec bin/todo new

$ bundle exec bin/todo done

$ bundle exec bin/todo list

$ bundle exec bin/todo foo

error: Unknown command 'foo'. Use 'todo help' for a list of commands

As you can see from the session dialog, our scaffolded app recognizes our commands, even though they’re not yet implemented. We even get an error when we try to use the command foo, which we didn’t declare. Don’t worry about bundle exec; we’ll explain the usage in future chapters.

Let’s now look at the code GLI produces to see how it works. As you can see, GLI generated only the code it needs to parse the commands we passed as arguments to the scaffold command. The switches and flags set by GLI are provided here as examples. We’ll cover how to customize them later.

We’ll go through the generated code step by step. First, we need to set up our app to bring GLI’s libraries in, via a require and an include.

be_easy_to_use/todo/bin/todo

#!/usr/bin/env ruby

require 'gli'

include GLI::App

Since we’ve included GLI, the remaining code is mostly method calls from the GLI module.[19] The next thing the code does is to declare some global options.

be_easy_to_use/todo/bin/todo

switch :s

flag [:f,:filename]

This declares that the app accepts a global switch -s and a global flag -f. Remember, these are just examples; we’ll change them later to meet our app’s requirements. Next, the code defines the new command:

be_easy_to_use/todo/bin/todo

command :new do |c|

c.switch :s

c.flag :f

c.action do |global_options,options,args|

# Your command logic here

# If you have any errors, just raise them

# raise "that command made no sense"

end

end

The block given to command establishes a context to declare command-specific options via the argument passed to the block (c). GLI has provided an example of command-specific options by declaring that the new command accepts a switch -s and a flag -f. Finally, we call the action method on c and give it a block. This block will be executed when the user executes the new command and is where we’d put the code to implement new. The block will be given the parsed global options, the parsed command-specific options, and the command-line arguments via global_options, options, and args, respectively.

GLI has generated similar code for the other commands we specified to gli scaffold:

be_easy_to_use/todo/bin/todo

command :list do |c|

c.action do |global_options,options,args|

end

end

command :done do |c|

c.action do |global_options,options,args|

end

end

The last step is to ask GLI to parse the command line and run our app. The run method returns with an appropriate exit code for our app (we’ll learn all about exit codes in Chapter 4, Play Well with Others).

be_easy_to_use/todo/bin/todo

exit run(ARGV)

GLI has provided us with a skeleton app that parses the command line for us; all we have to do is fill in the code (and replace GLI’s example options with our own).

Turning the Scaffold into an App

As we discussed previously, we need a global way to specify the location of the to-do list file, and we need our new command to take a flag to specify the position of a new task, as well as a switch to specify “this task should go first.” The list command needs a flag to control the way tasks are sorted.

Here’s the GLI code to make this interface. We’ve also added some simple debugging, so when we run our app, we can see that the command line is properly parsed.

be_easy_to_use/todo/bin/todo_integrated.rb

*

flag :f

command :new do |c|

*

c.flag :priority

*

c.switch :f

c.action do |global_options,options,args|

puts "Global:"

puts "-f - #{global_options[:f]}"

puts "Command:"

puts "-f - #{options[:f] ? 'true' : 'false'}"

puts "--priority - #{options[:priority]}"

puts "args - #{args.join(',')}"

end

end

command :list do |c|

*

c.flag :s

c.action do |global_options,options,args|

puts "Global:"

puts "-f - #{global_options[:f]}"

puts "Command:"

puts "-s - #{options[:s]}"

end

end

command :done do |c|

c.action do |global_options,options,args|

puts "Global:"

puts "-f - #{global_options[:f]}"

end

end

The highlighted code represents the changes we made to what GLI generated. We’ve removed the example global and command-specific options and replaced them with our own. Note that we can use both short-form and long-form options; GLI knows that a single-character symbol like :f is a short-form option but a multicharacter symbol like :priority is a long-form option.

We also added some calls to puts that demonstrate how we access the parsed command line (in lieu of the actual logic of our to-do list app). Let’s see it in action:

$ bin/todo -f ~/todo.txt new -f "A new task" "Another task"

Global:

-f - /Users/davec/todo.txt

Command:

-f - true

-p -

args - A new task,Another task

We can see that :f in global_options contains the file specified on the command line; that options[:f] is true, because we used the command-specific option -f; and that options[:priority] is missing, since we didn’t specify that on the command line at all.

Once we’ve done this, we can add our business logic to each of the c.action blocks, using global_options, options, and args as appropriate. For example, here’s how we might implement the logic for the to-do app list command:

c.action do |global_options,options,args|

todos = read_todos(global_options[:filename])

if options[:s] == 'name'

todos = todos.sort { |a,b| a <=> b }

end

todos.each do |todo|

puts todo

end

end

We’ve used very few lines of code yet can parse a sophisticated user interface. It’s a UI that users will find familiar, based on their past experience with other command suites. It also means that when we add more features to our app, it’ll be very simple.

Is there anything else that would be helpful to the user on the command line? Other than some help documentation (which we’ll develop in the next chapter), it would be nice if users could use the tab-completion features of their shell to help complete the commands of our command suite. Although our to-do app has only three commands now, it might need more later, and tab completion is a big command-line usability win.

Adding Tab Completion with GLI help and bash

An advantage of defining our command-suite’s user interface in the declarative style supported by GLI is that the result provides us with a model of our UI that we can use to do more than simply parse the command line.

We can use this model, along with the sophisticated completion function of bash, to let the user tab-complete our suite’s commands. First we tell bash that we want special completion for our app, by adding this to our ~/.bashrc and restarting our shell session:

complete -F get_todo_commands todo

The complete command tells bash to run a function (in our case, get_todo_commands ) whenever a user types the command (in our case, todo) followed by a space and some text (optionally) and then hits the a Tab key (i.e., is asked to complete something). complete expects the function to return the possible matches in the shell variable COMPREPLY, as shown in the implementation of get_todo_commands (which also goes in our .bashrc):

function get_todo_commands()

{

if [ -z $2 ] ; then

COMPREPLY=(`todo help -c`)

else

COMPREPLY=(`todo help -c $2`)

fi

}

Every GLI-powered app includes a built-in command called help that is mostly used for getting online help (we’ll see more about this in the next chapter). This command also takes a switch and an optional argument you can use to facilitate tab completion.

The switch -c tells help to output the app’s commands in a format suitable for bash completion. If the argument is also provided, the app will list only those commands that match the argument. Since our bash function is given an optional second argument representing what the user has entered thus far on the command line, we can use that to pass to help.

The end result is that your users can use tab completion with your app, and the chance of entering a nonexistent command is now very minimal—all without having to lift a finger! Note that for this to work, you must have todo installed in your PATH (we’ll see how users can do this in Chapter 7, Distribute Painlessly).

$ todo help -c

done

help

list

new

$ todo <TAB>

done help list new

$ todo d<TAB>

$ todo done

2.4 Moving On

We’ve learned in this chapter how simple it is to make an easy-to-use interface for a command-line application using built-in or open source libraries. With tools like OptionParser and GLI, you can spend more time on your app and rest easy knowing your user interface will be top notch and highly usable, even as you add new and more complex features.

Now that we know how to easily design and parse a good command-line interface, we need to find a way to let the user know how it works. In the next chapter, we’ll talk about in-app help, specifically how OptionParser and GLI make it easy to create and format help text, as well as some slightly philosophical points about what makes good command-line help.

Footnotes

[13]

http://www.gnu.org/prep/standards/html_node/Command_002dLine-Interfaces.html

[14]

http://en.wikipedia.org/wiki/Getopt

[15]

http://ruby-doc.org/stdlib-2.0.0/libdoc/optparse/rdoc/OptionParser.html#method-i-make_switch

[16]

http://visionmedia.github.com/commander/

[17]

https://github.com/wycats/thor

[18]

https://github.com/davetron5000/gli

[19]

http://davetron5000.github.io/gli/rdoc/classes/GLI.html