Commits

Michael Granger committed 9195b85

Split out the shell tools into their own gem

  • Participants
  • Parent commits 412e097

Comments (0)

Files changed (8)

 hoe-deveiate -v0.1.1
 ruby-ldap -v0.9.12
-sequel -v3.31.0
+sequel -v3.38.0
 loggability -v0.4.0
 

File Manifest.txt

 Manifest.txt
 README.rdoc
 Rakefile
-bin/treeirb
-bin/treequel
-bin/treewhat
 examples/company-directory.rb
 examples/ldap-rack-auth.rb
 examples/ldap_state.rb
 kick-ass database library.
 
 
+== Command-Line Tools
+
+There are several command-line tools that are built on top of Treequel in
+the 'treequel-shell' gem. They include:
+
+treequel :: an LDAP shell/editor; treat your LDAP directory like a filesystem!
+treewhat :: an LDAP schema explorer. Dump objectClasses and attribute type info
+            in several convenient formats.
+
+
 == Contributing
 
 You can check out the current development source
 	self.dependency 'loggability', '~> 0.4'
 
 	self.dependency 'rspec', '~> 2.8', :developer
-	self.dependency 'ruby-termios', '~> 0.9', :developer
-	self.dependency 'ruby-terminfo', '~> 0.1', :developer
-	self.dependency 'columnize', '~> 0.3', :developer
-	self.dependency 'sysexits', '~> 1.0', :developer
-	self.dependency 'sequel', '~> 3.20', :developer
+	self.dependency 'sequel', '~> 3.38', :developer
 
 	self.spec_extras[:licenses] = ["BSD"]
 	self.spec_extras[:post_install_message] = [
-		"If you want to use the included 'treequel' LDAP shell, you'll need to install",
-		"the following libraries as well:",
-		'',
-		"    - ruby-termios",
-		"    - ruby-terminfo",
-		"    - columnize",
-		"    - sysexits",
-		'',
-		"You can install them automatically if you use the --development flag when",
-		"installing Treequel."
+		'-' * 72,
+		"NOTE: The Treequel command-line tools are no longer distributed ",
+		"with the Treequel gem; to get the tools, install the 'treequel-shell' ",
+		"gem. Thanks!",
+		'-' * 72
 	  ].join( "\n" )
 
 	self.require_ruby_version( '>=1.8.7' )
 # Ensure the specs pass before checking in
 task 'hg:precheckin' => [ 'ChangeLog', :check_history, :check_manifest, :spec ]
 
-### Make the ChangeLog update if the repo has changed since it was last built
-file '.hg/branch'
-file 'ChangeLog' => '.hg/branch' do |task|
-	$stderr.puts "Updating the changelog..."
-	content = make_changelog()
-	File.open( task.name, 'w', 0644 ) do |fh|
-		fh.print( content )
-	end
-end
-
 # Rebuild the ChangeLog immediately before release
 task :prerelease => 'ChangeLog'
 

File bin/treeirb

-#!/usr/bin/env ruby
-
-require 'irb'
-require 'irb/extend-command'
-require 'irb/cmd/nop'
-require 'treequel'
-
-
-if uri = ARGV.shift
-	$dir = Treequel.directory( uri )
-else
-	$dir = Treequel.directory_from_config
-end
-
-$stderr.puts "Directory is in $dir:", '  ' + $dir.inspect
-
-IRB.start( $0 )
-

File bin/treequel

-#!/usr/bin/env ruby
-
-require 'abbrev'
-require 'columnize'
-require 'diff/lcs'
-require 'digest/sha1'
-require 'irb'
-require 'logger'
-require 'open3'
-require 'optparse'
-require 'ostruct'
-require 'pathname'
-require 'readline'
-require 'shellwords'
-require 'tempfile'
-require 'terminfo'
-require 'termios'
-require 'uri'
-require 'yaml'
-
-require 'treequel'
-require 'treequel/mixins'
-require 'treequel/constants'
-
-
-### Monkeypatch for resetting an OpenStruct's state.
-class OpenStruct
-
-	### Clear all defined fields and values.
-	def clear
-		@table.clear
-	end
-
-end
-
-
-### IRb.start_session, courtesy of Joel VanderWerf in [ruby-talk:42437].
-require 'irb'
-require 'irb/completion'
-
-module IRB # :nodoc:
-	def self.start_session( obj )
-		unless @__initialized
-			args = ARGV
-			ARGV.replace( [] )
-			IRB.setup( nil )
-			ARGV.replace( args )
-			@__initialized = true
-		end
-
-		workspace = WorkSpace.new( obj )
-		irb = Irb.new( workspace )
-
-		@CONF[:IRB_RC].call( irb.context ) if @CONF[:IRB_RC]
-		@CONF[:MAIN_CONTEXT] = irb.context
-
-		begin
-			prevhandler = Signal.trap( 'INT' ) do
-				irb.signal_handle
-			end
-
-			catch( :IRB_EXIT ) do
-				irb.eval_input
-			end
-		ensure
-			Signal.trap( 'INT', prevhandler )
-		end
-
-	end
-end
-
-# The Treequel shell.
-#
-#	TODO:
-#   * Make more commands use the convert_to_branchsets utility function
-#
-class Treequel::Shell
-	include Readline,
-	        Treequel::Loggable,
-	        Treequel::ANSIColorUtilities,
-	        Treequel::Constants::Patterns,
-	        Treequel::HashUtilities
-
-	extend Treequel::ANSIColorUtilities
-
-	# Prompt text for #prompt_for_multiple_values
-	MULTILINE_PROMPT = <<-'EOF'
-	Enter one or more values for '%s'.
-	A blank line finishes input.
-	EOF
-
-	# Some ANSI codes for fancier stuff
-	CLEAR_TO_EOL       = "\e[K"
-	CLEAR_CURRENT_LINE = "\e[2K"
-
-	# Valid connect-type arguments
-	VALID_CONNECT_TYPES = %w[tls ssl plain]
-
-	# Command option parsers
-	@@option_parsers = {}
-
-	# Path to the default history file
-	HISTORY_FILE = Pathname( "~/.treequel.history" )
-
-	# Number of items to store in history by default
-	DEFAULT_HISTORY_SIZE = 100
-
-	# The default editor, in case ENV['VISUAL'] and ENV['EDITOR'] are unset
-	DEFAULT_EDITOR = 'vi'
-
-
-	#################################################################
-	###	C L A S S   M E T H O D S
-	#################################################################
-
-	### Run the shell.
-	def self::run( args )
-		Treequel.logger.formatter = Treequel::ColorLogFormatter.new( Treequel.logger )
-		bind_as, plaintext, uri = self.parse_options( args )
-
-		connect_type = plaintext ? :plain : :tls
-
-		directory = if uri
-			Treequel.directory( uri, :connect_type => connect_type )
-		else
-			Treequel.directory_from_config
-		end
-
-		Treequel::Shell.new( directory ).run( bind_as )
-	end
-
-
-	### Parse command-line options for shell startup and return an options struct and
-	### the LDAP URI.
-	def self::parse_options( argv )
-		progname = File.basename( $0 )
-		loglevels = Treequel::LOG_LEVELS.
-			sort_by {|_,lvl| lvl }.
-			collect {|name,lvl| name.to_s }.
-			join(', ')
-		bind_as = nil
-		plaintext = false
-
-		oparser = OptionParser.new( "Usage: #{progname} [OPTIONS] [LDAPURL]" ) do |oparser|
-			oparser.separator ' '
-
-			oparser.on( '--binddn=DN', '-b DN', String, "Bind as DN" ) do |dn|
-				bind_as = dn
-			end
-
-			oparser.on( '--no-tls', FalseClass, "Use a plaintext (unencrypted) connection.",
-			 	"If you don't specify a connection URL, this option is ignored." ) do
-				plaintext = true
-			end
-
-			oparser.on( '--loglevel=LEVEL', '-l LEVEL', Treequel::LOG_LEVELS.keys,
-				"Set the logging level. Should be one of:", loglevels ) do |lvl|
-				Treequel.logger.level = Treequel::LOG_LEVELS[ lvl ] or
-					raise "Invalid logging level %p" % [ lvl ]
-			end
-
-			oparser.on( '--debug', '-d', FalseClass, "Turn debugging on" ) do
-				$DEBUG = true
-				$trace = true
-				Treequel.logger.level = Logger::DEBUG
-			end
-
-			oparser.on("-h", "--help", "Show this help message.") do
-				$stderr.puts( oparser )
-				exit!
-			end
-		end
-
-		remaining_args = oparser.parse( argv )
-
-		return bind_as, plaintext, *remaining_args
-	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 )
-	    options = OpenStruct.new
-		oparser = OptionParser.new( "Help for #{command}" ) do |o|
-			yield( o, options )
-		end
-		oparser.default_argv = []
-
-		@@option_parsers[command.to_sym] = [oparser, options]
-	end
-
-
-	#################################################################
-	###	I N S T A N C E   M E T H O D S
-	#################################################################
-
-	### Create a new shell for the specified +directory+ (a Treequel::Directory).
-	def initialize( directory )
-		@dir        = directory
-		@uri        = directory.uri
-		@quit       = false
-		@currbranch = @dir
-		@columns    = TermInfo.screen_width
-		@rows       = TermInfo.screen_height
-
-		@commands = self.find_commands
-		@completions = @commands.abbrev
-		@command_table = make_command_table( @commands )
-	end
-
-
-	######
-	public
-	######
-
-	# The number of columns in the current terminal
-	attr_reader :columns
-
-	# The number of rows in the current terminal
-	attr_reader :rows
-
-	# The flag which causes the shell to exit after the current loop
-	attr_accessor :quit
-
-
-	### The command loop: run the shell until the user wants to quit, binding as +bind_as+ if
-	### given.
-	def run( bind_as=nil )
-		@original_tty_settings = IO.read( '|-' ) or exec 'stty', '-g'
-		message "Connected to %s" % [ @uri ]
-
-		# Set up the completion callback
-		self.setup_completion
-
-		# Load saved command-line history
-		self.read_history
-
-		# If the user said to bind as someone on the command line, invoke a
-		# 'bind' command before dropping into the command line
-		if bind_as
-			options = OpenStruct.new # dummy options object
-			self.bind_command( options, bind_as )
-		end
-
-		# Run until something sets the quit flag
-		until @quit
-			$stderr.puts
-			prompt = make_prompt_string( @currbranch.dn + '> ' )
-			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
-
-			# Blank input -- just reprompt
-			elsif input == ''
-				self.log.debug "No command. Re-displaying the prompt."
-
-			# Parse everything else into command + args
-			else
-				self.log.debug "Dispatching input: %p" % [ input ]
-				self.dispatch_cmd( input )
-			end
-		end
-
-		message "\nSaving history...\n"
-		self.save_history
-
-		message "done."
-
-	rescue => err
-		error_message( err.class.name, err.message )
-		err.backtrace.each do |frame|
-			self.log.debug "  " + frame
-		end
-
-	ensure
-		system( 'stty', @original_tty_settings.chomp )
-	end
-
-
-	### Parse the specified +input+ into a command, options, and arguments and dispatch them
-	### to the appropriate command method.
-	def dispatch_cmd( input )
-		command, *args = Shellwords.shellwords( input )
-
-		# If it's a valid command, run it
-		if meth = @command_table[ command ]
-			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
-				self.log.warn "  no options defined for '%s' command" % [ command ]
-				meth.call( *args )
-			end
-
-		# ...otherwise call the fallback handler
-		else
-			self.handle_missing_cmd( command )
-		end
-
-	rescue LDAP::ResultError => err
-		case err.message
-		when /can't contact ldap server/i
-			if @dir.connected?
-				error_message( "LDAP connection went away." )
-			else
-				error_message( "Couldn't connect to the server." )
-			end
-			ask_for_confirmation( "Attempt to reconnect?" ) do
-				@dir.reconnect
-			end
-			retry
-
-		when /invalid credentials/i
-			error_message( "Authentication failed." )
-		else
-			error_message( err.class.name, err.message )
-			self.log.debug { "  " + err.backtrace.join("  \n") }
-		end
-
-	rescue => err
-		error_message( err.message )
-		self.log.debug { "  " + err.backtrace.join("  \n") }
-	end
-
-
-
-	#########
-	protected
-	#########
-
-	### Set up Readline completion
-	def setup_completion
-		Readline.completion_proc = self.method( :completion_callback ).to_proc
-		Readline.completer_word_break_characters = ''
-		Readline.basic_word_break_characters = ''
-	end
-
-
-	### Read command line history from HISTORY_FILE
-	def read_history
-		histfile = HISTORY_FILE.expand_path
-
-		if histfile.exist?
-			lines = histfile.readlines.collect {|line| line.chomp }
-			self.log.debug "Read %d saved history commands from %s." % [ lines.length, histfile ]
-			Readline::HISTORY.push( *lines )
-		else
-			self.log.debug "History file '%s' was empty or non-existant." % [ histfile ]
-		end
-	end
-
-
-	### Save command line history to HISTORY_FILE
-	def save_history
-		histfile = HISTORY_FILE.expand_path
-
-		lines = Readline::HISTORY.to_a.reverse.uniq.reverse
-		lines = lines[ -DEFAULT_HISTORY_SIZE, DEFAULT_HISTORY_SIZE ] if
-			lines.length > DEFAULT_HISTORY_SIZE
-
-		self.log.debug "Saving %d history lines to %s." % [ lines.length, histfile ]
-
-		histfile.open( File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
-			ofh.puts( *lines )
-		end
-	end
-
-
-	### Handle completion requests from Readline.
-	def completion_callback( input )
-		self.log.debug "Input completion: %p" % [ input ]
-		parts = Shellwords.shellwords( input )
-
-		# If there aren't any arguments, it's command completion
-		if parts.empty?
-			possible_completions = @commands.sort
-			self.log.debug "  possible completions: %p" % [ possible_completions ]
-			return possible_completions
-		elsif parts.length == 1
-			# One completion means it's an unambiguous match, so just complete it.
-			possible_completions = @commands.grep( /^#{Regexp.quote(input)}/ ).sort
-			self.log.debug "  possible completions: %p" % [ possible_completions ]
-			return possible_completions
-		else
-			incomplete = parts.pop
-			self.log.debug "  the incomplete bit is: %p" % [ incomplete ]
-			possible_completions = @currbranch.children.
-				collect {|br| br.rdn }.grep( /^#{Regexp.quote(incomplete)}/i ).sort
-
-			possible_completions.map! do |lastpart|
-				parts.join( ' ' ) + ' ' + lastpart
-			end
-
-			self.log.debug "  possible (argument) completions: %p" % [ possible_completions ]
-			return possible_completions
-		end
-	end
-
-
-	#################################################################
-	###	C O M M A N D S
-	#################################################################
-
-	### Show the completions hash
-	def show_completions_command
-		message "Completions:", @completions.inspect
-	end
-	set_options :show_completions do |oparser, options|
-		oparser.banner = "show_completions"
-		oparser.separator 'Show the list of command completions (for debugging the shell)'
-	end
-
-
-	### Show help text for the specified command, or a list of all available commands 
-	### if none is specified.
-	def help_command( options, *args )
-		if args.empty?
-			$stderr.puts
-			message colorize( "Available commands", :bold, :white ),
-				*columnize(@commands)
-		else
-			cmd = args.shift
-			full_command = @completions[ cmd ]
-
-			if @@option_parsers.key?( full_command.to_sym )
-				oparser, _ = @@option_parsers[ full_command.to_sym ]
-				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
-	end
-	set_options :help do |oparser, options|
-		oparser.banner = "help [COMMAND]"
-		oparser.separator 'Display general help, or help for a specific COMMAND.'
-	end
-
-
-	### Quit the shell.
-	def quit_command( options, *args )
-		message "Okay, exiting."
-		self.quit = true
-	end
-	set_options :quit do |oparser, options|
-		oparser.banner = "quit"
-		oparser.separator 'Exit the shell.'
-	end
-
-
-	### Set the logging level (if invoked with an argument) or display the current
-	### level (with no argument).
-	def log_command( options, *args )
-		newlevel = args.shift
-		if newlevel
-			if Treequel::LOG_LEVELS.key?( newlevel )
-				Treequel.logger.level = Treequel::LOG_LEVELS[ newlevel ]
-				message "Set log level to: %s" % [ newlevel ]
-			else
-				levelnames = Treequel::LOG_LEVEL_NAMES.keys.sort.join(', ')
-				raise "Invalid log level %p: valid values are:\n   %s" % [ newlevel, levelnames ]
-			end
-		else
-			message "Log level is currently: %s" %
-				[ Treequel::LOG_LEVEL_NAMES[Treequel.logger.level] ]
-		end
-	end
-	set_options :log do |oparser, options|
-		oparser.banner = "log [LEVEL]"
-		oparser.separator 'Set the logging level, or display the current level if no level ' +
-		                  "is given. Valid log levels are: %s" %
-		                  Treequel::LOG_LEVEL_NAMES.keys.sort.join(', ')
-	end
-
-
-	### Display LDIF for the specified RDNs.
-	def cat_command( options, *args )
-		validate_rdns( *args )
-		args.each do |rdn|
-			extended = rdn.chomp!( '+' )
-
-			branch = @currbranch.get_child( rdn )
-			branch.include_operational_attrs = true if extended
-
-			if branch.exists?
-				ldifstring = branch.to_ldif( self.columns - 2 )
-				self.log.debug "LDIF: #{ldifstring.dump}"
-
-				message( format_ldif(ldifstring) )
-			else
-				error_message( "No such entry %s" % [branch.dn] )
-			end
-		end
-	end
-	set_options :cat do |oparser, options|
-		oparser.banner = "cat [RDN]+"
-		oparser.separator 'Display the entries specified by RDN as LDIF.'
-	end
-
-
-	### Display YAML for the specified RDNs.
-	def yaml_command( options, *args )
-		validate_rdns( *args )
-		args.each do |rdn|
-			branch = @currbranch.get_child( rdn )
-			message( branch_as_yaml(branch) )
-		end
-	end
-	set_options :yaml do |oparser, options|
-		oparser.banner = "yaml [RDN]+"
-		oparser.separator 'Display the entries specified by RDN as YAML.'
-	end
-
-
-	### List the children of the branch specified by the given +rdn+, or the current branch if none
-	### are specified.
-	def ls_command( options, *args )
-		targets = []
-
-		# No argument, just use the current branch
-		if args.empty?
-			targets << @currbranch
-
-		# Otherwise, list each one specified
-		else
-			validate_rdns( *args )
-			args.each do |rdn|
-				if branch = @currbranch.get_child( rdn )
-					targets << branch
-				else
-					error_message( "cannot access #{rdn}: no such entry" )
-				end
-			end
-		end
-
-		# Fetch each branch's children, sort them, format them in columns, and highlight them
-		targets.each do |branch|
-			header( branch.dn ) if targets.length > 1
-			if options.longform
-				message self.make_longform_ls_output( branch, options )
-			else
-				message self.make_shortform_ls_output( branch, options )
-			end
-			message if targets.length > 1
-		end
-	end
-	set_options :ls do |oparser, options|
-		oparser.banner = "ls [OPTIONS] [DN]+"
-		oparser.separator 'List the entries specified, or the current entry if none are specified.'
-		oparser.separator ''
-
-		oparser.on( "-l", "--long", FalseClass, "List in long format." ) do 
-			options.longform = true
-		end
-		oparser.on( "-t", "--timesort", FalseClass,
-		            "Sort by time modified (most recently modified first)." ) do 
-			options.timesort = true
-		end
-		oparser.on( "-d", "--dirsort", FalseClass,
-		            "Sort entries with subordinate entries before those without." ) do 
-			options.dirsort = true
-		end
-		oparser.on( "-r", "--reverse", FalseClass, "Reverse the entry sort functions." ) do 
-			options.reversesort = true
-		end
-
-	end
-
-
-	### Change the current working DN to +rdn+.
-	def cdn_command( options, rdn=nil, *args )
-		if rdn.nil?
-			@currbranch = @dir.base
-			return
-		end
-
-		return self.parent_command( options ) if rdn == '..'
-
-		validate_rdns( rdn )
-
-		pairs = rdn.split( /\s*,\s*/ )
-		pairs.each do |dnpair|
-			self.log.debug "  cd to %p" % [ dnpair ]
-			attribute, value = dnpair.split( /=/, 2 )
-			self.log.debug "  changing to %s( %p )" % [ attribute.downcase, value ]
-			@currbranch = @currbranch.send( attribute.downcase, value )
-		end
-	end
-	set_options :cdn do |oparser, options|
-		oparser.banner = "cdn <RDN>"
-		oparser.separator 'Change the current entry to <RDN>.'
-	end
-
-
-	### Change the current working DN to the current entry's parent.
-	def parent_command( options, *args )
-		parent = @currbranch.parent or raise "%s is the root DN" % [ @currbranch.dn ]
-
-		self.log.debug "  changing to %s" % [ parent.dn ]
-		@currbranch = parent
-	end
-	set_options :parent do |oparser, options|
-		oparser.banner = "parent"
-		oparser.separator "Change to the current entry's parent."
-	end
-
-
-	# ### Create the entry specified by +rdn+.
-	def create_command( options, rdn )
-		validate_rdns( rdn )
-		branch = @currbranch.get_child( rdn )
-
-		raise "#{branch.dn}: already exists." if branch.exists?
-		create_new_entry( branch )
-	end
-	set_options :create do |oparser, options|
-		oparser.banner = "create <RDN>"
-		oparser.separator "Create a new entry at <RDN>."
-	end
-
-
-	### Edit the entry specified by +rdn+.
-	def edit_command( options, rdn )
-		validate_rdns( rdn )
-		branch = @currbranch.get_child( rdn )
-
-		raise "#{branch.dn}: no such entry. Did you mean to 'create' it instead? " unless
-		 	branch.exists?
-
-		if entryhash = edit_in_yaml( branch )
-			branch.merge( entryhash )
-		end
-
-		message "Saved #{rdn}."
-	end
-	set_options :edit do |oparser, options|
-		oparser.banner = "edit <RDN>"
-		oparser.separator "Edit the entry at RDN as YAML."
-	end
-
-
-	### Change the DN of an entry
-	def mv_command( options, rdn, newdn )
-		validate_rdns( rdn, newdn )
-		branch = @currbranch.get_child( rdn )
-
-		raise "#{branch.dn}: no such entry" unless branch.exists?
-		olddn = branch.dn
-		branch.move( newdn )
-		message "  %s -> %s: success" % [ olddn, branch.dn ]
-	end
-	set_options :mv do |oparser, options|
-		oparser.banner = "mv <RDN> <NEWRDN>"
-		oparser.separator "Move the entry at RDN to NEWRDN"
-	end
-
-
-	### Copy an entry
-	def cp_command( options, rdn, newrdn )
-		# Can't validate as RDNs because they might be full DNs
-
-		base_dn = @currbranch.directory.base_dn
-
-		# If the RDN includes the base, it's a DN
-		branch = if rdn =~ /,#{base_dn}$/i
-				Treequel::Branch.new( @currbranch.directory, rdn )
-			else
-				@currbranch.get_child( rdn )
-			end
-
-		# The source should already exist
-		raise "#{branch.dn}: no such entry" unless branch.exists?
-
-		# Same for the other RDN...
-		newbranch = if newrdn =~ /,#{base_dn}$/i
-				Treequel::Branch.new( @currbranch.directory, newrdn )
-			else
-				@currbranch.get_child( newrdn )
-			end
-
-		# But it *shouldn't* exist already
-		raise "#{newbranch.dn}: already exists" if newbranch.exists?
-
-		attributes = branch.entry.merge( :dn => newbranch.dn )
-		newbranch.create( attributes )
-
-		message "  %s -> %s: success" % [ rdn, branch.dn ]
-	end
-	set_options :cp do |oparser, options|
-		oparser.banner = "cp <RDN> <NEWRDN>"
-		oparser.separator "Copy the entry at RDN to a new entry at NEWRDN"
-	end
-
-
-	### Remove the entry specified by +rdn+.
-	def rm_command( options, *rdns )
-		validate_rdns( *rdns )
-		branchsets = self.convert_to_branchsets( *rdns )
-		coll = Treequel::BranchCollection.new( *branchsets )
-
-		branches = coll.all
-
-		msg = "About to delete the following entries:\n" +
-			columnize( branches.collect {|br| br.dn } )
-
-		if options.force
-			branches.each do |br|
-				br.directory.delete( br )
-				message "  delete %s: success" % [ br.dn ]
-			end
-		else
-			ask_for_confirmation( msg ) do
-				branches.each do |br|
-					br.directory.delete( br )
-					message "  delete %s: success" % [ br.dn ]
-				end
-			end
-		end
-	end
-	set_options :rm do |oparser, options|
-		oparser.banner = "rm <RDN>+"
-		oparser.separator 'Remove the entries at the given RDNs.'
-
-		oparser.on( '-f', '--force', TrueClass, "Force -- remove without confirmation." ) do
-			options.force = true
-		end
-	end
-
-
-	### Find entries that match the given filter_clauses.
-	def grep_command( options, *filter_clauses )
-		branchset = filter_clauses.inject( @currbranch ) do |branch, clause|
-			branch.filter( clause )
-		end
-
-		message "Searching for entries that match '#{branchset.to_s}'"
-
-		entries = branchset.all
-		output = columnize( entries ).gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
-			format_rdn( rdn )
-		end
-		message( output )
-	end
-	set_options :grep do |oparser, options|
-		oparser.banner = "grep [OPTIONS] <FILTER>"
-		oparser.separator 'Search for children of the current entry that match the given FILTER'
-
-		oparser.on( '-r', '--recursive', TrueClass, "Search recursively." ) do
-			options.force = true
-		end
-	end
-
-
-	### Show who the shell is currently bound as.
-	def whoami_command( options, *args )
-		if user = @dir.bound_user
-			message "Bound as #{user}"
-		else
-			message "Bound anonymously"
-		end
-	end
-	set_options :whoami do |oparser, options|
-		oparser.banner = "whoami"
-		oparser.separator 'Display the DN of the user the shell is bound as.'
-	end
-
-
-	### Bind as a user.
-	def bind_command( options, *args )
-		binddn = (args.first || prompt( "Bind DN/UID" )) or
-			raise "Cancelled."
-		password = prompt_for_password()
-
-		# Try to turn a non-DN into a DN
-		user = nil
-		if binddn.index( '=' )
-			user = Treequel::Branch.new( @dir, binddn )
-		else
-			user = @dir.filter( :uid => binddn ).first
-		end
-
-		@dir.bind( user, password )
-		message "Bound as #{user}"
-	end
-	set_options :bind do |oparser, options|
-		oparser.banner = "bind [BIND_DN or UID]"
-		oparser.separator "Bind as BIND_DN or UID"
-		oparser.separator "If you don't specify a BIND_DN, you will be prompted for it."
-	end
-
-
-	### Start an IRB session on either the current branchset, if invoked with no arguments, or
-	### on a branchset for the specified +rdn+ if one is given.
-	def irb_command( options, *args )
-		branch = nil
-		if args.empty?
-			branch = @currbranch
-		else
-			rdn = args.first
-			validate_rdns( rdn )
-			branch = @currbranch.get_child( rdn )
-		end
-
-		self.log.debug "Setting up IRb shell"
-		IRB.start_session( branch )
-	end
-	set_options :irb do |oparser, options|
-		oparser.banner = "irb [RDN]"
-		oparser.separator "Start an IRb shell with either the current branch (if none is " +
-		 	"specified) or a branch for the entry specified by the given RDN."
-	end
-
-
-	### Handle a command from the user that doesn't exist.
-	def handle_missing_cmd( *args )
-		command = args.shift || '(testing?)'
-		message "Unknown command %p" % [ command ]
-		message "Known commands: ", '  ' + @commands.join(', ')
-	end
-
-
-	### Find methods that implement commands and return them in a sorted Array.
-	def find_commands
-		return self.methods.
-			collect {|mname| mname.to_s }.
-			grep( /^(\w+)_command$/ ).
-			collect {|mname| mname[/^(\w+)_command$/, 1] }.
-			sort
-	end
-
-
-	#################################################################
-	###	U T I L I T Y   M E T H O D S
-	#################################################################
-
-	### Convert the given +patterns+ to branchsets relative to the current branch and return
-	### them. This is used to map shell arguments like 'cn=*', 'Hosts', 'cn=dav*' into
-	### branchsets that will find matching entries.
-	def convert_to_branchsets( *patterns )
-		self.log.debug "Turning %d patterns into branchsets." % [ patterns.length ]
-		return patterns.collect do |pat|
-			key, val = pat.split( /\s*=\s*/, 2 )
-			self.log.debug "  making a filter out of %p => %p" % [ key, val ]
-			@currbranch.filter( key => val )
-		end
-	end
-
-
-	### Generate long-form output lines for the 'ls' command for the given +branch+.
-	def make_longform_ls_output( branch, options )
-		children = branch.children
-		totalmsg = "total %d" % [ children.length ]
-
-		# Calcuate column widths
-		oclen = children.map do |subbranch|
-			subbranch.include_operational_attrs = true
-			subbranch[:structuralObjectClass] ? subbranch[:structuralObjectClass].length : 0
-		end.max
-
-		# Set up sorting by collecting all the requested sort criteria as Proc objects which
-		# will be applied
-		sortfuncs = []
-		sortfuncs << lambda {|subbranch| subbranch[:hasSubordinates] ? 0 : 1 } if options.dirsort
-		sortfuncs << lambda {|subbranch| subbranch[:modifyTimestamp] } if options.timesort
- 		sortfuncs << lambda {|subbranch| subbranch.rdn.downcase }
-
-		rows = children.
-			sort_by {|subbranch| sortfuncs.collect {|func| func.call(subbranch) } }.
-			collect {|subbranch| self.format_description(subbranch, oclen) }
-
-		return [ totalmsg ] + (options.reversesort ? rows.reverse : rows)
-	end
-
-
-	### Generate short-form 'ls' output for the given +branch+ and return it.
-	def make_shortform_ls_output( branch, options )
-		branch.include_operational_attrs = true
-		entries = branch.children.
-			collect {|b| b.rdn + (b[:hasSubordinates] ? '/' : '') }.
-			sort_by {|rdn| rdn.downcase }
-		self.log.debug "Displaying %d entries in short form." % [ entries.length ]
-
-		return columnize( entries ).gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
-			format_rdn( rdn )
-		end
-	end
-
-
-	### Return the description of the specified +branch+ suitable for displaying in
-	### the directory listing. The +oclen+ is the width of the objectclass column.
-	def format_description( branch, oclen=40 )
-		rdn = format_rdn( branch.rdn )
-		metadatalen = oclen + 16 + 6 # oc + timestamp + whitespace
-		maxdesclen = self.columns - metadatalen - rdn.length - 5
-
-		modtime = branch[:modifyTimestamp] || branch[:createTimestamp]
-		return "%#{oclen}s  %s  %s%s %s" % [
-			branch[:structuralObjectClass] || '',
-			modtime.strftime('%Y-%m-%d %H:%M'),
-			rdn,
-			branch[:hasSubordinates] ? '/' : '',
-			single_line_description( branch, maxdesclen )
-		]
-	end
-
-
-	### Generate a single-line description from the specified +branch+
-	def single_line_description( branch, maxlen=80 )
-		return '' unless branch[:description] && branch[:description].first
-		desc = branch[:description].join('; ').gsub( /\n+/, '' )
-		desc[ maxlen..desc.length ] = '...' if desc.length > maxlen
-		return '(' + desc + ')'
-	end
-
-
-	### Create a new entry in the directory for the specified +branch+.
-	def create_new_entry( branch )
-		raise "#{branch.dn} already exists." if branch.exists?
-
-		# Prompt for the list of included objectClasses and build the appropriate
-		# blank entry with them in mind.
-		completions = branch.directory.schema.object_classes.keys.collect {|oid| oid.to_s }
-		self.log.debug "Prompting for new entry object classes with %d completions." %
-			[ completions.length ]
-		object_classes = prompt_for_multiple_values( "Entry objectClasses:", nil, completions ).
-			collect {|arg| arg.strip }.compact
-		self.log.debug "  user wants %d objectclasses: %p" % [ object_classes.length, object_classes ]
-
-		# Edit the entry
-		if newhash = edit_in_yaml( branch, object_classes )
-			branch.create( newhash )
-			message "Saved #{branch.dn}."
-		else
-			error_message "#{branch.dn} not saved."
-		end
-	end
-
-
-	### 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, object_classes=[] )
-		yaml = branch_as_yaml( object, false, object_classes )
-		filename = Digest::SHA1.hexdigest( yaml )
-		tempfile = Tempfile.new( filename )
-
-		self.log.debug "Object as YAML is: %p" % [ yaml ]
-		tempfile.print( yaml )
-		tempfile.close
-
-		new_yaml = edit( tempfile.path )
-
-		if new_yaml == yaml
-			message "Unchanged."
-			return nil
-		else
-			return YAML.load( new_yaml )
-		end
-	end
-
-
-	### Return the specified Treequel::Branch object as YAML. If +include_operational+ is true,
-	### include the entry's operational attributes. If +extra_objectclasses+ contains
-	### one or more objectClass OIDs, include their MUST and MAY attributes when building the 
-	### YAML representation of the branch.
-	def branch_as_yaml( object, include_operational=false, extra_objectclasses=[] )
-		object.include_operational_attrs = include_operational
-
-		# Make sure the displayed entry has the MUST attributes
-		entryhash = stringify_keys( object.must_attributes_hash(*extra_objectclasses) )
-		entryhash.merge!( object.entry || {} )
-		entryhash.merge!( object.rdn_attributes )
-		entryhash['objectClass'] ||= []
-		entryhash['objectClass'] |= extra_objectclasses
-
-		entryhash.delete( 'dn' ) # Special attribute, can't be edited
-
-		yaml = entryhash.to_yaml
-		yaml[ 5, 0 ] = "# #{object.dn}\n"
-
-		# Make comments out of MAY attributes that are unset
-		mayhash = stringify_keys( object.may_attributes_hash(*extra_objectclasses) )
-		self.log.debug "MAY hash is: %p" % [ mayhash ]
-		mayhash.delete_if {|attrname,val| entryhash.key?(attrname) }
-		yaml << mayhash.to_yaml[5..-1].gsub( /\n\n/, "\n" ).gsub( /^/, '# ' )
-
-		return yaml
-	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
-
-
-	### Output a header containing the given +text+.
-	def header( text )
-		header = colorize( text, :underscore, :cyan )
-		$stderr.puts( header )
-	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, completions=[] )
-		old_completion_proc = nil
-
-		message( MULTILINE_PROMPT % [label] )
-		if default
-			message "Enter a single blank line to keep the default:\n  %p" % [ default ]
-		end
-
-		results = []
-		result = nil
-
-		if !completions.empty?
-			self.log.debug "Prompting with %d completions." % [ completions.length ]
-			old_completion_proc = Readline.completion_proc
-			Readline.completion_proc = Proc.new do |input|
-				completions.flatten.grep( /^#{Regexp.quote(input)}/i ).sort
-			end
-		end
-
-		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
-	ensure
-		Readline.completion_proc = old_completion_proc if old_completion_proc
-	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
-
-
-	### 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
-
-
-	### Raise a RuntimeError if the specified +rdn+ is invalid.
-	def validate_rdns( *rdns )
-		rdns.flatten.each do |rdn|
-			raise "invalid RDN %p" % [ rdn ] unless RELATIVE_DISTINGUISHED_NAME.match( rdn )
-		end
-	end
-
-
-	### Return an ANSI-colored version of the given +rdn+ string.
-	def format_rdn( rdn )
-		rdn.split( /,/ ).collect do |rdn_part|
-			key, val = rdn_part.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 )
-		self.log.debug "Formatting LDIF: %p" % [ ldif ]
-		return ldif.gsub( LDIF_ATTRVAL_SPEC ) do
-			key, val = $1, $2.strip
-			self.log.debug "  formatting attribute: [ %p, %p ], remainder: %p" %
-				[ key, val, $POSTMATCH ]
-
-			case val
-
-			# Base64-encoded value
-			when /^:/
-				val = val[1..-1].strip
-				key +
-					colorize( :dark, :green ) { ':: ' } +
-					colorize( :green ) { val } + "\n"
-
-			# URL
-			when /^</
-				val = val[1..-1].strip
-				key +
-					colorize( :dark, :yellow ) { ':< ' } +
-					colorize( :yellow ) { val } + "\n"
-
-			# Regular attribute
-			else
-				key +
-					colorize( :dark, :white ) { ': ' } +
-					colorize( :bold, :white ) { val } + "\n"
-			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 Treequel::Shell
-
-
-Treequel::Shell.run( ARGV.dup )
-

File bin/treewhat

-#!/usr/bin/env ruby
-
-require 'yaml'
-require 'abbrev'
-require 'trollop'
-require 'highline'
-require 'shellwords'
-
-# Work around MacOS X's vendored 'sysexits' that does the same thing,
-# but with a different API
-gem 'sysexits'
-require 'sysexits'
-
-require 'treequel'
-require 'treequel/mixins'
-require 'treequel/constants'
-
-
-# A tool for displaying information about a directory's records and schema artifacts.
-class Treequel::What
-	extend Sysexits
-	include Sysexits,
-	        Treequel::Loggable,
-	        Treequel::ANSIColorUtilities,
-	        Treequel::Constants::Patterns,
-	        Treequel::HashUtilities
-
-	COLOR_SCHEME = HighLine::ColorScheme.new do |scheme|
-		scheme[:header]	   = [ :bold, :yellow ]
-		scheme[:subheader] = [ :bold, :white ]
-		scheme[:key]	   = [ :white ]
-		scheme[:value]	   = [ :bold, :white ]
-		scheme[:error]	   = [ :red ]
-		scheme[:warning]   = [ :yellow ]
-		scheme[:message]   = [ :reset ]
-	end
-
-
-	### Run the utility with the given +args+.
-	def self::run( args )
-		HighLine.color_scheme = COLOR_SCHEME
-
-		oparser = self.make_option_parser
-		opts = Trollop.with_standard_exception_handling( oparser ) do
-			oparser.parse( args )
-		end
-
-		pattern = oparser.leftovers.join( ' ' ) if oparser.leftovers
-
-		self.new( opts ).run( pattern )
-		exit :ok
-
-	rescue => err
-		Treequel.logger.fatal "Oops: %s: %s" % [ err.class.name, err.message ]
-		Treequel.logger.debug { '  ' + err.backtrace.join("\n  ") }
-
-		exit :software_error
-	end
-
-
-	### Create and configure a command-line option parser (a Trollop::Parser) for the command.
-	def self::make_option_parser
-		progname = File.basename( $0 )
-		default_directory = Treequel.directory_from_config
-		loglevels = Treequel::LOG_LEVELS.
-			sort_by {|name,lvl| lvl }.
-			collect {|name,lvl| name.to_s }.
-			join( ', ' )
-
-		return Trollop::Parser.new do
-			banner "Usage: #{progname} [OPTIONS] [PATTERN]"
-
-			text ''
-			text %{Search for an object in an LDAP directory that matches PATTERN and } +
-			     %{display some information about it.}
-			text ''
-			text %{The PATTERN can be the DN (or RDN relative to the base) of an entry, } +
-			     %{a search filter, or the name of an artifact in the directory's schema, } +
-			     %{such as an objectClass, matching rule, syntax, etc.}
-			text ''
-			text %{If no PATTERN is specified, general information about the directory is } +
-			     %{output instead.}
-			text ''
-
-			text 'Connection Options:'
-			opt :ldapurl, "Specify the directory to connect to.",
-				:default => default_directory.uri.to_s
-			text ''
-
-			text 'Display Options:'
-			opt :attrtypes, "Show attribute types for objects that have them."
-			opt :objectclasses, "Show objectclasses for objects that have them."
-			opt :syntaxes, "Show syntaxes for objects that have them."
-			opt :matching_rules, "Show matching rules for objects that have them."
-			opt :matching_rule_uses, "Show matching rule uses for objects that have them."
-			opt :all, "Show any of the above that are applicable."
-			text ''
-
-			text 'Other Options:'
-			opt :debug, "Turn debugging on. Also sets the --loglevel to 'debug'."
-			opt :loglevel, "Set the logging level. Must be one of: #{loglevels}",
-				:default => Treequel::LOG_LEVEL_NAMES[ Treequel.logger.level ]
-			opt :binddn, "The DN of the user to bind as. Defaults to anonymous binding.",
-				:type => :string
-		end
-	end
-
-
-	#################################################################
-	###	I N S T A N C E   M E T H O D S
-	#################################################################
-
-	### Create a new instance of the command and set it up with the given
-	### +options+.
-	def initialize( options )
-		Treequel.logger.formatter = Treequel::ColorLogFormatter.new( Treequel.logger )
-
-		if options.debug
-			$DEBUG = true
-			$VERBOSE = true
-			Treequel.logger.level = Logger::DEBUG
-		elsif options.loglevel
-			Treequel.logger.level = Treequel::LOG_LEVELS[ options.loglevel ]
-		end
-
-		@options   = options
-		if @options.all?
-			@options[:attrtypes] =
-				@options[:objectclasses] =
-				@options[:syntaxes] =
-				@options[:matching_rules] =
-				@options[:matching_rule_uses] =
-				true
-		end
-
-		@directory = Treequel.directory( options.ldapurl )
-		@prompt    = HighLine.new
-
-		@prompt.wrap_at = @prompt.output_cols - 10
-
-		self.log.debug "Created new treewhat command object for %s" % [ @directory ]
-	end
-
-
-	######
-	public
-	######
-
-	# The LDAP directory the command will connect to
-	attr_reader :directory
-
-	# The Trollop options hash the command will read its configuration from
-	attr_reader :options
-
-	# The HighLine object to use for prompting and displaying stuff
-	attr_reader :prompt
-
-
-	### Display an +object+ highlighted as a header.
-	def print_header( object )
-		self.prompt.say( self.prompt.color(object.to_s, :header) )
-	end
-
-
-	### Run the command with the specified +pattern+.
-	def run( pattern=nil )
-		self.log.debug "Running with pattern = %p" % [ pattern ]
-
-		self.bind_to_directory if self.options.binddn
-
-		case pattern
-
-		# No argument
-		when NilClass, ''
-			self.show_directory_overview
-
-		# DN/RDN or filter if it contains a '='
-		when /=/
-			self.show_entry( pattern )
-
-		# Otherwise, try to find a schema item that matches
-		else
-			self.show_schema_artifact( pattern )
-		end
-
-	end
-
-
-	### Prompt for a password and then bind to the command's directory using the binddn in
-	### the options.
-	def bind_to_directory
-		binddn = self.options.binddn or
-			raise ArgumentError, "no binddn in the options hash?!"
-		self.log.debug "Attempting to bind to the directory as %s" % [ binddn ]
-
-		pass = self.prompt.ask( "password: " ) {|q| q.echo = '*' }
-		user = Treequel::Branch.new( self.directory, binddn )
-
-		self.directory.bind_as( user, pass )
-		self.log.debug "  bound as %s" % [ user ]
-
-		return true
-	end
-
-
-	### Show general information about the directory if the user doesn't give a pattern on
-	### the command line.
-	def show_directory_overview
-		pr  = self.prompt
-		dir = self.directory
-
-		self.print_header( dir.uri.to_s )
-		pr.say( "\n" )
-		pr.say( dir.schema.to_s )
-
-		self.show_column_list( dir.schema.attribute_types.values, 'Attribute Types' ) if
-			self.options.attrtypes
-		self.show_column_list( dir.schema.object_classes.values, "Object Classes" ) if 
-			self.options.objectclasses
-		self.show_column_list( dir.schema.ldap_syntaxes.values, "Syntaxes" ) if 
-			self.options.syntaxes
-		self.show_column_list( dir.schema.matching_rules.values, "Matching Rules" ) if 
-			self.options.matching_rules
-		self.show_column_list( dir.schema.matching_rule_uses.values, "Matching Rule Uses" ) if 
-			self.options.matching_rule_uses
-	end
-
-
-	### Show the items from the given +enum+ under the specified +subheading+ in a columnized list.
-	def show_column_list( enum, subheading )
-		pr = self.prompt
-		items = nil
-
-		if enum.first.respond_to?( :name )
-			items = enum.map( &:name ).map( &:to_s ).uniq
-		else
-			items = enum.map( &:oid ).uniq
-		end
-
-		pr.say( "\n" )
-		pr.say( pr.color(subheading, :subheader) )
-		pr.say( pr.list(items.sort_by(&:downcase), :columns_down) )
-	end
-
-
-	#
-	# 'Show entry' mode
-	#
-
-	### Fetch an entry from the directory and display it like Treequel's editing mode.
-	def show_entry( pattern )
-		dir = self.directory
-		branch = Treequel::Branch.new( dir, pattern )
-
-		if !branch.exists?
-			branch = Treequel::Branch.new( dir, pattern + ',' + dir.base_dn )
-		end
-
-		if !branch.exists?
-			branch = dir.filter( pattern ).first
-		end
-
-		if !branch
-			self.prompt.say( self.prompt.color("No match.", :error) )
-		end
-
-		yaml = self.branch_as_yaml( branch )
-		self.prompt.say( yaml )
-	end
-
-
-	### Return the specified Treequel::Branch object as YAML. If +include_operational+ is true,
-	### include the entry's operational attributes. If +extra_objectclasses+ contains
-	### one or more objectClass OIDs, include their MUST and MAY attributes when building the 
-	### YAML representation of the branch.
-	def branch_as_yaml( object, include_operational=false )
-		object.include_operational_attrs = include_operational
-
-		# Make sure the displayed entry has the MUST attributes
-		entryhash = stringify_keys( object.must_attributes_hash )
-		entryhash.merge!( object.entry || {} )
-		entryhash['objectClass'] ||= []
-
-		entryhash.delete( 'dn' ) # Special attribute, can't be edited
-
-		yaml = entryhash.to_yaml
-		yaml[ 5, 0 ] = self.prompt.color( "# #{object.dn}\n", :header )
-
-		# Make comments out of MAY attributes that are unset
-		mayhash = stringify_keys( object.may_attributes_hash )
-		self.log.debug "MAY hash is: %p" % [ mayhash ]
-		mayhash.delete_if {|attrname,val| entryhash.key?(attrname) }
-		yaml << mayhash.to_yaml[5..-1].gsub( /\n\n/, "\n" ).gsub( /^/, '# ' )
-
-		return yaml
-	end
-
-
-
-
-	#
-	# 'Show schema artifact' mode
-	#
-
-	SCHEMA_ARTIFACT_TYPES = [
-		:object_classes,
-		:attribute_types,
-		:ldap_syntaxes,
-		:matching_rules,
-		:matching_rule_uses,
-	  ]
-
-	### Find an artifact in the directory's schema that matches +pattern+, and display it
-	### if it exists.
-	def show_schema_artifact( pattern )
-		pr        = self.prompt
-		schema    = self.directory.schema
-		artifacts = SCHEMA_ARTIFACT_TYPES.
-			collect {|type| schema.send( type ).values.uniq }.flatten
-
-		if match = find_exact_matching_artifact( artifacts, pattern )
-			self.display_schema_artifact( match )
-		elsif match = find_substring_matching_artifact( artifacts, pattern )
-			pr.say( "No exact match. Falling back to substring match:" )
-			self.display_schema_artifact( match )
-		else
-			pr.say( pr.color("No match.", :error) )
-		end
-	end
-
-
-	### Display a schema artifact in a readable way.
-	def display_schema_artifact( artifact )
-		self.prompt.say( self.prompt.color(artifact.class.name.sub(/.*::/, ''), :header) + ' ' )
-		self.prompt.say( self.prompt.color(artifact.to_s, :subheader) )
-
-		# Display some other stuff depending on what kind of thing it is
-		case artifact
-		when Treequel::Schema::AttributeType
-			self.display_attrtype_details( artifact )
-		end
-	end
-
-
-	### Display additional details for the specified +attrtype+ (a Treequel::Schema::AttributeType).
-	def display_attrtype_details( attrtype )
-		ocs = self.directory.schema.object_classes.values.find_all do |oc|
-			( oc.must_oids | oc.may_oids ).include?( attrtype.name.to_sym )
-		end
-
-		if ocs.empty?
-			self.prompt.say "No objectClasses with the '%s' attribute are in the current schema." %
-				[ attrtype.name ]
-		else
-			ocnames = ocs.uniq.map( &:name ).map( &:to_s ).sort
-
-			self.prompt.say "objectClasses with the '%s' attribute in the current schema:" %
-				[ attrtype.name ]
-			self.prompt.say( self.prompt.list(ocnames, :columns_across) )
-		end
-	end
-
-
-	### Try to find an artifact in +artifacts+ whose name or oid matches +pattern+ exactly.
-	### Returns the first matching artifact.
-	def find_exact_matching_artifact( artifacts, pattern )
-		self.log.debug "Trying to find an exact match for %p in %d artifacts." %
-			[ pattern, artifacts.length ]
-		return artifacts.find do |obj|
-			(obj.respond_to?( :names ) && obj.names.map(&:to_s).include?(pattern) ) ||
-			(obj.respond_to?( :name )  && obj.name.to_s == pattern ) ||
-			(obj.respond_to?( :oid )   && obj.oid == pattern )
-		end
-	end
-
-
-	### Try to find an artifact in +artifacts+ whose name or oid contains +pattern+.
-	### Returns the first matching artifact.
-	def find_substring_matching_artifact( artifacts, pattern )
-		pattern = Regexp.new( Regexp.escape(pattern), Regexp::IGNORECASE )
-
-		return artifacts.find do |obj|
-			(obj.respond_to?( :names ) && obj.names.find {|name| name.to_s =~ pattern} ) ||
-			(obj.respond_to?( :name )  && obj.name.to_s =~ pattern ) ||
-			(obj.respond_to?( :oid )   && obj.oid =~ pattern )
-		end
-	end
-
-
-end # class Treequel::What
-
-
-Treequel::What.run( ARGV.dup )
-

File lib/treequel/branch.rb

 	###	I N S T A N C E   M E T H O D S
 	#################################################################
 
-	### Create a new Treequel::Branch with the given +directory+, +dn+, and an optional +entry+. 
-	### If the optional +entry+ object is given, it will be used to fetch values from the 
+	### Create a new Treequel::Branch with the given +directory+, +dn+, and an optional +entry+.
+	### If the optional +entry+ object is given, it will be used to fetch values from the
 	### directory; if it isn't provided, it will be fetched from the +directory+ the first
 	### time it is needed.
 	def initialize( directory, dn, entry=nil )