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.
Starts up the command loop
# File lib/cmd.rb, line 147 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 170 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
XXX Not implementd yet. Called when a do_ method that takes arguments doesn't get any
# File lib/cmd.rb, line 282 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 306 def command(cmd) "do_#{cmd}".intern end
Returns lookup table of unambiguous identifiers for commands.
# File lib/cmd.rb, line 364 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 353 def command_list collect_do - subcommand_list end
Definitive list of shortcuts and abbreviations of a command.
# File lib/cmd.rb, line 358 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 539 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 507 def command_shortcuts(cmd) shortcuts[cmd] end
The default completor. Looks up all do_* methods.
# File lib/cmd.rb, line 343 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 418 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 312 def complete_method(cmd) "complete_#{cmd}".intern end
# File lib/cmd.rb, line 422 def completion_grep(collection, pattern) collection.grep(/^#{Regexp.escape(pattern)}/) end
The current command.
# File lib/cmd.rb, line 271 def current_command translate_shortcut @current_command end
Returns the customized handler for the exception
# File lib/cmd.rb, line 242 def custom_exception_handler(exception) custom_exception_handlers[exception.to_s] end
# File lib/cmd.rb, line 543 def default_prompt "#{self.class.name}> " end
Displays the prompt.
# File lib/cmd.rb, line 260 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 301 def display_shortcuts(cmd) "(aliases: #{shortcuts[cmd].join(', ')})" end
Executes a shell, perhaps should only be defined by subclasses.
# File lib/cmd.rb, line 444 def do_shell(line) shell = ENV['SHELL'] line ? write(%(#{line}).strip) : system(shell) end
List of commands which are documented.
# File lib/cmd.rb, line 386 def documented_commands docs.keys.sort end
Called when an empty line is entered in response to the prompt.
# File lib/cmd.rb, line 336 def empty_line end
Determines if the given exception has a custome handler.
# File lib/cmd.rb, line 229 def exception_is_handled?(exception) custom_exception_handler(exception) end
# File lib/cmd.rb, line 197 def execute_command unless ARGV.empty? stoploop execute_line(ARGV * ' ') else execute_line(display_prompt(prompt, true)) end end
# File lib/cmd.rb, line 214 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 491 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 206 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 255 def handle_exception(exception) raise exception end
Indicates if the passed in command has any registerd shortcuts.
# File lib/cmd.rb, line 502 def has_shortcuts?(cmd) command_shortcuts(cmd) end
Indicates whether a given command has any subcommands.
# File lib/cmd.rb, line 381 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 290 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 474 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.to_s.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 331 def postcmd(line) line end
Call back executed at the end of the cmdloop.
# File lib/cmd.rb, line 321 def postloop end
Receives line submitted at prompt and passes it along to the command being called.
# File lib/cmd.rb, line 326 def precmd(line) line end
Call back executed at the start of the cmdloop.
# File lib/cmd.rb, line 317 def preloop end
Writes out a message without newlines appended.
# File lib/cmd.rb, line 437 def print(*strings) strings.each {|string| @stdout.write string} end
Displays the help for the passed in command.
# File lib/cmd.rb, line 295 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 396 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 223 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 451 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 234 def run_custom_exception_handling(exception) case handler = custom_exception_handler(exception) when String: write handler when Symbol: self.send(custom_exception_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 519 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 532 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 249 def setup end
List of all subcommands.
# File lib/cmd.rb, line 370 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 376 def subcommands(command) completion_grep(subcommand_list, translate_shortcut(command) + '_') end
Called on command arguments as they are passed into the command.
# File lib/cmd.rb, line 512 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 497 def translate_shortcut(cmd) command_lookup_table[cmd] || cmd end
Returns list of undocumented commands.
# File lib/cmd.rb, line 408 def undocumented_commands command_list - documented_commands end
Indicates if any commands are undocumeted.
# File lib/cmd.rb, line 413 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 276 def user_interrupt write 'Terminating' # XXX get rid of this stoploop end
Writes out a message with newline.
# File lib/cmd.rb, line 427 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
Generated with the Darkfish Rdoc Generator 2.