cmd is a library for building line-oriented command interpreters in Ruby. Simply inherit from cmd’s Cmd class, and methods whose names start with do_
become interactive commands. cmd is inspired by the Python library of the same name, but offers a distinctive Ruby feel and several additional features.
Consider the following example of a small program to manage a lightweight phone book.
We want to be able to add, find, list and delete phone book entries.
We are keeping it realy simple so the entries will be stored in a Hash with names as keys and numbers as values. Let’s assume that @numbers
is our hash. First we’ll write a command to add an entry. Entries will be entered like so:
PhoneBook> add Sam, 312-555-1212
So we define a do_add
method.
def do_add(args) name, number = args.to_s.split(/, +/) @numbers[name.strip] = number end
We add another entry for good measure.
PhoneBook> add Amy, 227-328-2868
We make a print_name_and_number
method to format our entries.
protected def print_name_and_number(*args) puts "%-25s %s" % args end
Writing a command to list all numbers is straightforward:
def do_list @numbers.sort.each do |name, number| print_name_and_number(name, number) end end
We run our list
command:
PhoneBook> list Amy 227-328-2868 Sam 312-555-1212
Then we write a find command to get the number for a given person.
def do_find(name) name.to_s.strip! if @numbers[name] print_name_and_number(name, @numbers[name]) else puts "#{name} isn't in the phone book" end end PhoneBook> find Sam Sam 312-555-1212 PhoneBook> find Matz Matz isn't in the phone book
Well we are cruising for burgers. But say we have a falling out with Amy (she was taking up too much disk space anyway). No reason to keep her in the phone book, so we’ll define a delete
command.
def do_delete(name) @numbers.delete(name) || write("No entry for '#{name}'") end PhoneBook> delete Amy PhoneBook> list Sam 312-555-1212
Commands like add
and delete
have clear names. They are self-documenting. But it can get tedious to type them all out all the time.
You can add shortcuts for commands using Cmd::ClassMethods.shortcut.
shortcut '+', :add
The default help
command lists shortcuts for a given command. The help
command itself has a shortcut: +?+.
PhoneBook> ? add add -- Add an entry (ex: add Sam, 312-555-1212) (aliases: +)
Additionally, any unambiguous abbreviation of a command will be translated to the full command (so aliases that simply shorten the name of a given command are unnecessary).
Since we only have one command that starts with h
, the above could have been written as:
PhoneBook> h add add -- Add an entry (ex: add Sam, 312-555-1212) (aliases: +)
Furthermore, abbreviations are acceptable in any place a command name appears, so you could write the above in an even more abbreviated way:
PhoneBook> h a add -- Add an entry (ex: add Sam, 312-555-1212) (aliases: +)
Our phone list now has its basic functionality. Let’s add some documentation so that someone other than you can figure out how to use it.
You document your commands using Cmd::ClassMethods.doc. We’ll add docs for our four commands so far:
doc :add, 'Add an entry (ex: add Sam, 312-555-1212)' doc :find, 'Look up an entry (ex: find Sam)' doc :list, 'List all entries' doc :delete, 'Remove an entry'
As illustrated above, there is a predefined help
command. Called without arguments, it displays a help line for each command that has been documented using the Cmd::ClassMethods.doc class method. (See Documenting your commands
for more on this.) By default, commands without documentation are listed at the end of the help
output; this can be turned off by setting YourCmdClass.hide_undocumented_commands = true. You can get help for a single command by passing it as an argument to the help
command.
PhoneBook> help add add -- Add an entry (ex: add Sam, 312-555-1212)
Typing help
affords you tab completion on all available commands with documentation, so the above could be accomplished (assuming there are no other documented commands that start with the letter a
) by typing:
PhoneBook> help a<Tab>
The help command is aliased to +?+.
Any method that is of the form do_command_subcommand
will be interpreted as a subcommand of command
. For example, if there was an add
command, a do_add_cellphone
method would be invoked if ‘add cellphone’ was entered at the prompt. If there was no add
command, do_add_cellphone
would not be interpreted as a subcommand; you’d need to enter ‘add_cellphone’ to invoke it.
Much like method_missing
, there is a command_missing
method which is called if an undefined command is entered in at the prompt. By default it simply reports that the command does not exist; subclasses can override this behavior. command_missing
is passed the entered command name as well as any arguments. You must define your command_missing
this way.
Let’s make phone book entry lookups more convenient by having command_missing
delegate to the find
command.
protected def command_missing(command, args) do_find(command) end
Now we can do
PhoneBook> Sam Sam 312-555-1212
Right now our phone book isn’t really useful as the hash gets lost any time you quit the program. Let’s implement a simple storage scheme so that our phone book entries will persist between invocations. A simple solution is just to serialize the phone book hash to YAML in a file.
First we’ll choose a place to store the file (apologies to people running Windows).
PHONEBOOK_FILE = File.expand_path('~/.phonebook')
When the command loop is started, your subclass’s setup
method is called. Consider this your initialize
. We can use this to grab the contents of our phone book file.
protected def setup @numbers = get_store || {} end def get_store File.open(PHONEBOOK_FILE) {|store| YAML.load(store)} rescue nil end
Now when we start up our phone book it will grab our entries or create a fresh Hash in which to add entries. But we don’t have any code to save our phone book entries!
A Cmd session happens mostly inside a loop. This loop accepts commands until it is told to stop. Like setup
, there are several methods that are called automatically during the lifetime of this loop. One such method is postloop
, which, as the name suggests, is called after the loop is done, or in other words, once the Cmd session is completed. This turns out to be a good candidate for the task of saving our phone book entries.
protected def postloop File.open(PHONEBOOK_FILE, 'w') {|store| store.write YAML.dump(@numbers)} end
And that is that. Now when we exit the phone book our numbers will be saved to our phone book file.
$ ruby phonebook.rb PhoneBook> l PhoneBook> a Sam, 312-555-1212 PhoneBook> Sam Sam 312-555-1212 PhoneBook> exit $ ruby phonebook.rb PhoneBook> l Sam 312-555-1212
There are five life-cycle callbacks. The complete list is below:
setup
-
Called when your Cmd subclass is created, like
initialize
. preloop
-
Called before the command loop begins
precmd
-
Called before each command
postcmd
-
Called after each command; has access to the
current_command
method, which returns the name of the current command postloop
-
Called after the command loop ends
Here we can have a look at a working copy of our PhoneBook.
By default Cmd supports readline functionality if it is enabled on your system. This affords you command line history as well as command completion. The default completion procedure will complete command names for you when you hit the Tab key.
PhoneBook> l<Tab> PhoneBook> list
As is the case in your standard shell, hitting tab twice when there is nothing to complete will list all commands.
PhoneBook> <Tab><Tab> add delete exit find help list shell
Completion can be customized on a per-command basis by defining a method of the form complete_command
(where command
is a command name) which returns an array with zero or more strings. The following (contrived) example illustrates the idea:
$ grep -A 3 complete_add def complete_add %w{ cellphone fax home office } end PhoneBook> add <Tab> cellphone fax home office PhoneBook> add o<Tab> PhoneBook> add office
If a given command has subcommands, Cmd’s built in completion method will complete with those subcommands automatically, so the above example would be redundant were there to be command methods such as do_add_office
and do_add_home
, etc.
FIXME These docs lie I’m afraid. The API is not that simple yet, though the above is the intended API. Check out the part of the files/TODO.html file that talks about improving how completion works.
By default the prompt will look like ‘YourSubclass> ’. So in the example above, where we have been writing all that code inside a PhoneBook class that inherits from Cmd, the prompt reads ‘PhoneBook> ’. The Cmd::ClassMethods.prompt_with macro style method can be used to set a custom prompt. The simplest prompt would just be a static string:
prompt_with '> '
You can, alternatively, pass Cmd::ClassMethods.prompt_with a Proc or method reference.
# Contrived... prompt_with { "#{Time.now}> " }
prompt_with :set_prompt protected # This assumes current_directory is defined by your Cmd subclass def set_prompt "#{ENV['USER']:#{current_directory}$ " end
Using a method reference affords you access to all the state of your Cmd instance.
N.B. The result of whatever is passed to Cmd::ClassMethods.prompt_with has to_s
called on it.
If a user attempts to exit the command loop (using, for example, Ctrl-C), the Cmd.user_interrupt method is called. Subclasses may override this. By default it simply exits.
For commands that take arguments (determined by whether or not you define your do_
method with arguments), a method Cmd#tokenize_args is called on the passed arguments. The default implementation has no side effects.
N.B. This API will more than likely change to something far more useful and Rubyesque. Please checkout the files/TODO.html for details.
All exceptions raised within the command loop are caught. You can specify what action should be taken if a specific exception is raised by using the Cmd::ClassMethods.handle method.
handle StackOverflowError, :handle_stack_overflow
If you specify a symbol the referenced method will be called. If you supply a string, such as
handle StackOverflowError, 'Stack underflowed'
the string will be displayed to the user.
All other exceptions are passed to a Cmd.handle_exception method which by default simply reraises the exception. Subclasses may use this to customize how exceptions are handled.
def handle_exception(exception) write 'Error' end
Though subclasses of Cmd are meant to be run interactively, you may find that you’d like to have access to a given command without starting up a session with the interactive interpreter. Cmd allows you to run commands from the command line. If you supply a command (with optional arguments) when invoking the program that runs your command loop, the supplied command will be invoked, and execution will stop.
$ ruby phonebook.rb Sam Sam 312-555-1212
If a user enters an empty line at the command prompt, the empty_line
method is called. By default it does nothing.
Calling the stoploop
method will stop the command loop once the current command is complete.
By default undocumented commands (if any) are listed at the bottom of the default help message. This behaviour can be disabled by setting hide_undocumented_commands
to true
.
MyCmdClass.hide_undocumented_commands = true
handle_exception
-
see Handling exceptions
user_interrupt
-
see Trapping user interrupts
tokenize_args
-
see Customizing passed in arguments
setup
-
see Setting up your environment
command_missing
-
see Missing commands
empty_line
-
see Emtpy command lines
Subversion
Documentation can be found at
See the files/INSTALL.html doc.