Table of Contents
 
 
 

What is Readline?

The GNU Readline Library’s website sums it up best:

The GNU Readline library provides a set of functions for use by applications that allow users to edit command lines as they are typed in. (…) The Readline library includes additional functions to maintain a list of previously-entered command lines, to recall and perhaps reedit those lines, and perform csh-like history expansion on previous commands.

Users that have worked with the shell are very familiar with readline. This is the library that provides ⌃A, ⌃E, , , and a number of other keyboard shortcuts that you probably expect to have when working in a shell.

Ruby ships with support for working with readline (or libedit). You just need to include the Readline module:

require 'readline'

It takes just a few lines of Ruby to get the basic functionality and the core advantages that Readline provides such as keyboard shortcuts and history. But with a little more effort your program can include some of the more useful and interesting features like command auto-completion. All of these topics will be covered in this tutorial.

Basic Functionality

Here is the standard Ruby readline sample program:

require 'readline'

while line = Readline.readline('> ', true)
  p line
end

With that you already have your keyboard shortcuts and basic history management. Lets dig into the details and then add features on top of this.

The Readline Module

You get input via Readline.readline:

readline( [prompt, [add_hist]] ) -> String | nil

After the prompt is printed, readline will wait for input from the user. Readline returns a String once the user finishes input by pressing Enter. If the add_hist argument was true, the String will also be appended to the history.

Readline will return nil if it receives an EOF (⌃D on *nix) without any other input. In the case of the basic loop, this is a natural terminating condition.

Handling ⌃C Interrupts

There is something to watch out for. If the user sends a SIGINT (⌃C on *nix) then Ruby will raise an Interrupt error. This usually exits the interpreter with an ugly error message. Michael gave three examples of different ways to gracefully handle this:

Rescue Interrupt and Restore the State of the Terminal:

require 'readline'

# Store the state of the terminal
stty_save = `stty -g`.chomp

begin
  while line = Readline.readline('> ', true)
    p line
  end
rescue Interrupt => e
  system('stty', stty_save) # Restore
  exit
end

Trap SIGINT and Restore the State of the Terminal:

require 'readline'

stty_save = `stty -g`.chomp
trap('INT') { system('stty', stty_save); exit }

while line = Readline.readline
  p line
end

Ignore SIGINT:

require 'readline'

trap('INT', 'SIG_IGN')

while line = Readline.readline
  p line
end

Note that the final version does not exit when it gets an interrupt. It will continue to loop. The other versions will both exit gracefully, without an error message, once receiving the interrupt and the final line of input.

Managing The History

The Readline Module stores its history in an “array-like” object Readline::HISTORY. You can directly manipulate the history with Readline::HISTORY.push and pop. However, if you’re setting the add_hist parameter of the Readline.readline to true then you won’t need to push values yourself. Also, since the history is “array-like” if you want the real array you can get it with to_a.

A few reasons that you might want to work with the history yourself could be to prevent storing empty commands or duplicates of previous commands. This example does just that:

require 'readline'

#
# Smarter Readline to prevent empty and dups
#   1. Read a line and append to history
#   2. Quick Break on nil
#   3. Remove from history if empty or dup
#
def readline_with_hist_management
  line = Readline.readline('> ', true)
  return nil if line.nil?
  if line =~ /^\s*$/ or Readline::HISTORY.to_a[-2] == line
    Readline::HISTORY.pop
  end
  line
end

# Debug
while line = readline_with_hist_management
  p line
  p Readline::HISTORY.to_a
end

WARNING: The Default Mac OS X Ruby build uses libedit and has some issues with HISTORY. See Below.

Completion

The auto-completion features for this module are very easy to work with. In Ruby its as simple as using a Proc. You can access and set the completion Proc for Readline by getting or setting Readline.completion_proc. The Proc must be defined as follows:

  • It must have a call method.
  • It takes in a single String as an argument. (See Below)
  • It returns an Array of candidates for completion.

The String that is passed to the Proc depends on the Readline.completer_word_break_characters property. The default settings are that “the word under the cursor” is passed to the Proc. This is the normal behavior you would expect. So if you had input foo bar⇥ then only 'bar' would get passed to the completion Proc.

Its common functionality that on a successful completion you may want to append a space character so that the user can immediately start working on their next argument. Readline has you covered. Just set Readline.completion_append_character.

Lets look at some examples:

Completion for a Static List:

require 'readline'

LIST = [
  'search', 'download', 'open',
  'help', 'history', 'quit',
  'url', 'next', 'clear',
  'prev', 'past'
].sort

comp = proc { |s| LIST.grep( /^#{Regexp.escape(s)}/ ) }

Readline.completion_append_character = " "
Readline.completion_proc = comp

while line = Readline.readline('> ', true)
  p line
end

Completion For Directory Contents:

require 'readline'

Readline.completion_append_character = " "
Readline.completion_proc = Proc.new do |str|
  Dir[str+'*'].grep( /^#{Regexp.escape(str)}/ )
end

while line = Readline.readline('> ', true)
  p line
end

WARNING: The Default Mac OS X Ruby build uses libedit and has issues with completion_append_character. See Below.

Autocomplete Strategies

When working with auto-complete there are some strategies that work well. To get some ideas you can take a look at the completion.rb file for irb. The common strategy is to take a list of possible strings, and filter it down to only those strings that start with a given substring. In the above examples I used Enumeration.grep and passed in a regular expression. Because the input comes from the user its smart to escape the special regular expression characters in the input using Regexp.quote or Regexp.escape. The source code looks like this:

# Straightforward
regex = Regexp.new( '^' + Regexp.escape(str) )

# Shortcut using Interpolation
regex = /^#{Regexp.escape(str)}/

It may also be helpful to check out the abbrev library. Their description says it Calculates the set of unique abbreviations for a given set of strings. Here is a quick example:

require 'abbrev'
%w[one two three].abbrev

# Results in:
#    { "three"=>"three", "two"=>"two", "tw"=>"two",
#      "o"=>"one", "one"=>"one", "thre"=>"three",
#      "thr"=>"three", "th"=>"three", "on"=>"one" }

Libedit Detection

Libedit is the default library used by the Readline module on Mac OS X builds of Ruby. Although, if you download the source and compile Ruby yourself, you can also download readline and compile with readline support instead of libedit. When Ruby uses libedit there are some unimplemented features and some buggy implementations that can be worked around.

One way you can might be able to identify if your build has some of these errors is to check if certain methods are unimplemented. In the libedit version the emacs_editing_mode and vi_editing_mode functions are not implemented. You can set a boolean by checking for these features, and if they don’t exist then you can prepare for other errors that might exist:

# Default
libedit = false

# If NotImplemented then this might be libedit
begin
  Readline.emacs_editing_mode
rescue NotImplementedError
  libedit = true
end

Libedit History Issue

In my libedit version Readline::HISTORY.to_a never shows the first value that was written to the history. Although the keyboard shortcuts still work, including up to the first value, it won’t show up in the history array.

If you really want to work around this you could simply store the first value, or push it an extra time to make sure it shows up. This is a small annoyance.

Libedit completion_append_character Issue

In my libedit version Readline.completion_append_character does not work at all. This is unfortunate because auto-appending a space on a successful completion is often the desired behavior.

A workaround is to quickly add a space to all of the possible results when they are being sent back from the completion_proc. Or, in the case of a static list you can get away with padding each value with a space once.

# Static List Example
list = ['alpha', 'beta', 'gamma']

# Possibility of libedit? manually add spaces
list.collect { |i| i += ' ' } if libedit

# Normal Implementations
Readline.completion_append_character = ' '

References and More

  • Michael’s documentation is very good and it goes into much more detail then the simple tutorial and examples I provided. You may find them very useful.