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