Michael Granger avatar Michael Granger committed adb6098

Checkpoint commit.

Comments (0)

Files changed (10)

-Copyright (c) 2009, Michael Granger
+Copyright (c) 2009 Michael Granger
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without

README

-= Ronin Shell
-
-This is an experimental object-oriented command shell, in the same vein as rush (http://rush.heroku.com/) or
-Windows "PowerShell".  
-
-You can check out the current development source with Subversion from the
-following URL:
-
-    http://repo.deveiate.org/RoninShell
-
-You can submit bug reports, suggestions, and read more about future plans at the
-project page:
-
-    http://deveiate.org/roninsh.html
-
-== License
-
-See the included LICENSE file for licensing details.
+h1. Ronin Shell
+
+This is an experimental object-oriented command shell, in the same vein as rush
+(http://rush.heroku.com/) or Windows "PowerShell".
+
+h2. Plans
+
+The stuff I definitely want to implement:
+
+* Proper pty 
+* Job control
+* Traditional @stdio@-based command pipelining.
+
+
+h3. Experimental Stuff
+
+Output from regular system commands are captured automatically, @_@ will be an
+IO opened to the last command's @STDOUT@, and @_err@, the last command's
+@STDERR@.
+
+  $> processes
+   PID TTY           TIME CMD
+   1 ??        14:01.77 /sbin/launchd
+  15 ??         0:56.58 /usr/libexec/kextd
+  16 ??         2:50.14 /usr/sbin/DirectoryService
+  17 ??         1:17.66 /usr/sbin/notifyd
+  18 ??         0:06.29 /usr/sbin/diskarbitrationd
+  19 ??         2:48.09 /usr/libexec/configd
+  20 ??         7:16.62 /usr/sbin/syslogd
+  21 ??         1:05.17 /usr/sbin/distnoted
+  [...]
+
+  $> _.grep( /notify/ )
+   17 ??         1:17.67 /usr/sbin/notifyd
+  275 ??         0:10.63 /usr/sbin/aosnotifyd
+
+  $> pid = _.first.split[0]
+  # => "17"
+
+h2. Hacking
+
+You can check out the current development source with Mercurial like so:
+
+    hg clone https://ged@bitbucket.org/ged/ronin-shell/
+
+You can submit bug reports, suggestions, and read more about future plans at the
+project page:
+
+    http://bitbucket.org/ged/ronin-shell/
+
+
+
+h2. License
+
+See the included LICENSE file for licensing details.
 
 # Gem dependencies: gemname => version
 DEPENDENCIES = {
+	'columnize' => '>= 0.3.1',
 	'pluginfactory' => '>= 1.0.4',
+	'arika-ruby-termios' => '>= 0.9.6',
+	'genki-ruby-terminfo' => '>= 0.1.1',
 }
 
 # Developer Gem dependencies: gemname => version

lib/roninshell/cli.rb

 require 'roninshell/mixins'
 require 'roninshell/constants'
 require 'roninshell/exceptions'
+require 'roninshell/command'
+require 'roninshell/command/builtins'
 
 # 
 # The command-line interpreter for the Ronin Shell.
 #
 class RoninShell::CLI
 	include RoninShell::Constants,
-	        RoninShell::Loggable
+	        RoninShell::Loggable,
+	        RoninShell::UtilityFunctions
 
 	@@option_parsers = {}
 
 	### Create a command-line interpreter with the specified +options+.
 	def initialize( options )
 		@startup_options = options
+
 		@prompt          = DEFAULT_PROMPT
-		@quit            = false
+		@aliases         = {}
+
 		@columns         = TermInfo.screen_width
 		@rows            = TermInfo.screen_height
+		@commands        = RoninShell::Command.require_all
+		@command_table   = self.make_command_table( @commands )
 
-		@commands      = self.find_commands
-		@completions   = @commands.abbrev
-		@command_table = make_command_table( @commands )
+		@quitting        = false
+
+		self.log.debug "%p: set up with %d builtin commands for a %dx%d terminal" %
+			[ self.class, @commands.length, @columns, @rows ]
 	end
 
 
 	# The options struct the CLI was created with.
 	attr_reader :startup_options
 
+	# Quit flag -- setting this to true will cause the shell to exit out of its input loop.
+	attr_accessor :quitting
+
+	# The loaded shell commands
+	attr_reader :commands
+
+
 
 	### Run the shell interpreter with the specified +args.
 	def run( *args )
 		self.read_history
 
 		# Run until something sets the quit flag
-		until @quit
+		until @quitting
 			input = Readline.readline( @prompt, true )
 			self.log.debug "Input is: %p" % [ input ]
 
 			# EOL makes the shell quit
 			if input.nil?
 				self.log.debug "EOL: setting quit flag"
-				@quit = true
+				@quitting = true
 
 			# Blank input -- just reprompt
 			elsif input == ''
 			# Act on everything else
 			else
 				self.log.debug "Dispatching input: %p" % [ input ]
-				self.dispatch_command( input )
+				command, *args = Shellwords.shellwords( input )
+				self.dispatch_command( command, *args )
 			end
 		end
 
 
 	end
 
+
 	### Dispatch a command.
-	def dispatch_command( input )
-		command, *args = Shellwords.shellwords( input )
+	def dispatch_command( command, *args )
 
-		# If it's a builtin command, run it
-		if meth = @command_table[ command ]
-			self.invoke_builtin_command( meth, args )
+		# If it's an alias, recurse 
+		if actual = @aliases[ command ]
+			self.log.debug "%s: Expanding alias to %p" % [ command, actual ]
+			self.dispatch_command( actual, *args )
+
+		# ...if it's a builtin command, run it
+		elsif cmdobj = @command_table[ command ]
+			self.log.debug "%s: Found %p in the command table" % [ command, cmdobj ]
+			self.invoke_command( cmdobj, *args )
 
 		# ...search the $PATH for it
 		elsif path = which( command )
+			self.log.debug "%s: Found %p in the PATH" % [ command, path ]
 			self.invoke_path_command( path, *args )
 
 		# ...otherwise call the fallback handler
 		else
+			self.log.debug "%s: Not found." % [ command ]
 			self.handle_missing_command( command )
 		end
 
 	end
 
 
-	### Invoke a builtin +command+ (a Method object) with the given +args+.
-	def invoke_builtin_command( command, args )
-		full_command = @completions[ command ].to_sym
-
-		# If there's a registered optionparser for the command, use it to 
-		# split out options and arguments, then pass those to the command.
-		if @@option_parsers.key?( full_command )
-			oparser, options = @@option_parsers[ full_command ]
-			self.log.debug "Got an option-parser for #{full_command}."
-
-			cmdargs = oparser.parse( args )
-			self.log.debug "  options=%p, args=%p" % [ options, cmdargs ]
-			meth.call( options, *cmdargs )
-
-			options.clear
-
-		# ...otherwise just call it with all the args.
-		else
-			meth.call( *args )
-		end
+	### Invoke a command object with the given +args_and_options+.
+	def invoke_command( command, *args_and_options )
+		self.log.debug "Invoking %p with args and options: %p" % [ command, args_and_options ]
+		# :TODO: Do option-parsing
+		options, *args = command.parse_options( args_and_options )
+		command.run( options, *args )
 	end
 
 
 	end
 
 
-	### Show help text for the specified command, or a list of all available commands 
-	### if none is specified.
-	def help_command( *args )
-		if args.empty?
-			$stderr.puts
-			message colorize( "Available commands", :bold, :white ),
-				*columnize(@commands)
-		else
-			cmd = args.shift.to_sym
-			if @@option_parsers.key?( cmd )
-				oparser, _ = @@option_parsers[ cmd ]
-				self.log.debug "Setting summary width to: %p" % [ @columns ]
-				oparser.summary_width = @columns
-				output = oparser.to_s.sub( /^(.*?)\n/ ) do |match|
-					colorize( :bold, :white ) { match }
-				end
-
-				$stderr.puts
-				message( output )
-			else
-				error_message( "No help for '#{cmd}'" )
-			end
-		end
+	### Output the specified message +parts+.
+	def message( *parts )
+		$stdout.puts( *parts )
 	end
 
 
-	### Quit the shell.
-	def quit_command( *args )
-		message "Okay, exiting."
-		@quit = true
+	### Output the specified <tt>msg</tt> as an ANSI-colored error message
+	### (white on red).
+	def error_message( msg, details='' )
+		$stderr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + ' ' + details
 	end
-
-
-
+	alias :error :error_message
 
 
 	#########
 		# If there aren't any arguments, it's command completion
 		if parts.length == 1
 			# One completion means it's an unambiguous match, so just complete it.
-			possible_completions = @commands.grep( /^#{Regexp.quote(input)}/ ).sort
+			possible_completions = @command_table.keys.grep( /^#{Regexp.quote(input)}/ ).sort
 			self.log.debug "  possible completions: %p" % [ possible_completions ]
 			return possible_completions
 		else
 			incomplete = parts.pop
-			possible_completions = @currbranch.children.
-				collect {|br| br.rdn }.grep( /^#{Regexp.quote(incomplete)}/ ).sort
-
-			return possible_completions.map do |lastpart|
-				parts.join( ' ' ) + ' ' + lastpart
-			end
+			self.log.warn "I don't yet do programmable or file completion."
+			return []
 		end
 	end
 
 
-	### Find methods that implement commands and return them in a sorted Array.
-	def find_commands
-		return self.public_methods.
-			collect {|mname| mname.to_s }.
-			grep( /^(\w+)_command$/ ).
-			collect {|mname| mname[/^(\w+)_command$/, 1] }.
-			sort
-	end
-
-
 	### Handle a command that doesn't map to a builtin or an executable in the $PATH
 	def handle_missing_command( command )
 		error_message "#$0: #{command}: command not found"
 	end
 
 
-	#######
-	private
-	#######
-
-	### Dump the specified +object+ to a file as YAML, invoke an editor on it, then undump the 
-	### result. If the file has changed, return the updated object, else returns +nil+.
-	def edit_in_yaml( object )
-		yaml = object.to_yaml
-
-		fn = Digest::SHA1.hexdigest( yaml )
-		tf = Tempfile.new( fn )
-
-		# message "Object as YAML is: ", yaml
-		tf.print( yaml )
-		tf.close
-
-		new_yaml = edit( tf.path )
-
-		if new_yaml == yaml
-			message "Unchanged."
-			return nil
-		else
-			return YAML.load( new_yaml )
-		end
-	end
-
-
 	### Create a command table that maps command abbreviations to the Method object that
 	### implements it.
-	def make_command_table( commands )
-		table = commands.abbrev
-		table.keys.each do |abbrev|
-			mname = table.delete( abbrev )
-			table[ abbrev ] = self.method( mname + '_command' )
+	def make_command_table( command_classes )
+		self.log.debug "Making a command table out of %d command classes" % [ command_classes.length ]
+
+		# Map command classes to their canonical command
+		table = command_classes.inject({}) {|hash,cmd| hash[ cmd.command.to_s ] = cmd.new( self ); hash }
+		self.log.debug "  command table (without abbrevs) is: %p" % [ table ]
+
+		# Now add abbreviations
+		abbrevs = table.keys.abbrev
+		abbrevs.keys.each do |abbrev|
+			cmd = abbrevs[ abbrev ]
+			self.log.debug "  mapping abbreviation %p to %p" % [ abbrev, table[cmd] ]
+			table[ abbrev ] ||= table[ cmd ]
 		end
 
 		return table
 	end
 
 
-	### Return the specified args as a string, quoting any that have a space.
-	def quotelist( *args )
-		return args.flatten.collect {|part| part =~ /\s/ ? part.inspect : part}
-	end
-
-
-	### Run the specified command +cmd+ with system(), failing if the execution
-	### fails.
-	def run_command( *cmd )
-		cmd.flatten!
-
-		if cmd.length > 1
-			self.log.debug( quotelist(*cmd) )
-		else
-			self.log.debug( cmd )
-		end
-
-		if $dryrun
-			self.log.error "(dry run mode)"
-		else
-			system( *cmd )
-			unless $?.success?
-				raise "Command failed: [%s]" % [cmd.join(' ')]
-			end
-		end
-	end
-
-
-	### Run the given +cmd+ with the specified +args+ without interpolation by the shell and
-	### return anything written to its STDOUT.
-	def read_command_output( cmd, *args )
-		self.log.debug "Reading output from: %s" % [ cmd, quotelist(cmd, *args) ]
-		output = IO.read( '|-' ) or exec cmd, *args
-		return output
-	end
-
-
-	### Run a subordinate Rake process with the same options and the specified +targets+.
-	def rake( *targets )
-		opts = ARGV.select {|arg| arg[0,1] == '-' }
-		args = opts + targets.map {|t| t.to_s }
-		run 'rake', '-N', *args
-	end
-
-
-	### Open a pipe to a process running the given +cmd+ and call the given block with it.
-	def pipeto( *cmd )
-		$DEBUG = true
-
-		cmd.flatten!
-		self.log.info( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) 
-		if $dryrun
-			message "(dry run mode)"
-		else
-			open( '|-', 'w+' ) do |io|
-
-				# Parent
-				if io
-					yield( io )
-
-				# Child
-				else
-					exec( *cmd )
-					raise "Command failed: [%s]" % [cmd.join(' ')]
-				end
-			end
-		end
-	end
-
-
-	### Return the fully-qualified path to the specified +program+ in the PATH.
-	def which( program )
-		ENV['PATH'].split(/:/).
-			collect {|dir| Pathname.new(dir) + program }.
-			find {|path| path.exist? && path.executable? }
-	end
-
-
-	### Create a string that contains the ANSI codes specified and return it
-	def ansi_code( *attributes )
-		attributes.flatten!
-		attributes.collect! {|at| at.to_s }
-		# message "Returning ansicode for TERM = %p: %p" %
-		# 	[ ENV['TERM'], attributes ]
-		return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
-		attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
-
-		# message "  attr is: %p" % [attributes]
-		if attributes.empty? 
-			return ''
-		else
-			return "\e[%sm" % attributes
-		end
-	end
-
-
-	### Colorize the given +string+ with the specified +attributes+ and return it, handling 
-	### line-endings, color reset, etc.
-	def colorize( *args )
-		string = ''
-
-		if block_given?
-			string = yield
-		else
-			string = args.shift
-		end
-
-		ending = string[/(\s)$/] || ''
-		string = string.rstrip
-
-		return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
-	end
-
-
-	### Output the specified message +parts+.
-	def message( *parts )
-		$stderr.puts( *parts )
-	end
-
-
-	### Output the specified <tt>msg</tt> as an ANSI-colored error message
-	### (white on red).
-	def error_message( msg, details='' )
-		$stderr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + ' ' + details
-	end
-	alias :error :error_message
-
-
-	### Highlight and embed a prompt control character in the given +string+ and return it.
-	def make_prompt_string( string )
-		return CLEAR_CURRENT_LINE + colorize( 'bold', 'yellow' ) { string + ' ' }
-	end
-
-
-	### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
-	### return the user's input with leading and trailing spaces removed.  If a
-	### test is provided, the prompt will repeat until the test returns true.
-	### An optional failure message can also be passed in.
-	def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
-		prompt_string.chomp!
-		prompt_string << ":" unless /\W$/.match( prompt_string )
-		response = nil
-
-		begin
-			prompt = make_prompt_string( prompt_string )
-			response = readline( prompt ) || ''
-			response.strip!
-			if block_given? && ! yield( response ) 
-				error_message( failure_msg + "\n\n" )
-				response = nil
-			end
-		end while response.nil?
-
-		return response
-	end
-
-
-	### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
-	### substituting the given <tt>default</tt> if the user doesn't input
-	### anything.  If a test is provided, the prompt will repeat until the test
-	### returns true.  An optional failure message can also be passed in.
-	def prompt_with_default( prompt_string, default, failure_msg="Try again." )
-		response = nil
-
-		begin
-			default ||= '~'
-			response = prompt( "%s [%s]" % [ prompt_string, default ] )
-			response = default.to_s if !response.nil? && response.empty? 
-
-			self.log.debug "Validating response %p" % [ response ]
-
-			# the block is a validator.  We need to make sure that the user didn't
-			# enter '~', because if they did, it's nil and we should move on.  If
-			# they didn't, then call the block.
-			if block_given? && response != '~' && ! yield( response )
-				error_message( failure_msg + "\n\n" )
-				response = nil
-			end
-		end while response.nil?
-
-		return nil if response == '~'
-		return response
-	end
-
-
-	### Prompt for an array of values
-	def prompt_for_multiple_values( label, default=nil )
-	    message( MULTILINE_PROMPT % [label] )
-	    if default
-			message "Enter a single blank line to keep the default:\n  %p" % [ default ]
-		end
-
-	    results = []
-	    result = nil
-
-	    begin
-	        result = readline( make_prompt_string("> ") )
-			if result.nil? || result.empty?
-				results << default if default && results.empty?
-			else
-	        	results << result 
-			end
-	    end until result.nil? || result.empty?
-
-	    return results.flatten
-	end
-
-
-	### Turn echo and masking of input on/off. 
-	def noecho( masked=false )
-		rval = nil
-		term = Termios.getattr( $stdin )
-
-		begin
-			newt = term.dup
-			newt.c_lflag &= ~Termios::ECHO
-			newt.c_lflag &= ~Termios::ICANON if masked
-
-			Termios.tcsetattr( $stdin, Termios::TCSANOW, newt )
-
-			rval = yield
-		ensure
-			Termios.tcsetattr( $stdin, Termios::TCSANOW, term )
-		end
-
-		return rval
-	end
-
-
-	### Prompt the user for her password, turning off echo if the 'termios' module is
-	### available.
-	def prompt_for_password( prompt="Password: " )
-		rval = nil
-		noecho( true ) do
-			$stderr.print( prompt )
-			rval = ($stdin.gets || '').chomp
-		end
-		$stderr.puts
-		return rval
-	end
-
-
-	### Display a description of a potentially-dangerous task, and prompt
-	### for confirmation. If the user answers with anything that begins
-	### with 'y', yield to the block. If +abort_on_decline+ is +true+,
-	### any non-'y' answer will fail with an error message.
-	def ask_for_confirmation( description, abort_on_decline=true )
-		puts description
-
-		answer = prompt_with_default( "Continue?", 'n' ) do |input|
-			input =~ /^[yn]/i
-		end
-
-		if answer =~ /^y/i
-			return yield
-		elsif abort_on_decline
-			error "Aborted."
-			fail
-		end
-
-		return false
-	end
-	alias :prompt_for_confirmation :ask_for_confirmation
-
-
-	### Search line-by-line in the specified +file+ for the given +regexp+, returning the
-	### first match, or nil if no match was found. If the +regexp+ has any capture groups,
-	### those will be returned in an Array, else the whole matching line is returned.
-	def find_pattern_in_file( regexp, file )
-		rval = nil
-
-		File.open( file, 'r' ).each do |line|
-			if (( match = regexp.match(line) ))
-				rval = match.captures.empty? ? match[0] : match.captures
-				break
-			end
-		end
-
-		return rval
-	end
-
-
-	### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
-	### returning the first match, or nil if no match was found. If the +regexp+ has any 
-	### capture groups, those will be returned in an Array, else the whole matching line
-	### is returned.
-	def find_pattern_in_pipe( regexp, *cmd )
-		output = []
-
-		self.log.info( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) 
-		Open3.popen3( *cmd ) do |stdin, stdout, stderr|
-			stdin.close
-
-			output << stdout.gets until stdout.eof?
-			output << stderr.gets until stderr.eof?
-		end
-
-		result = output.find { |line| regexp.match(line) } 
-		return $1 || result
-	end
-
-
-	### Invoke the user's editor on the given +filename+ and return the exit code
-	### from doing so.
-	def edit( filename )
-		editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
-		system editor, filename.to_s
-		unless $?.success? || editor =~ /vim/i
-			raise "Editor exited with an error status (%d)" % [ $?.exitstatus ]
-		end
-		return File.read( filename )
-	end
-
-
-	### Make an easily-comparable version vector out of +ver+ and return it.
-	def vvec( ver )
-		return ver.split('.').collect {|char| char.to_i }.pack('N*')
-	end
-
-
-	### Return an ANSI-colored version of the given +rdn+ string.
-	def format_rdn( rdn )
-		rdn.split( /,/ ).collect do |rdn|
-			key, val = rdn.split( /\s*=\s*/, 2 )
-			colorize( :white ) { key } +
-				colorize( :bold, :black ) { '=' } +
-				colorize( :bold, :white ) { val }
-		end.join( colorize(',', :green) )
-	end
-
-
-	### Highlight LDIF and return it.
-	def format_ldif( ldif )
-		return ldif.gsub( /^(\S[^:]*)(::?)\s*(.*)$/ ) do
-			key, sep, val = $1, $2, $3
-			case sep
-			when '::'
-				colorize( :cyan ) { key } + ':: ' + colorize( :dark, :white ) { val }
-			when ':'
-				colorize( :bold, :cyan ) { key } + ': ' + colorize( :dark, :white ) { val }
-			else
-				key + sep + ' ' + val
-			end
-		end
-	end
-
-
-	### Return the specified +entries+ as an Array of span-sorted columns fit to the
-	### current terminal width.
-	def columnize( *entries )
-		return Columnize.columnize( entries.flatten, @columns, '  ' )
-	end
-
-
-
-
 end # class RoninShell::CLI
 
 

lib/roninshell/command.rb

+#!/usr/bin/ruby
+
+require 'roninshell'
+require 'roninshell/cli'
+require 'roninshell/mixins'
+require 'roninshell/exceptions'
+
+
+# The base shell command class.
+# 
+# == Usage
+# 
+#   # yourneatolib/roninshell/commands.rb
+#   require 'roninshell/command'
+#   
+#   class NeatoCommand < RoninShell::Command
+#     name :neato
+#     
+#     def run( shell, *args )
+#       return self.neato_object
+#     end
+#     
+#   end
+#   
+class RoninShell::Command
+	include RoninShell::Loggable
+
+	# Command plugin loader 
+	COMMAND_PLUGIN_LOADER = 'roninshell/commands'
+
+	# Suffixes of files to try to load for commands -- stolen from RubyGems.
+	COMMAND_SUFFIXES = ['', '.rb', '.rbw', '.so', '.bundle', '.dll', '.sl', '.jar']
+
+	# Glob pattern for finding files that end in one of the COMMAND_SUFFIXES
+	COMMAND_SUFFIX_PATTERN = '{' + COMMAND_SUFFIXES.join(',') + '}'
+
+
+	#################################################################
+	###	C L A S S   M E T H O D S
+	#################################################################
+
+	# Subclasses of RoninShell::Command
+	@@subclasses = []
+
+	# The command name
+	@command = nil
+
+	# OptionParser instance and option struct for this command
+	@option_parser = nil
+
+	# OpenStruct prototype
+	@options = nil
+
+
+	### Return the Array of all known subclasses.
+	def self::subclasses
+		return @@subclasses
+	end
+
+
+	### Inheritance callback -- track subclasses of Command for later instantiation.
+	def self::inherited( subclass )
+		RoninShell.logger.debug "Loaded %s (%s)" % [ subclass.name, subclass ]
+		@@subclasses << subclass
+		subclass.instance_variable_set( :@command, nil )
+		super
+	end
+
+
+	### Search the $LOAD_PATH (and installed Gems, if Rubygems is loaded) for
+	### files under COMMAND_LIB_PREFIX, loading each one in turn. Once they're all loaded,
+	### return any resulting command classes.
+	def self::require_all
+		files = if defined?( Gem )
+				Gem.find_files( COMMAND_PLUGIN_LOADER )
+			else
+				$LOAD_PATH.collect do |dir|
+					pattern = File.expand_path(COMMAND_PLUGIN_LOADER, dir) + COMMAND_SUFFIX_PATTERN
+					Dir[ pattern ].select do |path|
+						File.file?( path.untaint )
+					end
+				end.flatten
+			end
+
+		require( *files ) unless files.empty?
+
+		return self.subclasses
+	end
+
+
+	### Get/set the command name
+	def self::command( newname=nil )
+		@command = newname if newname
+		return @command
+	end
+
+
+	### Create an option parser from the specified +block+ for the given +command+ and register
+	### it. Many thanks to apeiros and dominikh on #Ruby-Pro for the ideas behind this.
+	def self::set_options( command, &block )
+	    ostruct = OpenStruct.new
+		oparser = OptionParser.new( "Help for #{command}" ) do |o|
+			yield( o, options )
+		end
+		oparser.default_argv = []
+
+		self.option_parser = oparser
+		self.options = ostruct
+	end
+
+
+	#################################################################
+	###	I N S T A N C E   M E T H O D S
+	#################################################################
+
+	### Create a new instance of the command for the given +cli+ instance.
+	def initialize( cli )
+		@cli = cli
+	end
+
+
+	######
+	public
+	######
+
+	# The RoninShell::CLI instance this instance belongs to.
+	attr_reader :cli
+
+
+	### Return the name of the command.
+	def command
+		return self.class.command
+	end
+
+
+	### Virtual method -- you must override this method in your own command class.
+	def run( options, *args )
+		raise NotImplementedError, "%s does implement #run" % [ self.class.command ]
+	end
+
+
+end # class RoninShell::Command
+
+# vim: set nosta noet ts=4 sw=4:
+

lib/roninshell/command/builtins.rb

+#!/usr/bin/ruby
+
+require 'logger'
+require 'roninshell/command'
+
+
+#--
+# A collection of builtin commands for the Ronin Shell.
+#
+module RoninShell # :nodoc:
+
+	### The 'cd' command class.
+	class CdCommand < RoninShell::Command
+		command :cd
+
+		### Run the command.
+		def run( options, target, *ignored )
+			full_path = File.expand_path( target, Dir.pwd )
+			Dir.chdir( full_path )
+			self.cli.message( full_path )
+		end
+
+	end # RoninShell::CdCommand
+
+
+	### The 'ls' command class.
+	class LsCommand < RoninShell::Command
+		command :ls
+
+		### Run the command.
+		def run( options, target, *ignored )
+			full_path = File.expand_path( target, Dir.pwd )
+			Dir.chdir( full_path )
+			self.cli.message( full_path )
+		end
+
+	end # RoninShell::LsCommand
+
+
+
+end # module RoninShell
+
+# vim: set nosta noet ts=4 sw=4:
+

lib/roninshell/command/process.rb

+#!/usr/bin/ruby
+
+require 'logger'
+require 'sys/proctable'
+
+require 'roninshell/command'
+
+
+#--
+# A collection of stdobj-based process-related commands for the Ronin Shell.
+#
+module RoninShell # :nodoc:
+
+	### The 'process' command class.
+	class ProcessCommand < RoninShell::Command
+		command :process
+
+		### Run the command.
+		def run( options, *ignored )
+			return Sys::Proctable.ps
+		end
+
+	end # RoninShell::ProcessCommand
+
+
+end # module RoninShell
+
+# vim: set nosta noet ts=4 sw=4:
+

lib/roninshell/constants.rb

 	}
 
 
-
 end # module RoninShell::Constants

lib/roninshell/utilities.rb

 #
 module RoninShell
 
+
+	# A collection of utility functions for use in the Ronin Shell.
+	module UtilityFunctions
+		include RoninShell::Constants
+
+		###############
+		module_function
+		###############
+
+		### Dump the specified +object+ to a file as YAML, invoke an editor on it, then undump the 
+		### result. If the file has changed, return the updated object, else returns +nil+.
+		def edit_in_yaml( object )
+			yaml = object.to_yaml
+
+			fn = Digest::SHA1.hexdigest( yaml )
+			tf = Tempfile.new( fn )
+
+			tf.print( yaml )
+			tf.close
+
+			new_yaml = edit( tf.path )
+
+			if new_yaml == yaml
+				return nil
+			else
+				return YAML.load( new_yaml )
+			end
+		end
+
+
+		### Create a command table that maps command abbreviations to the Method object that
+		### implements it.
+		def make_command_table( commands )
+			table = commands.abbrev
+			table.keys.each do |abbrev|
+				mname = table.delete( abbrev )
+				table[ abbrev ] = self.method( mname + '_command' )
+			end
+
+			return table
+		end
+
+
+		### Return the specified args as a string, quoting any that have a space.
+		def quotelist( *args )
+			return args.flatten.collect {|part| part =~ /\s/ ? part.inspect : part}
+		end
+
+
+		### Run the specified command +cmd+ with system(), failing if the execution
+		### fails.
+		def run_command( *cmd )
+			cmd.flatten!
+
+			if cmd.length > 1
+				self.log.debug( quotelist(*cmd) )
+			else
+				self.log.debug( cmd )
+			end
+
+			if $dryrun
+				self.log.error "(dry run mode)"
+			else
+				system( *cmd )
+				unless $?.success?
+					raise "Command failed: [%s]" % [cmd.join(' ')]
+				end
+			end
+		end
+
+
+		### Run the given +cmd+ with the specified +args+ without interpolation by the shell and
+		### return anything written to its STDOUT.
+		def read_command_output( cmd, *args )
+			self.log.debug "Reading output from: %s" % [ cmd, quotelist(cmd, *args) ]
+			output = IO.read( '|-' ) or exec cmd, *args
+			return output
+		end
+
+
+		### Open a pipe to a process running the given +cmd+ and call the given block with it.
+		def pipeto( *cmd )
+			$DEBUG = true
+
+			cmd.flatten!
+			self.log.info( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) 
+			if $dryrun
+				$stderr.puts "(dry run mode)"
+			else
+				open( '|-', 'w+' ) do |io|
+
+					# Parent
+					if io
+						yield( io )
+
+					# Child
+					else
+						exec( *cmd )
+						raise "Command failed: [%s]" % [cmd.join(' ')]
+					end
+				end
+			end
+		end
+
+
+		### Return the fully-qualified path to the specified +program+ in the PATH.
+		def which( program )
+			ENV['PATH'].split(/:/).
+				collect {|dir| Pathname.new(dir) + program }.
+				find {|path| path.exist? && path.executable? }
+		end
+
+
+		### Create a string that contains the ANSI codes specified and return it
+		def ansi_code( *attributes )
+			attributes.flatten!
+			attributes.collect! {|at| at.to_s }
+			# $stderr.puts "Returning ansicode for TERM = %p: %p" %
+			# 	[ ENV['TERM'], attributes ]
+			return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
+			attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
+
+			# $stderr.puts "  attr is: %p" % [attributes]
+			if attributes.empty? 
+				return ''
+			else
+				return "\e[%sm" % attributes
+			end
+		end
+
+
+		### Colorize the given +string+ with the specified +attributes+ and return it, handling 
+		### line-endings, color reset, etc.
+		def colorize( *args )
+			string = ''
+
+			if block_given?
+				string = yield
+			else
+				string = args.shift
+			end
+
+			ending = string[/(\s)$/] || ''
+			string = string.rstrip
+
+			return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
+		end
+
+
+		### Highlight and embed a prompt control character in the given +string+ and return it.
+		def make_prompt_string( string )
+			return CLEAR_CURRENT_LINE + colorize( 'bold', 'yellow' ) { string + ' ' }
+		end
+
+
+		### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
+		### return the user's input with leading and trailing spaces removed.  If a
+		### test is provided, the prompt will repeat until the test returns true.
+		### An optional failure message can also be passed in.
+		def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
+			prompt_string.chomp!
+			prompt_string << ":" unless /\W$/.match( prompt_string )
+			response = nil
+
+			begin
+				prompt = make_prompt_string( prompt_string )
+				response = readline( prompt ) || ''
+				response.strip!
+				if block_given? && ! yield( response )
+					$stderr.puts( failure_msg + "\n\n" )
+					response = nil
+				end
+			end while response.nil?
+
+			return response
+		end
+
+
+		### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
+		### substituting the given <tt>default</tt> if the user doesn't input
+		### anything.  If a test is provided, the prompt will repeat until the test
+		### returns true.  An optional failure message can also be passed in.
+		def prompt_with_default( prompt_string, default, failure_msg="Try again." )
+			response = nil
+
+			begin
+				default ||= '~'
+				response = prompt( "%s [%s]" % [ prompt_string, default ] )
+				response = default.to_s if !response.nil? && response.empty? 
+
+				self.log.debug "Validating response %p" % [ response ]
+
+				# the block is a validator.  We need to make sure that the user didn't
+				# enter '~', because if they did, it's nil and we should move on.  If
+				# they didn't, then call the block.
+				if block_given? && response != '~' && ! yield( response )
+					$stderr.puts( failure_msg + "\n\n" )
+					response = nil
+				end
+			end while response.nil?
+
+			return nil if response == '~'
+			return response
+		end
+
+
+		### Prompt for an array of values
+		def prompt_for_multiple_values( label, default=nil )
+		    $stderr.puts( MULTILINE_PROMPT % [label] )
+		    if default
+				$stderr.puts "Enter a single blank line to keep the default:\n  %p" % [ default ]
+			end
+
+		    results = []
+		    result = nil
+
+		    begin
+		        result = readline( make_prompt_string("> ") )
+				if result.nil? || result.empty?
+					results << default if default && results.empty?
+				else
+		        	results << result 
+				end
+		    end until result.nil? || result.empty?
+
+		    return results.flatten
+		end
+
+
+		### Turn echo and masking of input on/off. 
+		def noecho( masked=false )
+			rval = nil
+			term = Termios.getattr( $stdin )
+
+			begin
+				newt = term.dup
+				newt.c_lflag &= ~Termios::ECHO
+				newt.c_lflag &= ~Termios::ICANON if masked
+
+				Termios.tcsetattr( $stdin, Termios::TCSANOW, newt )
+
+				rval = yield
+			ensure
+				Termios.tcsetattr( $stdin, Termios::TCSANOW, term )
+			end
+
+			return rval
+		end
+
+
+		### Prompt the user for her password, turning off echo if the 'termios' module is
+		### available.
+		def prompt_for_password( prompt="Password: " )
+			rval = nil
+			noecho( true ) do
+				$stderr.print( prompt )
+				rval = ($stdin.gets || '').chomp
+			end
+			$stderr.puts
+			return rval
+		end
+
+
+		### Display a description of a potentially-dangerous task, and prompt
+		### for confirmation. If the user answers with anything that begins
+		### with 'y', yield to the block. If +abort_on_decline+ is +true+,
+		### any non-'y' answer will fail with an error message.
+		def ask_for_confirmation( description, abort_on_decline=true )
+			puts description
+
+			answer = prompt_with_default( "Continue?", 'n' ) do |input|
+				input =~ /^[yn]/i
+			end
+
+			if answer =~ /^y/i
+				return yield
+			elsif abort_on_decline
+				error "Aborted."
+				fail
+			end
+
+			return false
+		end
+		alias :prompt_for_confirmation :ask_for_confirmation
+
+
+		### Search line-by-line in the specified +file+ for the given +regexp+, returning the
+		### first match, or nil if no match was found. If the +regexp+ has any capture groups,
+		### those will be returned in an Array, else the whole matching line is returned.
+		def find_pattern_in_file( regexp, file )
+			rval = nil
+
+			File.open( file, 'r' ).each do |line|
+				if (( match = regexp.match(line) ))
+					rval = match.captures.empty? ? match[0] : match.captures
+					break
+				end
+			end
+
+			return rval
+		end
+
+
+		### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
+		### returning the first match, or nil if no match was found. If the +regexp+ has any 
+		### capture groups, those will be returned in an Array, else the whole matching line
+		### is returned.
+		def find_pattern_in_pipe( regexp, *cmd )
+			output = []
+
+			self.log.info( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) 
+			Open3.popen3( *cmd ) do |stdin, stdout, stderr|
+				stdin.close
+
+				output << stdout.gets until stdout.eof?
+				output << stderr.gets until stderr.eof?
+			end
+
+			result = output.find { |line| regexp.match(line) } 
+			return $1 || result
+		end
+
+
+		### Invoke the user's editor on the given +filename+ and return the exit code
+		### from doing so.
+		def edit( filename )
+			editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
+			system editor, filename.to_s
+			unless $?.success? || editor =~ /vim/i
+				raise "Editor exited with an error status (%d)" % [ $?.exitstatus ]
+			end
+			return File.read( filename )
+		end
+
+
+		### Make an easily-comparable version vector out of +ver+ and return it.
+		def vvec( ver )
+			return ver.split('.').collect {|char| char.to_i }.pack('N*')
+		end
+
+
+		### Return the specified +entries+ as an Array of span-sorted columns fit to the
+		### current terminal width.
+		def columnize( *entries )
+			return Columnize.columnize( entries.flatten, @columns, '  ' )
+		end
+
+	end # module UtilityFunctions
+
+
 	# 
 	# A alternate formatter for Logger instances.
 	# 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.