Add Color, Formatting, and Interactivity - 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 10. Add Color, Formatting, and Interactivity

We could end this book right here, and you’d be fully capable of making awesome command-line applications. The conventions, techniques, and rules we’ve discussed are always safe to use and will never steer you wrong. But, this is not the end of what we can learn. If you followed along during Chapter 8, Test, Test, Test, you saw that Cucumber produced colorful output. If you’ve ever used a SQL client, you’ve seen that it both formats output in tables and provides an interactive prompt, complete with history and tab completion.

In general, your apps should not provide any of these things; avoiding color, formatted output, and user interaction make our apps interoperable with other systems and apps. Sometimes, however, these features are either called for or can greatly enhance the experience of your users. Imagine a SQL client that didn’t format output in tables; it would be very difficult to use for examining a database.

This chapter is about the when and how of creating a “rich” user interface. We’ll discuss three ways to create such an enhanced user interface. First, we’ll look at using color, specifically how to use ANSI escape sequences via the open source gem rainbow. Next, we’ll learn about formatted output, using the open source library terminal-table to create tabular output. Finally, we’ll talk about creating an interactive user experience using readline, which is built in to Ruby. We’ll talk about when to consider using each feature before diving in and seeing how it works.

One rule applies to all of these situations, and it’s a rule you should never break. No matter how much you enhance the output formatting or how much of an interactive experience you want to make, always provide a UNIX-style, noninteractive, machine-friendly mode. We saw how to do this in Chapter 4, Play Well with Others, and starting from a machine-friendly interface is always a good first step. You may choose to default to colorful output or interactive input, but always allow the user to use the app in a vanilla UNIX-compliant way through the use of command-line options.

With that being said, let’s see how to add color to our apps.

10.1 Adding Color Using ANSI Escape Sequences

Cucumber is a great example of a command-line app that uses color effectively. Cucumber wants to motivate the user to work in a certain way. The user first writes a feature file and runs it, seeing a stream of red steps output to the terminal. As the user works to “fix” each step, the steps turn green until all the steps of all scenarios of the feature are passing, showing a pleasant stream of green steps to the terminal. Working this way can be quite satisfying, and Cucumber’s use of color is a huge part of this.

We’ll learn how to make our apps use color, by using the rainbow gem, which provides an easy way to manipulate the ANSI escape sequences that allow a terminal to show color. Before that, however, we must understand when it’s OK, and our observation of Cucumber provides a good example.

When to Use Color

The authors of Cucumber have a strong opinion about how to use it, and their use of color reflects this opinion. You might think of this as some form of negative, and then positive, reinforcement, but it’s really just reporting information to the user in a way that can be scanned and quickly understood. If you see a lot of green, you don’t need to read further: everything is OK. If you see some red or yellow fly by, you’ll need to scroll back to see what the problem was.

If your app has a feature that reports some sort of status, especially among a large set of data, color could be a great benefit to your users. Suppose you have an application that monitors processes; you might show the process name in green if it’s functioning properly, yellow if it’s responding slowly, and red if it’s not responding at all. A user running your program can quickly get a summary of what’s going on without having to read each line carefully.

Even if your app isn’t strictly monitoring things, color is still a great way to catch the user’s eye. Suppose your app generates a lot of terminal output; coloring error messages red will help alert the user that something is wrong. Without the colored output, the user will have to look much more closely to see error messages scroll by. Or consider ack,[48] which is an alternative to grep. It uses color and styling to draw the user’s attention to parts of a line that matched the search string.

You could also use color purely for decoration. The OS X package manager homebrew[49] uses colorized output for no functional reason but essentially to “look pretty.” This may seem like a silly reason to use color, and you certainly shouldn’t just add color for the sake of color, but you also shouldn’t discount aesthetics. This is much more subjective, but if you think color really will enhance the user experience, by all means, go for it!

A slight word of warning, however. A surprisingly high number of people in the world are color-blind,[50] almost 9 percent, and cannot easily distinguish certain colors. As such, your app should never rely on color alone to be useful. Use color only to enhance information that’s being presented, never as information itself.

How to Use Color

Colored output on the command line is accomplished via the use of ANSI escape sequences.[51] These sequences of bytes are nonprintable characters that most terminal emulators will interpret as changes to styling and color. Generating them by hand is cumbersome and results in difficult-to-understand strings of text. Fortunately, there is a plethora of Ruby libraries to generate them for us. We’re going to use rainbow,[52] which provides a readable, low-impact API as well as a few other handy features that we’ll see (another popular library is term-ansicolor[53]).

To see how to use rainbow to add color and styling to our output, we’re going to enhance todo’s “pretty” format to help the user better understand their task list. If you’re reading this in a black-and-white format, you might want to follow along on your computer so you can see the effects of the changes we’re making.

First let’s review the current “pretty” format of todo by adding a few tasks, completing one, and getting the list. (Recall that we must use bundle exec since we are running out of our source tree for this example; users of todo will, of course, be able to run todo on its own, as discussed in Chapter 9,Be Easy to Maintain.)

$ bundle exec bin/todo new

Reading new tasks from stdin...

Design database schema

Get access to production logs

Code Review

Implement model objects for new scheme

^D

$ bundle exec bin/todo done 3

$ bundle exec bin/todo list

1 - Design database schema

Created: Sun Oct 30 12:53:11 -0400 2011

2 - Get access to production logs

Created: Sun Oct 30 12:53:11 -0400 2011

3 - Code Review

Created: Sun Oct 30 12:53:11 -0400 2011

Completed: Sun Oct 30 13:00:05 -0400 2011

4 - Implement model objects for new schema

Created: Sun Oct 30 12:53:11 -0400 2011

Let’s enhance the output of list as follows:

· Show the task name in a brighter/bolder font (the ANSI escape sequences provide two version of each color: a normal color and a brighter/bolder version). This will make it easier for the user to use the most important information in the task list.

· Show completed tasks in green. This will give the user a sense of satisfaction; the more green the user sees, the more they’ve accomplished.

Both of these formatting options are available via ANSI escape sequences and therefore are available via rainbow. rainbow works by adding a few new methods to Ruby’s built-in String class. The methods we’ll use are color and bright , which will set the color of and brighten the string on which they are called, respectively. Since we’ve extracted all the code for the pretty formatter to the class Todo::Format::Pretty, we know exactly where to go to add this feature. But first we need to add rainbow to our gemspec:

break_rules/todo/todo.gemspec

require File.join([File.dirname(__FILE__),'lib','todo/version.rb'])

spec = Gem::Specification.new do |s|

s.name = 'todo'

s.version = '0.0.1'

# ...

s.add_dependency('gli')

*

s.add_dependency('rainbow')

end

We then install it locally using Bundler:

$ bundle install

bundle install

Fetching source index for http://rubygems.org/

Using gherkin (2.12.1)

Using cucumber (1.3.8)

Using aruba (0.5.3)

*

Installing rainbow (1.1.4)

Using gli (2.8.0)

Windows users should also install the gem win32console, which adapts the Windows command prompt to ASCII escape sequences.

Adding colored output is going to make our formatter a bit more complex, so let’s take things one step at a time. First we’ll use bright to call out the task name:

break_rules/todo/lib/todo/format/pretty.rb

require 'rainbow'

module Todo

module Format

class Pretty

def format(index,task)

*

printf("%2d - %s\n",index,task.name.bright)

printf(" %-10s %s\n","Created:",task.created_date)

if task.completed?

printf(" %-10s %s\n","Completed:",task.completed_date)

end

end

end

end

end

When we run list, we can now see that the task names are bolder, as in the following figure:

images/break_rules/bright-task-names.png


Figure 4. Using bright results in task names that stand out.

Next, we’ll color the entire string green when the task is completed. This will require some restructuring, since we are currently using printf to output directly to the standard output. Instead, we’ll use sprintf , which simply returns the string, which we can then optionally apply color to, before outputting it to the standard output.

To handle the case where we don’t want to color the output, we’ll use a special color provided by rainbow named :default. This color just means “do not apply special colors.” We can use this as the argument to color , unless the task is completed, and then we’ll use :green. We have to change every line, so read this code a few times to make sure you see the differences:

break_rules/todo/lib/todo/format/pretty.rb

def format(index,task)

color = :default

if task.completed?

color = :green

end

puts sprintf("%2d - %s",index,task.name.bright).color(color)

puts sprintf(" %-10s %s","Created:",task.created_date).color(color)

if task.completed?

puts sprintf(" %-10s %s","Completed:",task.completed_date).color(color)

end

end

Now our completed task pops out as shown in the figure here:

images/break_rules/green-completed-tasks.png


Figure 5. Completed tasks show in green.

As you can see, formatting “stacks”: the task name of our completed task is both bright and green. This is how the ANSI escape sequences work, and it gives us great power over the formatting of our output. Before we move on, let’s cover a handy feature of rainbow that you’ll find useful when providing a noncolored version of your output.

Suppose a user’s terminal colors are set in such a way that our chosen colors make it difficult to read the output. We want the user to be able to use the “pretty” format but without the colors. Rainbow provides an attribute enabled that will globally disable colors. This turns all of rainbow’s methods into no-ops, meaning we don’t need to special case our code or create another formatter. We’ll handle this entirely in our executable by providing a new switch which lets us turn colors on or off:

break_rules/todo/bin/todo

command :list do |c|

# ...

*

c.desc "Use colors"

*

c.switch :color, default_value: true

c.action do |global_options,options,args|

*

Sickill::Rainbow.enabled = options[:color]

# ...

end

end

This might seem a bit confusing at first. We created a switch called :color that enables colors, when we actually wanted one to disable them. This demonstrates a handy feature of GLI (as well as good style in user interface design). Whenever you declare a switch, GLI will also accept the “negated” version of that switch, so if the user passes --no-color on the command line, the value for options[:color] will be false. If the user specifies neither --color nor --no-color, the value for options[:color] will be true, thus enabling color.

Now, when we run todo list with the --no-color option, the value of option[:color] is false. Since this globally disables the rainbow, we no longer see any colors or formatting, as shown in Figure 6, Colors are disabled.

images/break_rules/disabled-colors.png


Figure 6. Colors are disabled.

Colors aren’t the only way to make our output easier to understand by the user. Formatting using tables can be an effective way to present certain types of data, as we’ll see in the next section.

10.2 Formatting Output with Tables

In Chapter 4, Play Well with Others, we discussed using CSV format to organize output, where each line would represent a record and each comma-separated value would represent a field. We even updated todo to use this format to make it easier to integrate with other programs. Humans have a hard time reading CSV-formatted data, instead finding it easier to view such data in a tabular format. This is why spreadsheet programs like Microsoft Excel can import CSV files and display their data in tables. It’s also why most SQL database clients show their output in tables; it’s a great way to look at a lot of data in an organized fashion.

Using the gem terminal-table, it’s very easy to produce tabular output from our command-line app, but first it’s good to understand when it’s appropriate to do so.

When to Format Output as Tables

You’ll want to use a tabular view for apps that allow the user to examine large amounts of “records and fields” data. The content of a database is a good example. You should, of course, always provide a machine-readable format, but a tabular view can be handy for users who might want to examine some data before piping it to another program.

How to Format Output As Tables

To see how to format output as tables using terminal-tables, let’s add a new formatter to todo that will output the tasks in a tabular format. We want the output to look something like this:

$ bundle exec todo/bin/todo --format=table

+----+----------------------------------------+------------+------------+

| id | name | created | completed |

+----+----------------------------------------+------------+------------+

| 1 | Design database schema | 2011-10-03 | |

| 2 | Get access to production logs | 2011-09-27 | |

| 3 | Code Review | 2011-10-29 | 2011-10-30 |

| 4 | Implement model objects for new schema | 2011-10-13 | |

+----+----------------------------------------+------------+------------+

Notice how the cell size is just big enough to hold the largest piece of data, including the header. Notice further that the numbers in the ID column are right-aligned. We’ll see that this is very easy to accomplish. Because of the refactoring of our code from Chapter 9, Be Easy to Maintain, it will be easy to create a new formatter, although we’ll first need to make a slight change to the way the output is done.

terminal table works by creating an instance of Terminal::Table. You then use the << method to append rows to the table, followed by a call to to_s to generate the output. Since our formatter classes currently have no way of knowing when output starts or completes, there’s no obvious place to put the setup code or the call to to_s that we need to make the formatting work.

Now, we’ll add a new method to the formatter interface. (The interface of a set of classes refers to a set of methods that all those classes implement. Right now, the interface for our formatter classes just contain the format method.) We’ll add the after method that is intended to be called after all the tasks have been given to format . This is because terminal-table needs to know everything it will output before formatting the table, so our code will use after to say, “I’m done giving you tasks to format, go ahead and do your formatting.”

Before we begin, we’ll need to make sure that the gem terminal-table is in our gemspec as a dependency and that we run bundle install to install it locally (we’ll omit the code, since you should be well familiar with this by now). Once that’s done, we’ll use terminal-table to implement our new formatter, called Todo::Format::Table, which will live in lib/todo/format/table.rb, as per our conventions in Chapter 9, Be Easy to Maintain.

break_rules/todo_tables/lib/todo/format/table.rb

require 'terminal-table'

require 'time'

module Todo

module Format

class Table

def initialize

@table = Terminal::Table.new headings: %w(id name created completed)

@table.align_column(0,:right)

end

def format(index,task)

row = []

row << index

row << task.name

row << as_date(task.created_date)

if task.completed?

row << as_date(task.completed_date)

else

row << ''

end

@table << row

end

def after

puts @table.to_s

end

private

def as_date(string)

Time.parse(string).strftime("%Y-%m-%d")

end

end

end

end

This is a big block of code, so let’s focus on a few of the important statements, which are called out in the listing with line numbers:

We create our Terminal::Table instance inside the constructor. We specify the names of the headings as an array. If we omitted this option, the table would still work but wouldn’t have any headings.

Here is where we make sure that the ID field is right-aligned. The first field is 0 and the default alignment is :left, so we specify :right for the first field. We could also use :center to center the data in a cell.

Here we format the timestamps to just show us the date and not the time component. Since the value is actually a String, the formatting is a bit complex, so we defer to a helper method named as_date .

Here, we output the table to the standard output using to_s .

This is the implementation of as_date , and it simply parses the string from the task and uses strftime (which is named after a UNIX system call that works the same way) to format the date the way we want.

Since neither Todo::Format::Pretty nor Todo::Format::CSV need to do any setup or cleanup, these classes can implement the new method as a no-op:

break_rules/todo_tables/lib/todo/format/pretty.rb

class Pretty

*

def after; end

# ...

end

break_rules/todo_tables/lib/todo/format/csv.rb

class CSV

*

def after; end

# ...

end

Now, we make a slight change to the action block of the list command to call our after after all the tasks have been given to the formatter:

break_rules/todo_tables/bin/todo

command :list do |c|

# ...

formatter = output_formats[options[:format]]

File.open(global_options[:filename]) do |tasklist|

index = 1

tasks = Todo::TaskBuilder.from_file(tasklist)

tasks.each do |task|

formatter.format(index,task)

index += 1

end

*

formatter.after

end

end

end

We need to do only two more things to make this work. We need to update lib/todo.rb to require our new formatter file, and we need to add it to our list of formatters in the executable.

break_rules/todo_tables/lib/todo.rb

require 'todo/version.rb'

require 'todo/task'

require 'todo/format/csv'

require 'todo/format/pretty'

*

require 'todo/format/table'

Thanks to our use of the Strategy pattern that we learned about in Chapter 9, Be Easy to Maintain, it takes only one line of code to add the new formatter:

break_rules/todo_tables/bin/todo

command :list do |c|

# ...

output_formats = {

'csv' => Todo::Format::CSV.new,

'pretty' => Todo::Format::Pretty.new,

*

'table' => Todo::Format::Table.new,

}

end

$ bundle exec bin/todo help list

NAME

list - List tasks

SYNOPSIS

todo [global options] list [command options]

COMMAND OPTIONS

--[no-]color - Don't use colors (default: enabled)

*

--format=csv|pretty|table - Format of the output (pretty for TTY, csv

*

otherwise) (default: none)

Now when we list our tasks using the “table” format, we get tables, just as expected:

$ bundle exec bin/todo list --format=table

+----+----------------------------------------+------------+------------+

| id | name | created | completed |

+----+----------------------------------------+------------+------------+

| 1 | Design database schema | 2011-10-30 | |

| 2 | Get access to production logs | 2011-10-30 | |

| 3 | Code Review | 2011-10-30 | 2011-10-30 |

| 4 | Implement model objects for new schema | 2011-10-30 | |

+----+----------------------------------------+------------+------------+

We’ve talked about nonstandard formatting options for output, but what about input? Most command-line apps accept input from the standard input stream or files, but occasionally we might need an app that allows more user interaction. Such apps are rare, but if you find you need one, Ruby’s built-in readline library can allow you to create a sophisticated and easy-to-use user interface.

10.3 Providing Interactive User Input with readline

An interactive user interface, like the one provided by your average SQL client, is a “command-line interface with a command-line app.” In other words, this sort of interface provides a customized “shell” into another environment. irb, the Ruby interactive interpreter, is a common example, as are SQL clients. Rather than accept a file full of strings as input, interactive applications provide a command prompt where commands or other input are entered by the user. The user typically has access to a history of commands previously entered and has the ability to edit commands in place before sending them to the program. Also, the user will have the ability to use tab completion of common commands or strings. For example, the MySQL command-line client allows you to tab-complete the names of tables and columns.

Virtually all UNIX applications that provide such an interface use the standard readline library (although some use libedit, which is a replacement and works the same way). Ruby provides bindings for this that make it very easy to use. Keep in mind that it is rare to need to provide such an interface, so let’s first talk about when it makes sense.

When to Use an Interactive User Interface

You’ll typically want to provide an interactive user interface when your application acts as a gateway into some sort of nonstandard environment. irb is a great example; it allows you to execute arbitrary Ruby code, one command at a time. This sort of interface is especially useful if the expected input is likely to contain characters that are viewed as “special” by the shell. This is one reason why SQL clients use interactive interfaces. The frequent use of asterisks, semicolons, parentheses, and quotes would make it very awkward to enter on a UNIX command line; inside a custom interactive prompt, none of these characters is special and won’t need escaping. Further, the ability to control tab completion from within your app can be very useful.

If you aren’t making a SQL client or a “Read/Eval/Print Loop” interface to a programming language, it will be unlikely that you’ll need to provide an interactive interface. As such, neither db_backup.rb nor todo really lends itself to it, so we’ll create a new application to learn the mechanics of doing so using readline.

How to Implement an Interactive Input

Implementing an interactive input, with history browsing and tab completion, is fairly difficult using the various terminal control characters that would be required. Fortunately, the readline C library is widely available on most systems, and the Ruby standard library contains bindings for it so we can access its power from our app.

To learn how to use it, we’re going to implement a JSON browser. JSON is a widely used format in web application APIs, something that command-line applications often need to consume. Sophisticated web applications yield heavily nested JSON objects that can be hard to understand by simply viewing them in the terminal. We’ll make an interactive application that reads a JSON file from disk and allows the user to move around inside it, inspecting bits of it at time. We’ll allow the user to navigate the structure of the JSON data in much the same way a user might navigate a file structure. To make it familiar and easy to learn, we’ll use the following commands, closely modeled after similar UNIX commands:

ls

Lists all attributes in the current context

cd xxx

Changes context to the object referenced by the key “xxx”

cd ..

Changes to the context “one up” from where the user is currently

cat xxx

Prints the contents of everything referenced by the key “xxx”

exit

Exits the application

Let’s see an example of what we’re aiming for. Suppose we have the following JSON file:

break_rules/jb/file.json

{

"result": [

{

"name": "Dave",

"age": 38,

"state": {

"name": "Washington, DC",

"code": "DC"

}

},

{

"name": "Clay",

"age": 37,

"state": {

"name": "Maryland",

"code": "MD"

}

},

{

"name": "Adam",

"age": 26,

"state": {

"name": "California",

"code": "CA"

}

}

]

}

If we were to run our application on this file, the following session demonstrates the behavior we’re looking for:

> ls

result

> cd result

> ls

0 1 2

cd 0

> ls

name age state

> cat name

"Dave"

> cd ..

> cd 1

> cd state

> ls

name code

> cat code

"CA"

The user also has the ability to use the cursor keys to find old commands and can use tab completion for the cd and cat commands to complete keys available in the current context. This is going to be a much more complex application than we’ve seen before, and it’s all new code, so watch closely.

Bundler has a command, gem, that will provide a rudimentary scaffold for a simple command-line application. We’ll call our app jb for “JSON browser” and create it like so:

$ bundle gem jb -b

create jb/Gemfile

create jb/Rakefile

create jb/.gitignore

create jb/jb.gemspec

create jb/lib/jb.rb

create jb/lib/jb/version.rb

create jb/bin/jb

Note that we are using -b, which tells Bundler to create an executable for us. Now that that’s set up, we can use Ruby’s built-in JSON parser as well as its built-in readline implementation. To use them, we need to require both "json" and "readline" at the top of our new executable file, bin/jb:

break_rules/jb/bin/jb

require 'json'

require 'readline'

require 'optparse'

Next, we’ll need to set up the basics of our command-line interface. We don’t need any options, so things are pretty minimal:

break_rules/jb/bin/jb

option_parser = OptionParser.new do |opts|

executable_name = File.basename($PROGRAM_NAME)

opts.banner = "Interactively browse a JSON file

Usage: #{executable_name} json_file"

end

option_parser.parse!

json_file = ARGV.shift

if json_file && File.exists?(json_file)

main(json_file)

else

STDERR.puts "error: you must provide a JSON file as an argument"

exit 1

end

You’ll notice that we still use OptionParser even though we have no options. This gives us --help for free and provides an easy way to add options later. You’ll also notice that we’re calling a main method. We’ll see that next, as we move onto the meat of the program.

The way readline works is very simple. We call the method readline on the class Readline, which provides the interactive prompt and returns us the string that the user entered. The readline method takes two arguments: a String, representing the prompt to show the user, and a boolean that, if true, will instruct Readline to store the history so the user can use their cursor keys to scroll back through the command history. Here’s the basic loop we’ll run to get the commands the user typed:

break_rules/jb/bin/jb

def main(json_file)

root = JSON.parse(File.read(json_file))

command = nil

while command != 'exit'

command = Readline.readline("> ",true)

breakif command.nil?

# execute the command

end

end

The first thing we do is parse the JSON file using the JSON library’s JSON.parse method. File.read returns the contents of a file as a string, and parse returns a Hash with the parsed JSON. After that, we enter a loop that uses Readline to get the user’s input. The readline method returns whatever the user typed at the prompt. If the user hit Ctrl - D (which is the control character for “end of file” and indicates the user wants to exit), null is returned, so we break out in that case. Next, we need to handle an actual command, which will require us to determine a way to navigate up and down the tree of the parsed JSON.

We’ll do this by using a nested structure that records where we are in the Hash. This structure, called a Context, will have a reference to the current location and to a parent Context. This way, we can easily traverse back up when a user enters cd ... Here’s part of the class:

break_rules/jb/bin/jb

class Context

attr_reader :here

attr_reader :parent_context

def initialize(here,parent_context)

@here = here

@parent_context = parent_context

end

end

Next, we’ll initialize the root context in main and defer all command-handling duties to a method called execute_command , which takes the current context as an argument and returns the new context resulting from whatever command was executed:

break_rules/jb/bin/jb

def main(json_file)

root = JSON.parse(File.read(json_file))

command = nil

*

current_context = Context.new(root,nil)

while command != 'exit'

command = Readline.readline("> ",true)

breakif command.nil?

# execute the command

*

current_context = execute_command(command.strip,current_context)

end

end

Next, we’ll implement execute_command . This method will use a case statement to match on the known commands. Each when clause will handle one command by passing it to the current_context (which, you’ll recall, is an instance of Context). Note the highlighted methods in Context that we’re assuming exist.

break_rules/jb/bin/jb

def execute_command(command,current_context)

case command

when /^ls$/

*

puts current_context.to_s

when /^cd (.*$)/

*

new_context = current_context.cd($1)

if new_context.nil?

puts "No such key #{$1}"

else

current_context = new_context

end

when /^cat (.*)$/

*

item = current_context.cat($1)

if item.nil?

puts "No such item #{$1}"

else

puts item.inspect

end

when /^help$/

puts "cat <item> - print the contents of <item> in the current context"

puts "cd <item> - change context to the context of <item>"

puts "cd .. - change up one level"

puts "ls - list available items in the current context"

end

current_context

end

As you can see, we’ve assumed that the methods cd , to_s , and cat exist on Context. We’ll see those in a minute, but we’ve assumed that cd and cat return nil if anything goes wrong, and we message the user in this case. Note that we’re not using the standard error here. Since we’re interacting with the user, there’s no other output than the results of the user’s actions, so it’s simplest to use the standard output stream for everything. We’ve also included a help command, since we want our app to be helpful (as we learned in Chapter 3, Be Helpful).

We’ll start off with to_s , which will check the type of here and transform it into the list of keys to which the user can cd:

break_rules/jb/bin/jb

def to_s

if self.here.kind_of? Array

indices = []

self.here.each_index { |i| indices << i }

indices.join(' ')

elsif self.here.kind_of? Hash

self.here.keys.join(' ')

else

self.here.to_s

end

end

cat and cd are both very simple, relying on a private method item_at , which handles accessing the item inside here that matches path, the parameter to both cat and cd :

break_rules/jb/bin/jb

def cat(path)

item_at(path)

end

def cd(path)

if path == '..'

self.parent_context

else

item = item_at(path)

if item.nil?

nil

else

Context.new(item,self)

end

end

end

private

def item_at(path)

if path == '..'

self.parent_context.here

elsif self.here.kind_of? Array

self.here[path.to_i]

elsif self.here.kind_of? Hash

self.here[path]

else

nil

end

end

That’s a lot of code, but it’ll make it easy to add tab completion to our app, which we’re going to do next. First, let’s see this version in action:

$ bundle exec bin/jb file.json

> ls

result

> cd result

> ls

0 1 2

> cd 99

No such key 99

> cd 1

> ls

name age state

> cat name

"Clay"

> cd ..

> cat 0

{"name"=>"Dave", "age"=>38, "state"=>{"name"=>"Washington, DC", "code"=>"DC"}}

> exit

Everything works great! Now, let’s allow the user to tab-complete the keys available when using the cd or cat command. To do this, we register a Proc with Readline that, when executed, will be given the current input as a string and expects an Array of possible completions as a result. To set it up, we call completion_proc= on Readline:

break_rules/jb_completion/bin/jb

def main(json_file)

root = JSON.parse(File.read(json_file))

command = nil

current_context = Context.new(root,nil)

*

Readline.completion_proc = ->(input) {

*

current_context.completions(input)

*

}

while command != 'exit'

command = Readline.readline("> ",true)

breakif command.nil?

current_context = execute_command(command.strip,current_context)

end

end

Our Proc is simply sending the input to a new method of Context called completions . Since the list of completions is essentially the same as the output of ls, we’ll use the to_s method to get a list of completions:

break_rules/jb_completion/bin/jb

class Context

# ...

def completions(input)

self.to_s.split(/\s+/).grep(/^#{input}/)

end

end

Here, we split the output of to_s on a space and then use the method grep , available on all Array instances, to trim out only what matches the input. This way, if we have a JSON object with the keys “dave,” “dan,” and “amy” and the user types “da” and hits tab , the user will see only “dave” and “dan” as possible completions. Let’s try it:

bundle exec bin/jb file.json

> cd res<TAB>

> cd result

> cd<TAB>

0 1 2

> cd 1

> cat na<TAB>

> cat name

"Clay"

> exit

If you can try this on your computer, it will be easier to see how it works, but you can tab-complete just as you can in your shell. Since the completion algorithm is entirely under your control, you can make it as sophisticated as you like.

10.4 Moving On

This brings us to the end of our journey. We’ve come a long way from using OptionParser to parse the command line. We can now provide sophisticated help text, integrate with any other system or command, and distribute our code to any environment, all while keeping our app tested and maintainable. As we learned in this chapter, we can even spruce it up with sophisticated input and output, if the need should arise.

So, what’s left? The techniques we’ve learned are applicable to your everyday tasks and can be applied to any command-line app you need to write. We’ve also learned some handy tools and libraries; however, these only scratch the surface. The great thing about the Ruby community is the wide variety of tools available to solve problems. If you thought OptionParser was too verbose or you didn’t like the way your command suite looked using GLI, never fear; there’s more than one way to do it. In the appendix that follows, we’ll take a quick tour of some other popular command-line libraries and show you how our running examples, db_backup and todo, might look using tools like Thor, Main, and Trollop.

Footnotes

[48]

http://betterthangrep.com/

[49]

http://mxcl.github.com/homebrew

[50]

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

[51]

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

[52]

http://github.com/sickill/rainbow

[53]

http://flori.github.com/term-ansicolor/