class Cmd
A simple framework for writing line-oriented command interpreters, based heavily on Python's cmd.py.
These are often useful for test harnesses, administrative tools, and prototypes that will later be wrapped in a more sophisticated interface.
A Cmd
instance or subclass instance is a line-oriented interpreter framework. There is no good reason to instantiate Cmd
itself; rather, it's useful as a superclass of an interpreter class you define yourself in order to inherit Cmd's methods and encapsulate action methods.
Attributes
Flag that sets whether undocumented commands are listed in the help
The current command
STDIN stream used
STDOUT stream used
Public Class Methods
# File lib/cmd.rb, line 147 def initialize @stdin, @stdout = STDIN, STDOUT @stop = false setup end
# File lib/cmd.rb, line 131 def run(intro = nil) new.cmdloop(intro) end
Public Instance Methods
Starts up the command loop
# File lib/cmd.rb, line 154 def cmdloop(intro = nil) preloop write intro if intro begin set_completion_proc(:complete) begin execute_command # Catch ^C rescue Interrupt user_interrupt # I don't know why ZeroDivisionError isn't caught below... rescue ZeroDivisionError handle_all_remaining_exceptions(ZeroDivisionError) rescue => exception handle_all_remaining_exceptions(exception) end end until @stop postloop end
# File lib/cmd.rb, line 194 def do_exit; stoploop end
# File lib/cmd.rb, line 177 def do_help(command = nil) if command command = translate_shortcut(command) docs.include?(command) ? print_help(command) : no_help(command) else documented_commands.each {|cmd| print_help cmd} print_undocumented_commands if undocumented_commands? end end
Called when the command
has no associated documentation, this could potentially mean that the command is non existant
# File lib/cmd.rb, line 189 def no_help(command) write "No help for command '#{command}'" end
Turns off readline even if it is supported
# File lib/cmd.rb, line 197 def turn_off_readline @readline_supported = false self end
Protected Instance Methods
XXX Not implementd yet. Called when a do_ method that takes arguments doesn't get any
# File lib/cmd.rb, line 293 def arguments_missing write 'Invalid arguments' do_help(current_command) if docs.include?(current_command) end
The method name that corresponds to the passed in command.
# File lib/cmd.rb, line 317 def command(cmd) "do_#{cmd}".intern end
Returns lookup table of unambiguous identifiers for commands.
# File lib/cmd.rb, line 375 def command_abbreviations return @command_abbreviations if @command_abbreviations @command_abbreviations = Abbrev::abbrev(command_list) end
Lists of commands (i.e. do_* methods minus the 'do_' part).
# File lib/cmd.rb, line 364 def command_list collect_do - subcommand_list end
Definitive list of shortcuts and abbreviations of a command.
# File lib/cmd.rb, line 369 def command_lookup_table return @command_lookup_table if @command_lookup_table @command_lookup_table = command_abbreviations.merge(shortcut_table) end
Called when the line entered at the prompt does not map to any of the defined commands. By default it reports that there is no such command.
# File lib/cmd.rb, line 550 def command_missing(command, args) write "No such command '#{command}'" end
Returns the set of registered shortcuts for a command, or nil if none.
# File lib/cmd.rb, line 518 def command_shortcuts(cmd) shortcuts[cmd] end
The default completor. Looks up all do_* methods.
# File lib/cmd.rb, line 354 def complete(command) commands = completion_grep(command_list, command) if commands.size == 1 cmd = commands.first set_completion_proc(complete_method(cmd)) if collect_complete.include?(cmd) end commands end
Completor for the help command.
# File lib/cmd.rb, line 429 def complete_help(command) completion_grep(documented_commands, command) end
The method name that corresponds to the complete command for the pass in command.
# File lib/cmd.rb, line 323 def complete_method(cmd) "complete_#{cmd}".intern end
# File lib/cmd.rb, line 433 def completion_grep(collection, pattern) collection.grep(/^#{Regexp.escape(pattern)}/) end
The current command.
# File lib/cmd.rb, line 282 def current_command translate_shortcut @current_command end
Returns the customized handler for the exception
# File lib/cmd.rb, line 252 def custom_exception_handler(exception) # grych@tg.pl FIX, was: exception.to_s which return exception message in ruby >=(?) 1.9.3 custom_exception_handlers[exception.class.to_s] end
# File lib/cmd.rb, line 554 def default_prompt "#{self.class.name}> " end
Displays the prompt.
# File lib/cmd.rb, line 271 def display_prompt(prompt, with_history = true) line = if readline_supported? Readline::readline(prompt, with_history) else print prompt @stdin.gets end line.respond_to?(:strip) ? line.strip : line end
# File lib/cmd.rb, line 312 def display_shortcuts(cmd) "(aliases: #{shortcuts[cmd].join(', ')})" end
List of commands which are documented.
# File lib/cmd.rb, line 397 def documented_commands docs.keys.sort end
Called when an empty line is entered in response to the prompt.
# File lib/cmd.rb, line 347 def empty_line end
Determines if the given exception has a custome handler.
# File lib/cmd.rb, line 236 def exception_is_handled?(exception) custom_exception_handler(exception) end
# File lib/cmd.rb, line 204 def execute_command unless ARGV.empty? stoploop execute_line(ARGV * ' ') else execute_line(display_prompt(prompt, true)) end end
# File lib/cmd.rb, line 221 def execute_line(command) postcmd(run_command(precmd(command))) end
Extracts a subcommand if there is one from the command line submitted. I guess this is a hack.
# File lib/cmd.rb, line 502 def find_subcommand_in_args(subcommands, args) (subcommands & (1..args.size).to_a.map {|num_elems| args.first(num_elems).join('_')}).max end
# File lib/cmd.rb, line 213 def handle_all_remaining_exceptions(exception) if exception_is_handled?(exception) run_custom_exception_handling(exception) else handle_exception(exception) end end
Exceptions in the cmdloop are caught and passed to handle_exception
. Custom exception classes must inherit from StandardError to be passed to handle_exception
.
# File lib/cmd.rb, line 266 def handle_exception(exception) raise exception end
Indicates if the passed in command has any registerd shortcuts.
# File lib/cmd.rb, line 513 def has_shortcuts?(cmd) command_shortcuts(cmd) end
Indicates whether a given command has any subcommands.
# File lib/cmd.rb, line 392 def has_subcommands?(command) !subcommands(command).empty? end
A bit of a hack I'm afraid. Since subclasses will be potentially overriding user_interrupt
we want to ensure that it returns true so that it can be called with 'and return'
# File lib/cmd.rb, line 301 def interrupt user_interrupt or true end
Receives the line as it was passed from the prompt (barring modification in precmd) and splits it into a command section and an args section. The args are by default set to nil if they are boolean false or empty then joined with spaces. The tokenize method can be used to further alter the args.
# File lib/cmd.rb, line 485 def parse_line(line) # line will be nil if ctr-D was pressed user_interrupt and return if line.nil? cmd, *args = line.split args = args.empty? ? nil : args * ' ' if args and has_subcommands?(cmd) if cmd = find_subcommand_in_args(subcommands(cmd), line.split) # XXX Completion proc should be passed array of subcommands somewhere args = line.split.join('_').match(/^#{cmd}/).post_match.gsub('_', ' ').strip args = nil if args.empty? end end [cmd, args] end
Receives the returned value of the called command.
# File lib/cmd.rb, line 342 def postcmd(line) line end
Call back executed at the end of the cmdloop.
# File lib/cmd.rb, line 332 def postloop end
Receives line submitted at prompt and passes it along to the command being called.
# File lib/cmd.rb, line 337 def precmd(line) line end
Call back executed at the start of the cmdloop.
# File lib/cmd.rb, line 328 def preloop end
Writes out a message without newlines appended.
# File lib/cmd.rb, line 448 def print(*strings) strings.each {|string| @stdout.write string} end
Displays the help for the passed in command.
# File lib/cmd.rb, line 306 def print_help(cmd) offset = docs.keys.longest_string_length write "#{cmd.ljust(offset)} -- #{docs[cmd]}" + (has_shortcuts?(cmd) ? " #{display_shortcuts(cmd)}" : '') end
# File lib/cmd.rb, line 407 def print_undocumented_commands return if undocumented_commands_hidden? # TODO perhaps do some fancy stuff so that if the number of undocumented # commands is greater than 80 cols or some such passed in number it # presents them in a columnar fashion much the way readline does by default write ' ' write 'Undocumented commands' write '=====================' write undocumented_commands.join(' ' * 4) end
Indicates whether readline support is enabled
# File lib/cmd.rb, line 230 def readline_supported? @readline_supported = READLINE_SUPPORTED if @readline_supported.nil? @readline_supported end
Takes care of collecting the current command and its arguments if any and dispatching the appropriate command.
# File lib/cmd.rb, line 462 def run_command(line) cmd, args = parse_line(line) sanitize_readline_history(line) if line unless cmd then empty_line; return end cmd = translate_shortcut(cmd) self.current_command = cmd set_completion_proc(complete_method(cmd)) if collect_complete.include?(complete_method(cmd)) cmd_method = command(cmd) if self.respond_to?(cmd_method) # Perhaps just catch exceptions here (related to arity) and call a # method that reports a generic error like 'invalid arguments' self.method(cmd_method).arity.zero? ? self.send(cmd_method) : self.send(cmd_method, tokenize_args(args)) else command_missing(current_command, tokenize_args(args)) end end
Runs the customized exception handler for the given exception.
# File lib/cmd.rb, line 241 def run_custom_exception_handling(exception) case handler = custom_exception_handler(exception) when String write handler when Symbol # grych@tg.pl FIX: added exception as an argument self.send(handler, exception) end end
Cleans up the readline history buffer by performing tasks such as removing empty lines and piggy-backed duplicates. Only executed if running with readline support.
# File lib/cmd.rb, line 530 def sanitize_readline_history(line) return unless readline_supported? # Strip out empty lines Readline::HISTORY.pop if line.match(/^\s*$/) # Remove duplicates Readline::HISTORY.pop if Readline::HISTORY[-2] == line rescue IndexError end
Readline completion uses a procedure that takes the current readline buffer and returns an array of possible matches against the current buffer. This method sets the current procedure to use. Commands can specify customized completion procs by defining a method following the naming convetion complet_{command_name}.
# File lib/cmd.rb, line 543 def set_completion_proc(cmd) return unless readline_supported? Readline.completion_proc = self.method(cmd) end
Called at object creation. This can be treated like 'initialize' for sub classes.
# File lib/cmd.rb, line 260 def setup end
# File lib/cmd.rb, line 225 def stoploop @stop = true end
List of all subcommands.
# File lib/cmd.rb, line 381 def subcommand_list with_underscore, without_underscore = collect_do.partition {|command| command.include?('_')} with_underscore.find_all {|do_method| without_underscore.include?(do_method[/^[^_]+/])} end
Lists all subcommands of a given command.
# File lib/cmd.rb, line 387 def subcommands(command) completion_grep(subcommand_list, translate_shortcut(command).to_s + '_') end
Called on command arguments as they are passed into the command.
# File lib/cmd.rb, line 523 def tokenize_args(args) args end
Looks up command shortcuts (e.g. '?' is a shortcut for 'help'). Short cuts can be added by using the shortcut class method.
# File lib/cmd.rb, line 508 def translate_shortcut(cmd) command_lookup_table[cmd] || cmd end
Returns list of undocumented commands.
# File lib/cmd.rb, line 419 def undocumented_commands command_list - documented_commands end
Indicates if any commands are undocumeted.
# File lib/cmd.rb, line 424 def undocumented_commands? !undocumented_commands.empty? end
Called when the user hits ctrl-C or ctrl-D. Terminates execution by default.
# File lib/cmd.rb, line 287 def user_interrupt write 'Terminating' # XXX get rid of this stoploop end
Writes out a message with newline.
# File lib/cmd.rb, line 438 def write(*strings) # We want newlines at the end of every line, so don't join with "\n" strings.each do |string| @stdout.write string @stdout.write "\n" end end