|
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 |