utils.rb
changeset 6 66beb495a861
equal deleted inserted replaced
5:804e1c2b9a40 6:66beb495a861
       
     1 #
       
     2 #	Install/distribution utility functions
       
     3 #	$Id$
       
     4 #
       
     5 #	Copyright (c) 2001-2008, The FaerieMUD Consortium.
       
     6 #
       
     7 #   All rights reserved.
       
     8 #   
       
     9 #   Redistribution and use in source and binary forms, with or without modification, are
       
    10 #   permitted provided that the following conditions are met:
       
    11 #   
       
    12 #       * Redistributions of source code must retain the above copyright notice, this
       
    13 #         list of conditions and the following disclaimer.
       
    14 #   
       
    15 #       * Redistributions in binary form must reproduce the above copyright notice, this
       
    16 #         list of conditions and the following disclaimer in the documentation and/or
       
    17 #         other materials provided with the distribution.
       
    18 #   
       
    19 #       * Neither the name of LAIKA, nor the names of its contributors may be used to
       
    20 #         endorse or promote products derived from this software without specific prior
       
    21 #         written permission.
       
    22 #   
       
    23 #   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
       
    24 #   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
       
    25 #   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
       
    26 #   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
       
    27 #   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
       
    28 #   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
       
    29 #   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
       
    30 #   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
       
    31 #   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
       
    32 #   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
       
    33 #   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    34 # 
       
    35 
       
    36 BEGIN {
       
    37 	require 'rbconfig'
       
    38 	require 'uri'
       
    39 	require 'find'
       
    40 	require 'pp'
       
    41 	require 'irb'
       
    42 
       
    43 	begin
       
    44 		require 'readline'
       
    45 		include Readline
       
    46 	rescue LoadError => e
       
    47 		$stderr.puts "Faking readline..."
       
    48 		def readline( prompt )
       
    49 			$stderr.print prompt.chomp
       
    50 			return $stdin.gets.chomp
       
    51 		end
       
    52 	end
       
    53 
       
    54 }
       
    55 
       
    56 
       
    57 ### Command-line utility functions
       
    58 module UtilityFunctions
       
    59 	include Config
       
    60 
       
    61 	# The list of regexen that eliminate files from the MANIFEST
       
    62 	ANTIMANIFEST = [
       
    63 		/makedist\.rb/,
       
    64 		/\bCVS\b/,
       
    65 		/~$/,
       
    66 		/^#/,
       
    67 		%r{docs/html},
       
    68 		%r{docs/man},
       
    69 		/\bTEMPLATE\.\w+\.tpl\b/,
       
    70 		/\.cvsignore/,
       
    71 		/\.s?o$/,
       
    72 	]
       
    73 
       
    74 	# Set some ANSI escape code constants (Shamelessly stolen from Perl's
       
    75 	# Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
       
    76 	AnsiAttributes = {
       
    77 		'clear'      => 0,
       
    78 		'reset'      => 0,
       
    79 		'bold'       => 1,
       
    80 		'dark'       => 2,
       
    81 		'underline'  => 4,
       
    82 		'underscore' => 4,
       
    83 		'blink'      => 5,
       
    84 		'reverse'    => 7,
       
    85 		'concealed'  => 8,
       
    86 
       
    87 		'black'      => 30,   'on_black'   => 40, 
       
    88 		'red'        => 31,   'on_red'     => 41, 
       
    89 		'green'      => 32,   'on_green'   => 42, 
       
    90 		'yellow'     => 33,   'on_yellow'  => 43, 
       
    91 		'blue'       => 34,   'on_blue'    => 44, 
       
    92 		'magenta'    => 35,   'on_magenta' => 45, 
       
    93 		'cyan'       => 36,   'on_cyan'    => 46, 
       
    94 		'white'      => 37,   'on_white'   => 47
       
    95 	}
       
    96 
       
    97 	ErasePreviousLine = "\033[A\033[K"
       
    98 
       
    99 	ManifestHeader = (<<-"EOF").gsub( /^\t+/, '' )
       
   100 		#
       
   101 		# Distribution Manifest
       
   102 		# Created: #{Time::now.to_s}
       
   103 		# 
       
   104 
       
   105 	EOF
       
   106 
       
   107 	###############
       
   108 	module_function
       
   109 	###############
       
   110 
       
   111 	# Create a string that contains the ANSI codes specified and return it
       
   112 	def ansi_code( *attributes )
       
   113 		attributes.flatten!
       
   114 		# $stderr.puts "Returning ansicode for TERM = %p: %p" %
       
   115 		# 	[ ENV['TERM'], attributes ]
       
   116 		return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
       
   117 		attributes = AnsiAttributes.values_at( *attributes ).compact.join(';')
       
   118 
       
   119 		# $stderr.puts "  attr is: %p" % [attributes]
       
   120 		if attributes.empty? 
       
   121 			return ''
       
   122 		else
       
   123 			return "\e[%sm" % attributes
       
   124 		end
       
   125 	end
       
   126 
       
   127 
       
   128 	### Colorize the given +string+ with the specified +attributes+ and return it, handling line-endings, etc.
       
   129 	def colorize( string, *attributes )
       
   130 		ending = string[/(\s)$/] || ''
       
   131 		string = string.rstrip
       
   132 		return ansi_code( attributes.flatten ) + string + ansi_code( 'reset' ) + ending
       
   133 	end
       
   134 
       
   135 
       
   136 	# Test for the presence of the specified <tt>library</tt>, and output a
       
   137 	# message describing the test using <tt>nicename</tt>. If <tt>nicename</tt>
       
   138 	# is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default.
       
   139 	def test_for_library( library, nicename=nil, progress=false )
       
   140 		nicename ||= library
       
   141 		message( "Testing for the #{nicename} library..." ) if progress
       
   142 		if $LOAD_PATH.detect {|dir|
       
   143 				File.exists?(File.join(dir,"#{library}.rb")) ||
       
   144 				File.exists?(File.join(dir,"#{library}.#{CONFIG['DLEXT']}"))
       
   145 			}
       
   146 			message( "found.\n" ) if progress
       
   147 			return true
       
   148 		else
       
   149 			message( "not found.\n" ) if progress
       
   150 			return false
       
   151 		end
       
   152 	end
       
   153 
       
   154 	# Test for the presence of the specified <tt>library</tt>, and output a
       
   155 	# message describing the problem using <tt>nicename</tt>. If
       
   156 	# <tt>nicename</tt> is <tt>nil</tt>, the value in <tt>library</tt> is used
       
   157 	# to build a default. If <tt>raaUrl</tt> and/or <tt>downloadUrl</tt> are
       
   158 	# specified, they are also use to build a message describing how to find the
       
   159 	# required library. If <tt>fatal</tt> is <tt>true</tt>, a missing library
       
   160 	# will cause the program to abort.
       
   161 	def test_for_required_library( library, nicename=nil, raaUrl=nil, downloadUrl=nil, fatal=true )
       
   162 		nicename ||= library
       
   163 		unless test_for_library( library, nicename )
       
   164 			msgs = [ "You are missing the required #{nicename} library.\n" ]
       
   165 			msgs << "RAA: #{raaUrl}\n" if raaUrl
       
   166 			msgs << "Download: #{downloadUrl}\n" if downloadUrl
       
   167 			if fatal
       
   168 				abort msgs.join('')
       
   169 			else
       
   170 				error_message msgs.join('')
       
   171 			end
       
   172 		end
       
   173 		return true
       
   174 	end
       
   175 
       
   176 	### Output <tt>msg</tt> as a ANSI-colored program/section header (white on
       
   177 	### blue).
       
   178 	def header( msg )
       
   179 		msg.chomp!
       
   180 		$stderr.puts ansi_code( 'bold', 'white', 'on_blue' ) + msg + ansi_code( 'reset' )
       
   181 		$stderr.flush
       
   182 	end
       
   183 
       
   184 	### Output <tt>msg</tt> to STDERR and flush it.
       
   185 	def message( *msgs )
       
   186 		$stderr.print( msgs.join("\n") )
       
   187 		$stderr.flush
       
   188 	end
       
   189 
       
   190 	### Output +msg+ to STDERR and flush it if $VERBOSE is true.
       
   191 	def verbose_msg( msg )
       
   192 		msg.chomp!
       
   193 		message( msg + "\n" ) if $VERBOSE
       
   194 	end
       
   195 
       
   196 	### Output the specified <tt>msg</tt> as an ANSI-colored error message
       
   197 	### (white on red).
       
   198 	def error_msg( msg )
       
   199 		message ansi_code( 'bold', 'white', 'on_red' ) + msg + ansi_code( 'reset' )
       
   200 	end
       
   201 	alias :error_message :error_msg
       
   202 
       
   203 	### Output the specified <tt>msg</tt> as an ANSI-colored debugging message
       
   204 	### (yellow on blue).
       
   205 	def debug_msg( msg )
       
   206 		return unless $DEBUG
       
   207 		msg.chomp!
       
   208 		$stderr.puts ansi_code( 'yellow' ) + ">>> #{msg}" + ansi_code( 'reset' )
       
   209 		$stderr.flush
       
   210 	end
       
   211 
       
   212 	### Erase the previous line (if supported by your terminal) and output the
       
   213 	### specified <tt>msg</tt> instead.
       
   214 	def replace_msg( msg )
       
   215 		$stderr.puts
       
   216 		$stderr.print ErasePreviousLine
       
   217 		message( msg )
       
   218 	end
       
   219 	alias :replace_message :replace_msg
       
   220 
       
   221 	### Output a divider made up of <tt>length</tt> hyphen characters.
       
   222 	def divider( length=75 )
       
   223 		$stderr.puts "\r" + ("-" * length )
       
   224 	end
       
   225 	alias :writeLine :divider
       
   226 
       
   227 
       
   228 	### Output the specified <tt>msg</tt> colored in ANSI red and exit with a
       
   229 	### status of 1.
       
   230 	def abort( msg )
       
   231 		print ansi_code( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansi_code( 'reset' ) + "\n\n"
       
   232 		Kernel.exit!( 1 )
       
   233 	end
       
   234 
       
   235 
       
   236 	### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
       
   237 	### return the user's input with leading and trailing spaces removed.  If a
       
   238 	### test is provided, the prompt will repeat until the test returns true.
       
   239 	### An optional failure message can also be passed in.
       
   240 	def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
       
   241 		prompt_string.chomp!
       
   242 		prompt_string << ":" unless /\W$/.match( prompt_string )
       
   243 		response = nil
       
   244 
       
   245 		begin
       
   246 			response = readline( ansi_code('bold', 'green') +
       
   247 				"#{prompt_string} " + ansi_code('reset') ) || ''
       
   248 			response.strip!
       
   249 			if block_given? && ! yield( response ) 
       
   250 				error_message( failure_msg + "\n\n" )
       
   251 				response = nil
       
   252 			end
       
   253 		end until response
       
   254 
       
   255 		return response
       
   256 	end
       
   257 
       
   258 
       
   259 	### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
       
   260 	### substituting the given <tt>default</tt> if the user doesn't input
       
   261 	### anything.  If a test is provided, the prompt will repeat until the test
       
   262 	### returns true.  An optional failure message can also be passed in.
       
   263 	def prompt_with_default( prompt_string, default, failure_msg="Try again." )
       
   264 		response = nil
       
   265 
       
   266 		begin
       
   267 			response = prompt( "%s [%s]" % [ prompt_string, default ] )
       
   268 			response = default if response.empty?
       
   269 
       
   270 			if block_given? && ! yield( response ) 
       
   271 				error_message( failure_msg + "\n\n" )
       
   272 				response = nil
       
   273 			end
       
   274 		end until response
       
   275 
       
   276 		return response
       
   277 	end
       
   278 
       
   279 
       
   280 	$programs = {}
       
   281 
       
   282 	### Search for the program specified by the given <tt>progname</tt> in the
       
   283 	### user's <tt>PATH</tt>, and return the full path to it, or <tt>nil</tt> if
       
   284 	### no such program is in the path.
       
   285 	def find_program( progname )
       
   286 		unless $programs.key?( progname )
       
   287 			ENV['PATH'].split(File::PATH_SEPARATOR).each {|d|
       
   288 				file = File.join( d, progname )
       
   289 				if File.executable?( file )
       
   290 					$programs[ progname ] = file 
       
   291 					break
       
   292 				end
       
   293 			}
       
   294 		end
       
   295 
       
   296 		return $programs[ progname ]
       
   297 	end
       
   298 
       
   299 
       
   300 	### Search for the release version for the project in the specified
       
   301 	### +directory+.
       
   302 	def extract_version( directory='.' )
       
   303 		release = nil
       
   304 
       
   305 		Dir::chdir( directory ) do
       
   306 			if File::directory?( "CVS" )
       
   307 				verbose_msg( "Project is versioned via CVS. Searching for RELEASE_*_* tags..." )
       
   308 
       
   309 				if (( cvs = find_program('cvs') ))
       
   310 					revs = []
       
   311 					output = %x{cvs log}
       
   312 					output.scan( /RELEASE_(\d+(?:_\d\w+)*)/ ) {|match|
       
   313 						rev = $1.split(/_/).collect {|s| Integer(s) rescue 0}
       
   314 						verbose_msg( "Found %s...\n" % rev.join('.') )
       
   315 						revs << rev
       
   316 					}
       
   317 
       
   318 					release = revs.sort.last
       
   319 				end
       
   320 
       
   321 			elsif File::directory?( '.svn' )
       
   322 				verbose_msg( "Project is versioned via Subversion" )
       
   323 
       
   324 				if (( svn = find_program('svn') ))
       
   325 					output = %x{svn pg project-version}.chomp
       
   326 					unless output.empty?
       
   327 						verbose_msg( "Using 'project-version' property: %p" % output )
       
   328 						release = output.split( /[._]/ ).collect {|s| Integer(s) rescue 0}
       
   329 					end
       
   330 				end
       
   331 			end
       
   332 		end
       
   333 
       
   334 		return release
       
   335 	end
       
   336 
       
   337 
       
   338 	### Find the current release version for the project in the specified
       
   339 	### +directory+ and return its successor.
       
   340 	def extract_next_version( directory='.' )
       
   341 		version = extract_version( directory ) || [0,0,0]
       
   342 		version.compact!
       
   343 		version[-1] += 1
       
   344 
       
   345 		return version
       
   346 	end
       
   347 
       
   348 
       
   349 	# Pattern for extracting the name of the project from a Subversion URL
       
   350 	SVNUrlPath = %r{
       
   351 		.*/						# Skip all but the last bit
       
   352 		([^/]+)					# $1 = project name
       
   353 		/						# Followed by / +
       
   354 		(?:
       
   355 			trunk |				# 'trunk'
       
   356 			(
       
   357 				branches |		# ...or branches/branch-name
       
   358 				tags			# ...or tags/tag-name
       
   359 			)/\w	
       
   360 		)
       
   361 		$						# bound to the end
       
   362 	}ix
       
   363 
       
   364 	### Extract the project name (CVS Repository name) for the given +directory+.
       
   365 	def extract_project_name( directory='.' )
       
   366 		name = nil
       
   367 
       
   368 		Dir::chdir( directory ) do
       
   369 
       
   370 			# CVS-controlled
       
   371 			if File::directory?( "CVS" )
       
   372 				verbose_msg( "Project is versioned via CVS. Using repository name." )
       
   373 				name = File.open( "CVS/Repository", "r").readline.chomp
       
   374 				name.sub!( %r{.*/}, '' )
       
   375 
       
   376 			# Subversion-controlled
       
   377 			elsif File::directory?( '.svn' )
       
   378 				verbose_msg( "Project is versioned via Subversion" )
       
   379 
       
   380 				# If the machine has the svn tool, try to get the project name
       
   381 				if (( svn = find_program( 'svn' ) ))
       
   382 
       
   383 					# First try an explicit property
       
   384 					output = shell_command( svn, 'pg', 'project-name' )
       
   385 					if !output.empty?
       
   386 						verbose_msg( "Using 'project-name' property: %p" % output )
       
   387 						name = output.first.chomp
       
   388 
       
   389 					# If that doesn't work, try to figure it out from the URL
       
   390 					elsif (( uri = get_svn_uri() ))
       
   391 						name = uri.path.sub( SVNUrlPath ) { $1 }
       
   392 					end
       
   393 				end
       
   394 			end
       
   395 
       
   396 			# Fall back to guessing based on the directory name
       
   397 			unless name
       
   398 				name = File::basename(File::dirname( File::expand_path(__FILE__) ))
       
   399 			end
       
   400 		end
       
   401 
       
   402 		return name
       
   403 	end
       
   404 
       
   405 
       
   406 	### Extract the Subversion URL from the specified directory and return it as
       
   407 	### a URI object.
       
   408 	def get_svn_uri( directory='.' )
       
   409 		uri = nil
       
   410 
       
   411 		Dir::chdir( directory ) do
       
   412 			output = %x{svn info}
       
   413 			debug_msg( "Using info: %p" % output )
       
   414 
       
   415 			if /^URL: \s* ( .* )/xi.match( output )
       
   416 				uri = URI::parse( $1 )
       
   417 			end
       
   418 		end
       
   419 
       
   420 		return uri
       
   421 	end
       
   422 
       
   423 
       
   424 	### (Re)make a manifest file in the specified +path+.
       
   425 	def make_manifest( path="MANIFEST" )
       
   426 		if File::exists?( path )
       
   427 			reply = prompt_with_default( "Replace current '#{path}'? [yN]", "n" )
       
   428 			return false unless /^y/i.match( reply )
       
   429 
       
   430 			verbose_msg "Replacing manifest at '#{path}'"
       
   431 		else
       
   432 			verbose_msg "Creating new manifest at '#{path}'"
       
   433 		end
       
   434 
       
   435 		files = []
       
   436 		verbose_msg( "Finding files...\n" )
       
   437 		Find::find( Dir::pwd ) do |f|
       
   438 			Find::prune if File::directory?( f ) &&
       
   439 				/^\./.match( File::basename(f) )
       
   440 			verbose_msg( "  found: #{f}\n" )
       
   441 			files << f.sub( %r{^#{Dir::pwd}/?}, '' )
       
   442 		end
       
   443 		files = vet_manifest( files )
       
   444 
       
   445 		verbose_msg( "Writing new manifest to #{path}..." )
       
   446 		File::open( path, File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
       
   447 			ofh.puts( ManifestHeader )
       
   448 			ofh.puts( files )
       
   449 		end
       
   450 		verbose_msg( "done." )
       
   451 	end
       
   452 
       
   453 
       
   454 	### Read the specified <tt>manifestFile</tt>, which is a text file
       
   455 	### describing which files to package up for a distribution. The manifest
       
   456 	### should consist of one or more lines, each containing one filename or
       
   457 	### shell glob pattern.
       
   458 	def read_manifest( manifestFile="MANIFEST" )
       
   459 		verbose_msg "Building manifest..."
       
   460 		raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile
       
   461 
       
   462 		manifest = IO::readlines( manifestFile ).collect {|line|
       
   463 			line.chomp
       
   464 		}.select {|line|
       
   465 			line !~ /^(\s*(#.*)?)?$/
       
   466 		}
       
   467 
       
   468 		filelist = []
       
   469 		for pat in manifest
       
   470 			verbose_msg "Adding files that match '#{pat}' to the file list"
       
   471 			filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)}
       
   472 		end
       
   473 
       
   474 		verbose_msg "found #{filelist.length} files.\n"
       
   475 		return filelist
       
   476 	end
       
   477 
       
   478 
       
   479 	### Given a <tt>filelist</tt> like that returned by #read_manifest, remove
       
   480 	### the entries therein which match the Regexp objects in the given
       
   481 	### <tt>antimanifest</tt> and return the resultant Array.
       
   482 	def vet_manifest( filelist, antimanifest=ANTIMANIFEST )
       
   483 		origLength = filelist.length
       
   484 		verbose_msg "Vetting manifest..."
       
   485 
       
   486 		for regex in antimanifest
       
   487 			verbose_msg "\n\tPattern /#{regex.source}/ removed: " +
       
   488 				filelist.find_all {|file| regex.match(file)}.join(', ')
       
   489 			filelist.delete_if {|file| regex.match(file)}
       
   490 		end
       
   491 
       
   492 		verbose_msg "removed #{origLength - filelist.length} files from the list.\n"
       
   493 		return filelist
       
   494 	end
       
   495 
       
   496 
       
   497 	### Combine a call to #read_manifest with one to #vet_manifest.
       
   498 	def get_vetted_manifest( manifestFile="MANIFEST", antimanifest=ANTIMANIFEST )
       
   499 		vet_manifest( read_manifest(manifestFile), antimanifest )
       
   500 	end
       
   501 
       
   502 
       
   503 	### Given a documentation <tt>catalogFile</tt>, extract the title, if
       
   504 	### available, and return it. Otherwise generate a title from the name of
       
   505 	### the CVS module.
       
   506 	def find_rdoc_title( catalogFile="docs/CATALOG" )
       
   507 
       
   508 		# Try extracting it from the CATALOG file from a line that looks like:
       
   509 		# Title: Foo Bar Module
       
   510 		title = find_catalog_keyword( 'title', catalogFile )
       
   511 
       
   512 		# If that doesn't work for some reason, use the name of the project.
       
   513 		title = extract_project_name()
       
   514 
       
   515 		return title
       
   516 	end
       
   517 
       
   518 
       
   519 	### Given a documentation <tt>catalogFile</tt>, extract the name of the file
       
   520 	### to use as the initally displayed page. If extraction fails, the
       
   521 	### +default+ will be used if it exists. Returns +nil+ if there is no main
       
   522 	### file to be found.
       
   523 	def find_rdoc_main( catalogFile="docs/CATALOG", default="README" )
       
   524 
       
   525 		# Try extracting it from the CATALOG file from a line that looks like:
       
   526 		# Main: Foo Bar Module
       
   527 		main = find_catalog_keyword( 'main', catalogFile )
       
   528 
       
   529 		# Try to make some educated guesses if that doesn't work
       
   530 		if main.nil?
       
   531 			basedir = File::dirname( __FILE__ )
       
   532 			basedir = File::dirname( basedir ) if /docs$/ =~ basedir
       
   533 
       
   534 			if File::exists?( File::join(basedir, default) )
       
   535 				main = default
       
   536 			end
       
   537 		end
       
   538 
       
   539 		return main
       
   540 	end
       
   541 
       
   542 
       
   543 	### Given a documentation <tt>catalogFile</tt>, extract an upload URL for
       
   544 	### RDoc.
       
   545 	def find_rdoc_upload( catalogFile="docs/CATALOG" )
       
   546 		find_catalog_keyword( 'upload', catalogFile )
       
   547 	end
       
   548 
       
   549 
       
   550 	### Given a documentation <tt>catalogFile</tt>, extract a CVS web frontend
       
   551 	### URL for RDoc.
       
   552 	def find_rdoc_cvs_url( catalogFile="docs/CATALOG" )
       
   553 		find_catalog_keyword( 'webcvs', catalogFile )
       
   554 	end
       
   555 
       
   556 
       
   557 	### Find one or more 'accessor' directives in the catalog if they exist and
       
   558 	### return an Array of them.
       
   559 	def find_rdoc_accessors( catalogFile="docs/CATALOG" )
       
   560 		accessors = []
       
   561 		in_attr_section = false
       
   562 		indent = ''
       
   563 
       
   564 		if File::exists?( catalogFile )
       
   565 			verbose_msg "Extracting accessors from CATALOG file (%s).\n" % catalogFile
       
   566 
       
   567 			# Read lines from the catalog
       
   568 			File::foreach( catalogFile ) do |line|
       
   569 				debug_msg( "  Examining line #{line.inspect}..." )
       
   570 
       
   571 				# Multi-line accessors
       
   572 				if in_attr_section
       
   573 					if /^#\s+([a-z0-9_]+(?:\s*=\s*.*)?)$/i.match( line )
       
   574 						debug_msg( "    Found accessor: #$1" )
       
   575 						accessors << $1
       
   576 						next
       
   577 					end
       
   578 
       
   579 					debug_msg( "  End of accessors section." )
       
   580 					in_attr_section = false
       
   581 
       
   582 				# Single-line accessor
       
   583 				elsif /^#\s*Accessors:\s*(\S+)$/i.match( line )
       
   584 					debug_msg( "  Found single accessors line: #$1" )
       
   585 					vals = $1.split(/,/).collect {|val| val.strip }
       
   586 					accessors.replace( vals )
       
   587 
       
   588 				# Multi-line accessor header
       
   589 				elsif /^#\s*Accessors:\s*$/i.match( line )
       
   590 					debug_msg( "  Start of accessors section." )
       
   591 					in_attr_section = true
       
   592 				end
       
   593 
       
   594 			end
       
   595 		end
       
   596 
       
   597 		debug_msg( "Found accessors: %s" % accessors.join(",") )
       
   598 		return accessors
       
   599 	end
       
   600 
       
   601 
       
   602 	### Given a documentation <tt>catalogFile</tt>, try extracting the given
       
   603 	### +keyword+'s value from it. Keywords are lines that look like:
       
   604 	###   # <keyword>: <value>
       
   605 	### Returns +nil+ if the catalog file was unreadable or didn't contain the
       
   606 	### specified +keyword+.
       
   607 	def find_catalog_keyword( keyword, catalogFile="docs/CATALOG" )
       
   608 		val = nil
       
   609 
       
   610 		if File::exists? catalogFile
       
   611 			verbose_msg "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalogFile
       
   612 			File::foreach( catalogFile ) do |line|
       
   613 				debug_msg( "Examining line #{line.inspect}..." )
       
   614 				val = $1.strip and break if /^#\s*#{keyword}:\s*(.*)$/i.match( line )
       
   615 			end
       
   616 		end
       
   617 
       
   618 		return val
       
   619 	end
       
   620 
       
   621 
       
   622 	### Given a documentation <tt>catalogFile</tt>, which is in the same format
       
   623 	### as that described by #read_manifest, read and expand it, and then return
       
   624 	### a list of those files which appear to have RDoc documentation in
       
   625 	### them. If <tt>catalogFile</tt> is nil or does not exist, the MANIFEST
       
   626 	### file is used instead.
       
   627 	def find_rdocable_files( catalogFile="docs/CATALOG" )
       
   628 		startlist = []
       
   629 		if File.exists? catalogFile
       
   630 			verbose_msg "Using CATALOG file (%s).\n" % catalogFile
       
   631 			startlist = get_vetted_manifest( catalogFile )
       
   632 		else
       
   633 			verbose_msg "Using default MANIFEST\n"
       
   634 			startlist = get_vetted_manifest()
       
   635 		end
       
   636 
       
   637 		verbose_msg "Looking for RDoc comments in:\n"
       
   638 		startlist.select {|fn|
       
   639 			verbose_msg "  #{fn}: "
       
   640 			found = false
       
   641 			File::open( fn, "r" ) {|fh|
       
   642 				fh.each {|line|
       
   643 					if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*}
       
   644 						found = true
       
   645 						break
       
   646 					end
       
   647 				}
       
   648 			}
       
   649 
       
   650 			verbose_msg( (found ? "yes" : "no") + "\n" )
       
   651 			found
       
   652 		}
       
   653 	end
       
   654 
       
   655 
       
   656 	### Open a file and filter each of its lines through the given block a
       
   657 	### <tt>line</tt> at a time. The return value of the block is used as the
       
   658 	### new line, or omitted if the block returns <tt>nil</tt> or
       
   659 	### <tt>false</tt>.
       
   660 	def edit_in_place( file, testMode=false ) # :yields: line
       
   661 		raise "No block specified for editing operation" unless block_given?
       
   662 
       
   663 		tempName = "#{file}.#{$$}"
       
   664 		File::open( tempName, File::RDWR|File::CREAT, 0600 ) {|tempfile|
       
   665 			File::open( file, File::RDONLY ) {|fh|
       
   666 				fh.each {|line|
       
   667 					newline = yield( line ) or next
       
   668 					tempfile.print( newline )
       
   669 					$stderr.puts "%p -> %p" % [ line, newline ] if
       
   670 						line != newline
       
   671 				}
       
   672 			}
       
   673 		}
       
   674 
       
   675 		if testMode
       
   676 			File::unlink( tempName )
       
   677 		else
       
   678 			File::rename( tempName, file )
       
   679 		end
       
   680 	end
       
   681 
       
   682 
       
   683 	### Execute the specified shell <tt>command</tt>, read the results, and
       
   684 	### return them. Like a %x{} that returns an Array instead of a String.
       
   685 	def shell_command( *command )
       
   686 		raise "Empty command" if command.empty?
       
   687 
       
   688 		cmdpipe = IO::popen( command.join(' '), 'r' )
       
   689 		return cmdpipe.readlines
       
   690 	end
       
   691 
       
   692 
       
   693 	### Execute a block with $VERBOSE set to +false+, restoring it to its
       
   694 	### previous value before returning.
       
   695 	def verbose_off
       
   696 		raise LocalJumpError, "No block given" unless block_given?
       
   697 
       
   698 		thrcrit = Thread.critical
       
   699 		oldverbose = $VERBOSE
       
   700 		begin
       
   701 			Thread.critical = true
       
   702 			$VERBOSE = false
       
   703 			yield
       
   704 		ensure
       
   705 			$VERBOSE = oldverbose
       
   706 			Thread.critical = false
       
   707 		end
       
   708 	end
       
   709 
       
   710 
       
   711 	### Try the specified code block, printing the given 
       
   712 	def try( msg, bind=TOPLEVEL_BINDING )
       
   713 		result = ''
       
   714 		if msg =~ /^to\s/
       
   715 			message "Trying #{msg}...\n"
       
   716 		else
       
   717 			message msg + "\n"
       
   718 		end
       
   719 
       
   720 		begin
       
   721 			rval = nil
       
   722 			if block_given?
       
   723 				rval = yield
       
   724 			else
       
   725 				file, line = caller(1)[0].split(/:/,2)
       
   726 				rval = eval( msg, bind, file, line.to_i )
       
   727 			end
       
   728 
       
   729 			PP.pp( rval, result )
       
   730 
       
   731 		rescue Exception => err
       
   732 			if err.backtrace
       
   733 				nicetrace = err.backtrace.delete_if {|frame|
       
   734 					/in `(try|eval)'/ =~ frame
       
   735 				}.join("\n\t")
       
   736 			else
       
   737 				nicetrace = "Exception had no backtrace"
       
   738 			end
       
   739 
       
   740 			result = err.message + "\n\t" + nicetrace
       
   741 
       
   742 		ensure
       
   743 			divider
       
   744 			message result.chomp + "\n"
       
   745 			divider
       
   746 			$stderr.puts
       
   747 		end
       
   748 	end
       
   749 
       
   750 
       
   751 	### Start an IRB session with the specified binding +b+ as the current scope.
       
   752 	def start_irb_session( b )
       
   753 		IRB.setup(nil)
       
   754 
       
   755 		workspace = IRB::WorkSpace.new( b )
       
   756 
       
   757 		if IRB.conf[:SCRIPT]
       
   758 			irb = IRB::Irb.new( workspace, IRB.conf[:SCRIPT] )
       
   759 		else
       
   760 			irb = IRB::Irb.new( workspace )
       
   761 		end
       
   762 
       
   763 		IRB.conf[:IRB_RC].call( irb.context ) if IRB.conf[:IRB_RC]
       
   764 		IRB.conf[:MAIN_CONTEXT] = irb.context
       
   765 
       
   766 		trap("SIGINT") do
       
   767 			irb.signal_handle
       
   768 		end
       
   769 
       
   770 		catch(:IRB_EXIT) do
       
   771 			irb.eval_input
       
   772 		end
       
   773 	end
       
   774 
       
   775 end # module UtilityFunctions
       
   776 
       
   777 
       
   778 
       
   779 if __FILE__ == $0
       
   780 	# $DEBUG = true
       
   781 	include UtilityFunctions
       
   782 
       
   783 	projname = extract_project_name()
       
   784 	header "Project: #{projname}"
       
   785 
       
   786 	ver = extract_version() || [0,0,1]
       
   787 	puts "Version: %s\n" % ver.join('.')
       
   788 
       
   789 	if File::directory?( "docs" )
       
   790 		puts "Rdoc:",
       
   791 			"  Title: " + find_rdoc_title(),
       
   792 			"  Main: " + find_rdoc_main(),
       
   793 			"  Upload: " + find_rdoc_upload(),
       
   794 			"  SCCS URL: " + find_rdoc_cvs_url(),
       
   795 			"  Accessors: " + find_rdoc_accessors().join(",")
       
   796 	end
       
   797 
       
   798 	puts "Manifest:",
       
   799 		"  " + get_vetted_manifest().join("\n  ")
       
   800 end