wordnet 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ BEGIN {
4
+ require 'pathname'
5
+ basedir = Pathname.new( __FILE__ ).dirname.parent.parent
6
+
7
+ libdir = basedir + 'lib'
8
+
9
+ $LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
10
+ }
11
+
12
+ begin
13
+ require 'fileutils'
14
+ require 'tmpdir'
15
+ require 'bdb'
16
+ require 'spec/runner'
17
+ require 'spec/lib/helpers'
18
+
19
+ require 'wordnet/lexicon'
20
+ require 'wordnet/synset'
21
+ rescue LoadError
22
+ unless Object.const_defined?( :Gem )
23
+ require 'rubygems'
24
+ retry
25
+ end
26
+ raise
27
+ end
28
+
29
+
30
+ #####################################################################
31
+ ### C O N T E X T S
32
+ #####################################################################
33
+
34
+ describe WordNet::Synset do
35
+
36
+ Accessors = [
37
+ :part_of_speech,
38
+ :offset,
39
+ :filenum,
40
+ :wordlist,
41
+ :pointerlist,
42
+ :frameslist,
43
+ :gloss,
44
+ ]
45
+
46
+ RelationMethods = [
47
+ :antonyms,
48
+ :hypernyms,
49
+ :entailment,
50
+ :hyponyms,
51
+ :causes,
52
+ :verb_groups,
53
+ :similar_to,
54
+ :participles,
55
+ :pertainyms,
56
+ :attributes,
57
+ :derived_from,
58
+ :derivations,
59
+ :see_also,
60
+
61
+ :instance_hyponyms,
62
+
63
+ :instance_hypernyms,
64
+
65
+ :member_meronyms,
66
+ :stuff_meronyms,
67
+ :portion_meronyms,
68
+ :component_meronyms,
69
+ :feature_meronyms,
70
+ :phase_meronyms,
71
+ :place_meronyms,
72
+
73
+ :member_holonyms,
74
+ :stuff_holonyms,
75
+ :portion_holonyms,
76
+ :component_holonyms,
77
+ :feature_holonyms,
78
+ :phase_holonyms,
79
+ :place_holonyms,
80
+
81
+ :category_domains,
82
+ :region_domains,
83
+ :usage_domains,
84
+
85
+ :category_members,
86
+ :region_members,
87
+ :usage_members,
88
+ ]
89
+
90
+ AggregateRelationMethods = [
91
+ :meronyms,
92
+ :holonyms,
93
+ :domains,
94
+ :members,
95
+ ]
96
+
97
+
98
+ before( :each ) do
99
+ @blank_syn = WordNet::Synset::new( @lexicon, "1%n", WordNet::Noun )
100
+ @traversal_syn = @lexicon.lookup_synsets( 'linguistics', :noun, 1 )
101
+ end
102
+
103
+
104
+ #################################################################
105
+ ### T E S T S
106
+ #################################################################
107
+
108
+ ### Accessors
109
+ def test_accessors
110
+ printTestHeader "Synset: Accessors"
111
+ rval = nil
112
+
113
+ assert_respond_to @blankSyn, :lexicon
114
+
115
+ Accessors.each do |meth|
116
+ assert_respond_to @blankSyn, meth
117
+ assert_respond_to @blankSyn, "#{meth}="
118
+
119
+ assert_nothing_raised do
120
+ rval = @blankSyn.send( meth )
121
+ end
122
+ end
123
+ end
124
+
125
+ ### Relations
126
+ def test_relations
127
+ printTestHeader "Synset: Relation methods"
128
+ rval = nil
129
+
130
+ RelationMethods.each do |meth|
131
+ casemeth = meth.to_s.sub( /^(\w)/ ) {|char| char.upcase }.intern
132
+
133
+ assert_respond_to @blankSyn, meth
134
+ assert_respond_to @blankSyn, "#{meth}="
135
+
136
+ assert_nothing_raised {
137
+ rval = @blankSyn.send( meth )
138
+ }
139
+
140
+ assert_instance_of Array, rval
141
+ end
142
+ end
143
+
144
+ ### Aggregate relation methods
145
+ def test_aggregate_relations
146
+ printTestHeader "Synset: Aggregate relations"
147
+ rval = nil
148
+
149
+ AggregateRelationMethods.each {|meth|
150
+ assert_respond_to @blankSyn, meth
151
+
152
+ assert_nothing_raised {
153
+ rval = @blankSyn.send( meth )
154
+ }
155
+
156
+ assert_instance_of Array, rval
157
+ }
158
+ end
159
+
160
+ ### Traversal method
161
+ def test_synset_should_respond_to_traverse_method
162
+ printTestHeader "Synset: Traversal method"
163
+ assert_respond_to @traversalSyn, :traverse
164
+ end
165
+
166
+ ### :TODO: This should really be split into two tests.
167
+ ### Traversal: include origin, break loop
168
+ def test_traversal_with_true_second_arg_should_include_origin
169
+ printTestHeader "Synset: Traversal, including origin, break"
170
+ rval = nil
171
+ count = depth = 0
172
+ sets = []
173
+
174
+ assert_nothing_raised {
175
+ rval = @traversalSyn.traverse( :hyponyms, true ) {|tsyn,tdepth|
176
+ sets << tsyn
177
+ depth = tdepth
178
+ count += 1
179
+ return true
180
+ }
181
+ }
182
+ assert_equal true, rval
183
+ assert_equal 1, sets.length
184
+ assert_equal @traversalSyn, sets[0]
185
+ assert_equal 0, depth
186
+ assert_equal 1, count
187
+ end
188
+
189
+ ### :TODO: This should really be split into two tests.
190
+ ### Traversal: exclude origin, break loop
191
+ def test_traversal_with_false_second_arg_should_not_include_origin
192
+ printTestHeader "Synset: Traversal, excluding origin, break"
193
+ rval = nil
194
+ count = depth = 0
195
+ sets = []
196
+
197
+ assert_nothing_raised {
198
+ rval = @traversalSyn.traverse( :hyponyms, false ) {|tsyn,tdepth|
199
+ sets << tsyn
200
+ depth = tdepth
201
+ count += 1
202
+ return true
203
+ }
204
+ }
205
+ assert_equal true, rval
206
+ assert_equal 1, sets.length
207
+ assert_not_equal @traversalSyn, sets[0]
208
+ assert_equal 1, depth
209
+ assert_equal 1, count
210
+ end
211
+
212
+ ### Traversal: include origin, nobreak, noblock
213
+ def test_hyponym_traversal_with_no_block_should_return_appropriate_hyponyms
214
+ printTestHeader "Synset: Traversal, include origin, nobreak, noblock"
215
+ sets = []
216
+
217
+ assert_nothing_raised {
218
+ sets = @traversalSyn.traverse( :hyponyms )
219
+ }
220
+ assert_block { sets.length > 1 }
221
+ assert_equal @traversalSyn, sets[0]
222
+ assert_block { sets.find {|hsyn| hsyn.words.include?( "grammar" )} }
223
+ assert_block { sets.find {|hsyn| hsyn.words.include?( "syntax" )} }
224
+ assert_block { sets.find {|hsyn| hsyn.words.include?( "computational linguistics" )} }
225
+ end
226
+
227
+
228
+ ### Traversal: exclude origin, nobreak, noblock
229
+ def test_hyponym_traversal_with_no_block_and_false_second_arg_should_return_holonyms_but_not_the_origin
230
+ printTestHeader "Synset: Traversal, exclude origin, nobreak, noblock"
231
+ sets = []
232
+
233
+ assert_nothing_raised {
234
+ sets = @traversalSyn.traverse( :hyponyms, false )
235
+ }
236
+ assert_block { sets.length > 1 }
237
+ assert_not_equal @traversalSyn, sets[0]
238
+ assert_block { sets.find {|hsyn| hsyn.words.include?( "grammar" )} }
239
+ assert_block { sets.find {|hsyn| hsyn.words.include?( "syntax" )} }
240
+ assert_block { sets.find {|hsyn| hsyn.words.include?( "computational linguistics" )} }
241
+ end
242
+
243
+
244
+ ### Traversal: include origin, nobreak, noblock
245
+ def test_traversal_break_after_3_should_include_three_sets_plus_origin
246
+ printTestHeader "Synset: Traversal, break after 3"
247
+ rval = nil
248
+ sets = Hash::new {|hsh,key| hsh[key] = []}
249
+
250
+ assert_nothing_raised {
251
+ rval = @traversalSyn.traverse( :hyponyms ) {|tsyn,tdepth|
252
+ sets[tdepth] << tsyn
253
+ tdepth == 3
254
+ }
255
+ }
256
+ assert_equal 4, sets.keys.length
257
+ assert_equal [0,1,2,3], sets.keys.sort
258
+ assert_equal 1, sets[3].length
259
+ assert rval, "Break early flag expected to be set"
260
+ end
261
+
262
+
263
+ ### Part of speech: part_of_speech
264
+ def test_part_of_speech_should_return_the_symbol_part_of_speech
265
+ printTestHeader "Synset: part_of_speech"
266
+ rval = nil
267
+
268
+ assert_nothing_raised { rval = @traversalSyn.part_of_speech }
269
+ assert_equal :noun, rval
270
+ end
271
+
272
+
273
+ ### Part of speech: pos
274
+ def test_pos_should_return_the_synsets_singlechar_part_of_speech
275
+ printTestHeader "Synset: pos"
276
+ rval = nil
277
+
278
+ assert_nothing_raised { rval = @traversalSyn.pos }
279
+ assert_equal "n", rval
280
+ end
281
+
282
+
283
+ ### :TODO: Test traversal, content, storing, higher-order functions
284
+
285
+
286
+ end
287
+
288
+
data/utils.rb ADDED
@@ -0,0 +1,838 @@
1
+ #
2
+ # Install/distribution utility functions
3
+ # $Id: utils.rb 94 2008-07-25 02:47:42Z deveiant $
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 FaerieMUD, 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
+
37
+ BEGIN {
38
+ require 'pathname'
39
+ require 'rbconfig'
40
+ require 'uri'
41
+ require 'find'
42
+ require 'pp'
43
+ require 'irb'
44
+
45
+ begin
46
+ require 'readline'
47
+ include Readline
48
+ rescue LoadError => e
49
+ $stderr.puts "Faking readline..."
50
+ def readline( prompt )
51
+ $stderr.print prompt.chomp
52
+ return $stdin.gets.chomp
53
+ end
54
+ end
55
+
56
+ }
57
+
58
+
59
+ ### Command-line utility functions
60
+ module UtilityFunctions
61
+ include Config
62
+
63
+ # The list of regexen that eliminate files from the MANIFEST
64
+ ANTIMANIFEST = [
65
+ /makedist\.rb/,
66
+ /\bCVS\b/,
67
+ /~$/,
68
+ /^#/,
69
+ %r{docs/html},
70
+ %r{docs/man},
71
+ /\bTEMPLATE\.\w+\.tpl\b/,
72
+ /\.cvsignore/,
73
+ /\.s?o$/,
74
+ ]
75
+
76
+ # Set some ANSI escape code constants (Shamelessly stolen from Perl's
77
+ # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
78
+ AnsiAttributes = {
79
+ 'clear' => 0,
80
+ 'reset' => 0,
81
+ 'bold' => 1,
82
+ 'dark' => 2,
83
+ 'underline' => 4,
84
+ 'underscore' => 4,
85
+ 'blink' => 5,
86
+ 'reverse' => 7,
87
+ 'concealed' => 8,
88
+
89
+ 'black' => 30, 'on_black' => 40,
90
+ 'red' => 31, 'on_red' => 41,
91
+ 'green' => 32, 'on_green' => 42,
92
+ 'yellow' => 33, 'on_yellow' => 43,
93
+ 'blue' => 34, 'on_blue' => 44,
94
+ 'magenta' => 35, 'on_magenta' => 45,
95
+ 'cyan' => 36, 'on_cyan' => 46,
96
+ 'white' => 37, 'on_white' => 47
97
+ }
98
+
99
+ ErasePreviousLine = "\033[A\033[K"
100
+
101
+ ManifestHeader = (<<-"EOF").gsub( /^\t+/, '' )
102
+ #
103
+ # Distribution Manifest
104
+ # Created: #{Time::now.to_s}
105
+ #
106
+
107
+ EOF
108
+
109
+ # A cache of programs found by find_program()
110
+ Programs = {}
111
+
112
+
113
+ ###############
114
+ module_function
115
+ ###############
116
+
117
+ # Create a string that contains the ANSI codes specified and return it
118
+ def ansi_code( *attributes )
119
+ attributes.flatten!
120
+ # $stderr.puts "Returning ansicode for TERM = %p: %p" %
121
+ # [ ENV['TERM'], attributes ]
122
+ return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
123
+ attributes = AnsiAttributes.values_at( *attributes ).compact.join(';')
124
+
125
+ # $stderr.puts " attr is: %p" % [attributes]
126
+ if attributes.empty?
127
+ return ''
128
+ else
129
+ return "\e[%sm" % attributes
130
+ end
131
+ end
132
+
133
+
134
+ ### Colorize the given +string+ with the specified +attributes+ and return it, handling line-endings, etc.
135
+ def colorize( string, *attributes )
136
+ ending = string[/(\s)$/] || ''
137
+ string = string.rstrip
138
+ return ansi_code( attributes.flatten ) + string + ansi_code( 'reset' ) + ending
139
+ end
140
+
141
+
142
+ # Test for the presence of the specified <tt>library</tt>, and output a
143
+ # message describing the test using <tt>nicename</tt>. If <tt>nicename</tt>
144
+ # is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default.
145
+ def test_for_library( library, nicename=nil, progress=false )
146
+ nicename ||= library
147
+ message( "Testing for the #{nicename} library..." ) if progress
148
+ if $LOAD_PATH.detect {|dir|
149
+ File.exists?(File.join(dir,"#{library}.rb")) ||
150
+ File.exists?(File.join(dir,"#{library}.#{CONFIG['DLEXT']}"))
151
+ }
152
+ message( "found.\n" ) if progress
153
+ return true
154
+ else
155
+ message( "not found.\n" ) if progress
156
+ return false
157
+ end
158
+ end
159
+
160
+
161
+ # Test for the presence of the specified <tt>library</tt>, and output a
162
+ # message describing the problem using <tt>nicename</tt>. If
163
+ # <tt>nicename</tt> is <tt>nil</tt>, the value in <tt>library</tt> is used
164
+ # to build a default. If <tt>raa_url</tt> and/or <tt>download_url</tt> are
165
+ # specified, they are also use to build a message describing how to find the
166
+ # required library. If <tt>fatal</tt> is <tt>true</tt>, a missing library
167
+ # will cause the program to abort.
168
+ def test_for_required_library( library, nicename=nil, raa_url=nil, download_url=nil, fatal=true )
169
+ nicename ||= library
170
+ unless test_for_library( library, nicename )
171
+ msgs = [ "You are missing the required #{nicename} library.\n" ]
172
+ msgs << "RAA: #{raa_url}\n" if raa_url
173
+ msgs << "Download: #{download_url}\n" if download_url
174
+ if fatal
175
+ abort msgs.join('')
176
+ else
177
+ error_message msgs.join('')
178
+ end
179
+ end
180
+ return true
181
+ end
182
+
183
+
184
+ ### Output <tt>msg</tt> as a ANSI-colored program/section header (white on
185
+ ### blue).
186
+ def header( msg )
187
+ msg.chomp!
188
+ $stderr.puts ansi_code( 'bold', 'white', 'on_blue' ) + msg + ansi_code( 'reset' )
189
+ $stderr.flush
190
+ end
191
+
192
+
193
+ ### Output <tt>msg</tt> to STDERR and flush it.
194
+ def message( *msgs )
195
+ $stderr.print( msgs.join("\n") )
196
+ $stderr.flush
197
+ end
198
+
199
+
200
+ ### Output +msg+ to STDERR and flush it if $VERBOSE is true.
201
+ def verbose_msg( msg )
202
+ msg.chomp!
203
+ message( msg + "\n" ) if $VERBOSE
204
+ end
205
+
206
+
207
+ ### Output the specified <tt>msg</tt> as an ANSI-colored error message
208
+ ### (white on red).
209
+ def error_msg( msg )
210
+ message ansi_code( 'bold', 'white', 'on_red' ) + msg + ansi_code( 'reset' )
211
+ end
212
+ alias :error_message :error_msg
213
+
214
+
215
+ ### Output the specified <tt>msg</tt> as an ANSI-colored debugging message
216
+ ### (yellow on blue).
217
+ def debug_msg( msg )
218
+ return unless $DEBUG
219
+ msg.chomp!
220
+ $stderr.puts ansi_code( 'bold', 'yellow', 'on_blue' ) + ">>> #{msg}" + ansi_code( 'reset' )
221
+ $stderr.flush
222
+ end
223
+
224
+
225
+ ### Erase the previous line (if supported by your terminal) and output the
226
+ ### specified <tt>msg</tt> instead.
227
+ def replace_msg( msg )
228
+ $stderr.puts
229
+ $stderr.print ErasePreviousLine
230
+ message( msg )
231
+ end
232
+ alias :replace_message :replace_msg
233
+
234
+
235
+ ### Output a divider made up of <tt>length</tt> hyphen characters.
236
+ def divider( length=75 )
237
+ $stderr.puts "\r" + ("-" * length )
238
+ end
239
+ alias :write_line :divider
240
+
241
+
242
+ ### Output the specified <tt>msg</tt> colored in ANSI red and exit with a
243
+ ### status of 1.
244
+ def abort( msg )
245
+ print ansi_code( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansi_code( 'reset' ) + "\n\n"
246
+ Kernel.exit!( 1 )
247
+ end
248
+
249
+
250
+ ### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
251
+ ### return the user's input with leading and trailing spaces removed. If a
252
+ ### test is provided, the prompt will repeat until the test returns true.
253
+ ### An optional failure message can also be passed in.
254
+ def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
255
+ prompt_string.chomp!
256
+ prompt_string << ":" unless /\W$/.match( prompt_string )
257
+ response = nil
258
+
259
+ begin
260
+ response = readline( ansi_code('bold', 'green') +
261
+ "#{prompt_string} " + ansi_code('reset') ) || ''
262
+ response.strip!
263
+ if block_given? && ! yield( response )
264
+ error_message( failure_msg + "\n\n" )
265
+ response = nil
266
+ end
267
+ end until response
268
+
269
+ return response
270
+ end
271
+
272
+
273
+ ### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
274
+ ### substituting the given <tt>default</tt> if the user doesn't input
275
+ ### anything. If a test is provided, the prompt will repeat until the test
276
+ ### returns true. An optional failure message can also be passed in.
277
+ def prompt_with_default( prompt_string, default, failure_msg="Try again." )
278
+ response = nil
279
+
280
+ begin
281
+ response = prompt( "%s [%s]" % [ prompt_string, default ] )
282
+ response = default if response.empty?
283
+
284
+ if block_given? && ! yield( response )
285
+ error_message( failure_msg + "\n\n" )
286
+ response = nil
287
+ end
288
+ end until response
289
+
290
+ return response
291
+ end
292
+
293
+
294
+ ### Display a description of a potentially-dangerous task, and prompt
295
+ ### for confirmation. If the user answers with anything that begins
296
+ ### with 'y', yield to the block, else raise with an error.
297
+ def ask_for_confirmation( description )
298
+ puts description
299
+
300
+ answer = prompt_with_default( "Continue?", 'n' ) do |input|
301
+ input =~ /^[yn]/i
302
+ end
303
+
304
+ case answer
305
+ when /^y/i
306
+ yield
307
+ else
308
+ error "Aborted."
309
+ fail
310
+ end
311
+ end
312
+
313
+
314
+ ### Search for the program specified by the given <tt>progname</tt> in the
315
+ ### user's <tt>PATH</tt>, and return the full path to it, or <tt>nil</tt> if
316
+ ### no such program is in the path.
317
+ def find_program( progname )
318
+ unless Programs.key?( progname )
319
+ ENV['PATH'].split(File::PATH_SEPARATOR).
320
+ collect {|dir| Pathnanme.new(dir) }.each do |dir|
321
+ file = dir + progname
322
+ if file.executable?
323
+ Programs[ progname ] = file
324
+ break
325
+ end
326
+ end
327
+ end
328
+
329
+ return Programs[ progname ].to_s
330
+ end
331
+
332
+
333
+ ### Search for the release version for the project in the specified
334
+ ### +directory+ using tags named "RELEASE_<major>_<minor>" if it's a CVS project
335
+ ### or the 'project-version' metadata value of the toplevel directory if it's
336
+ ### a Subversion project.
337
+ def extract_version( directory='.' )
338
+ release = nil
339
+
340
+ Dir::chdir( directory ) do
341
+ if File::directory?( "CVS" )
342
+ verbose_msg( "Project is versioned via CVS. Searching for RELEASE_*_* tags..." )
343
+
344
+ if (( cvs = find_program('cvs') ))
345
+ revs = []
346
+ output = %x{cvs log}
347
+ output.scan( /RELEASE_(\d+(?:_\d\w+)*)/ ) {|match|
348
+ rev = $1.split(/_/).collect {|s| Integer(s) rescue 0}
349
+ verbose_msg( "Found %s...\n" % rev.join('.') )
350
+ revs << rev
351
+ }
352
+
353
+ release = revs.sort.last
354
+ end
355
+
356
+ elsif File::directory?( '.svn' )
357
+ verbose_msg( "Project is versioned via Subversion" )
358
+
359
+ if (( svn = find_program('svn') ))
360
+ output = %x{svn pg project-version}.chomp
361
+ unless output.empty?
362
+ verbose_msg( "Using 'project-version' property: %p" % output )
363
+ release = output.split( /[._]/ ).collect {|s| Integer(s) rescue 0}
364
+ end
365
+ end
366
+ end
367
+ end
368
+
369
+ return release
370
+ end
371
+
372
+
373
+ ### Find the current release version for the project in the specified
374
+ ### +directory+ and return its successor.
375
+ def extract_next_version( directory='.' )
376
+ version = extract_version( directory ) || [0,0,0]
377
+ version.compact!
378
+ version[-1] += 1
379
+
380
+ return version
381
+ end
382
+
383
+
384
+ # Pattern for extracting the name of the project from a Subversion URL
385
+ SVNUrlPath = %r{
386
+ .*/ # Skip all but the last bit
387
+ ([^/]+) # $1 = project name
388
+ / # Followed by / +
389
+ (?:
390
+ trunk | # 'trunk'
391
+ (
392
+ branches | # ...or branches/branch-name
393
+ tags # ...or tags/tag-name
394
+ )/\w
395
+ )
396
+ $ # bound to the end
397
+ }ix
398
+
399
+ ### Extract the project name for the given +directory+. The project name is
400
+ ### the repository name if it's versioned with CVS, set via the 'project-name'
401
+ ### metadata value if versioned with Subversion, or just based on the name of the
402
+ ### directory itself if it's not versioned with one of those two systems.
403
+ def extract_project_name( directory='.' )
404
+ name = nil
405
+
406
+ Dir::chdir( directory ) do
407
+
408
+ # CVS-controlled
409
+ if File::directory?( "CVS" )
410
+ verbose_msg( "Project is versioned via CVS. Using repository name." )
411
+ name = File.open( "CVS/Repository", "r").readline.chomp
412
+ name.sub!( %r{.*/}, '' )
413
+
414
+ # Subversion-controlled
415
+ elsif File::directory?( '.svn' )
416
+ verbose_msg( "Project is versioned via Subversion" )
417
+
418
+ # If the machine has the svn tool, try to get the project name
419
+ if (( svn = find_program( 'svn' ) ))
420
+
421
+ # First try an explicit property
422
+ output = shell_command( svn, 'pg', 'project-name' )
423
+ if !output.empty?
424
+ verbose_msg( "Using 'project-name' property: %p" % output )
425
+ name = output.first.chomp
426
+
427
+ # If that doesn't work, try to figure it out from the URL
428
+ elsif (( uri = get_svn_uri() ))
429
+ name = uri.path.sub( SVNUrlPath ) { $1 }
430
+ end
431
+ end
432
+ end
433
+
434
+ # Fall back to guessing based on the directory name
435
+ unless name
436
+ name = File::basename(File::dirname( File::expand_path(__FILE__) ))
437
+ end
438
+ end
439
+
440
+ return name
441
+ end
442
+
443
+
444
+ ### Extract the Subversion URL from the specified directory and return it as
445
+ ### a URI object.
446
+ def get_svn_uri( directory='.' )
447
+ uri = nil
448
+
449
+ Dir::chdir( directory ) do
450
+ output = %x{svn info}
451
+ debug_msg( "Using info: %p" % output )
452
+
453
+ if /^URL: \s* ( .* )/xi.match( output )
454
+ uri = URI::parse( $1 )
455
+ end
456
+ end
457
+
458
+ return uri
459
+ end
460
+
461
+
462
+ ### (Re)make a manifest file in the specified +path+.
463
+ def make_manifest( path="MANIFEST" )
464
+ if File::exists?( path )
465
+ reply = prompt_with_default( "Replace current '#{path}'? [yN]", "n" )
466
+ return false unless /^y/i.match( reply )
467
+
468
+ verbose_msg "Replacing manifest at '#{path}'"
469
+ else
470
+ verbose_msg "Creating new manifest at '#{path}'"
471
+ end
472
+
473
+ files = []
474
+ verbose_msg( "Finding files...\n" )
475
+ Find::find( Dir::pwd ) do |f|
476
+ Find::prune if File::directory?( f ) &&
477
+ /^\./.match( File::basename(f) )
478
+ verbose_msg( " found: #{f}\n" )
479
+ files << f.sub( %r{^#{Dir::pwd}/?}, '' )
480
+ end
481
+ files = vet_manifest( files )
482
+
483
+ verbose_msg( "Writing new manifest to #{path}..." )
484
+ File::open( path, File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
485
+ ofh.puts( ManifestHeader )
486
+ ofh.puts( files )
487
+ end
488
+ verbose_msg( "done." )
489
+ end
490
+
491
+
492
+ ### Read the specified <tt>manifest_file</tt>, which is a text file
493
+ ### describing which files to package up for a distribution. The manifest
494
+ ### should consist of one or more lines, each containing one filename or
495
+ ### shell glob pattern.
496
+ def read_manifest( manifest_file="MANIFEST" )
497
+ verbose_msg "Building manifest..."
498
+ raise "Missing #{manifest_file}, please remake it" unless File.exists? manifest_file
499
+
500
+ manifest = IO::readlines( manifest_file ).collect {|line|
501
+ line.chomp
502
+ }.select {|line|
503
+ line !~ /^(\s*(#.*)?)?$/
504
+ }
505
+
506
+ filelist = []
507
+ for pat in manifest
508
+ verbose_msg "Adding files that match '#{pat}' to the file list"
509
+ filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)}
510
+ end
511
+
512
+ verbose_msg "found #{filelist.length} files.\n"
513
+ return filelist
514
+ end
515
+
516
+
517
+ ### Given a <tt>filelist</tt> like that returned by #read_manifest, remove
518
+ ### the entries therein which match the Regexp objects in the given
519
+ ### <tt>antimanifest</tt> and return the resultant Array.
520
+ def vet_manifest( filelist, antimanifest=ANTIMANIFEST )
521
+ orig_length = filelist.length
522
+ verbose_msg "Vetting manifest..."
523
+
524
+ for regex in antimanifest
525
+ verbose_msg "\n\tPattern /#{regex.source}/ removed: " +
526
+ filelist.find_all {|file| regex.match(file)}.join(', ')
527
+ filelist.delete_if {|file| regex.match(file)}
528
+ end
529
+
530
+ verbose_msg "removed #{orig_length - filelist.length} files from the list.\n"
531
+ return filelist
532
+ end
533
+
534
+
535
+ ### Combine a call to #read_manifest with one to #vet_manifest.
536
+ def get_vetted_manifest( manifest_file="MANIFEST", antimanifest=ANTIMANIFEST )
537
+ vet_manifest( read_manifest(manifest_file), antimanifest )
538
+ end
539
+
540
+
541
+ ### Given a documentation <tt>catalog_file</tt>, extract the title, if
542
+ ### available, and return it. Otherwise generate a title from the name of
543
+ ### the CVS module.
544
+ def find_rdoc_title( catalog_file="docs/CATALOG" )
545
+
546
+ # Try extracting it from the CATALOG file from a line that looks like:
547
+ # Title: Foo Bar Module
548
+ title = find_catalog_keyword( 'title', catalog_file )
549
+
550
+ # If that doesn't work for some reason, use the name of the project.
551
+ title = extract_project_name()
552
+
553
+ return title
554
+ end
555
+
556
+
557
+ ### Given a documentation <tt>catalog_file</tt>, extract the name of the file
558
+ ### to use as the initally displayed page. If extraction fails, the
559
+ ### +default+ will be used if it exists. Returns +nil+ if there is no main
560
+ ### file to be found.
561
+ def find_rdoc_main( catalog_file="docs/CATALOG", default="README" )
562
+
563
+ # Try extracting it from the CATALOG file from a line that looks like:
564
+ # Main: Foo Bar Module
565
+ main = find_catalog_keyword( 'main', catalog_file )
566
+
567
+ # Try to make some educated guesses if that doesn't work
568
+ if main.nil?
569
+ basedir = File::dirname( __FILE__ )
570
+ basedir = File::dirname( basedir ) if /docs$/ =~ basedir
571
+
572
+ if File::exists?( File::join(basedir, default) )
573
+ main = default
574
+ end
575
+ end
576
+
577
+ return main
578
+ end
579
+
580
+
581
+ ### Given a documentation <tt>catalog_file</tt>, extract an upload URL for
582
+ ### RDoc.
583
+ def find_rdoc_upload( catalog_file="docs/CATALOG" )
584
+ find_catalog_keyword( 'upload', catalog_file )
585
+ end
586
+
587
+
588
+ ### Given a documentation <tt>catalog_file</tt>, extract a CVS web frontend
589
+ ### URL for RDoc.
590
+ def find_rdoc_cvs_url( catalog_file="docs/CATALOG" )
591
+ find_catalog_keyword( 'webcvs', catalog_file )
592
+ end
593
+
594
+
595
+ ### Find one or more 'accessor' directives in the catalog if they exist and
596
+ ### return an Array of them.
597
+ def find_rdoc_accessors( catalog_file="docs/CATALOG" )
598
+ accessors = []
599
+ in_attr_section = false
600
+ indent = ''
601
+
602
+ if File::exists?( catalog_file )
603
+ verbose_msg "Extracting accessors from CATALOG file (%s).\n" % catalog_file
604
+
605
+ # Read lines from the catalog
606
+ File::foreach( catalog_file ) do |line|
607
+ debug_msg( " Examining line #{line.inspect}..." )
608
+
609
+ # Multi-line accessors
610
+ if in_attr_section
611
+ if /^#\s+([a-z0-9_]+(?:\s*=\s*.*)?)$/i.match( line )
612
+ debug_msg( " Found accessor: #$1" )
613
+ accessors << $1
614
+ next
615
+ end
616
+
617
+ debug_msg( " End of accessors section." )
618
+ in_attr_section = false
619
+
620
+ # Single-line accessor
621
+ elsif /^#\s*Accessors:\s*(\S+)$/i.match( line )
622
+ debug_msg( " Found single accessors line: #$1" )
623
+ vals = $1.split(/,/).collect {|val| val.strip }
624
+ accessors.replace( vals )
625
+
626
+ # Multi-line accessor header
627
+ elsif /^#\s*Accessors:\s*$/i.match( line )
628
+ debug_msg( " Start of accessors section." )
629
+ in_attr_section = true
630
+ end
631
+
632
+ end
633
+ end
634
+
635
+ debug_msg( "Found accessors: %s" % accessors.join(",") )
636
+ return accessors
637
+ end
638
+
639
+
640
+ ### Given a documentation <tt>catalog_file</tt>, try extracting the given
641
+ ### +keyword+'s value from it. Keywords are lines that look like:
642
+ ### # <keyword>: <value>
643
+ ### Returns +nil+ if the catalog file was unreadable or didn't contain the
644
+ ### specified +keyword+.
645
+ def find_catalog_keyword( keyword, catalog_file="docs/CATALOG" )
646
+ val = nil
647
+
648
+ if File::exists? catalog_file
649
+ verbose_msg "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalog_file
650
+ File::foreach( catalog_file ) do |line|
651
+ debug_msg( "Examining line #{line.inspect}..." )
652
+ val = $1.strip and break if /^#\s*#{keyword}:\s*(.*)$/i.match( line )
653
+ end
654
+ end
655
+
656
+ return val
657
+ end
658
+
659
+
660
+ ### Given a documentation <tt>catalog_file</tt>, which is in the same format
661
+ ### as that described by #read_manifest, read and expand it, and then return
662
+ ### a list of those files which appear to have RDoc documentation in
663
+ ### them. If <tt>catalog_file</tt> is nil or does not exist, the MANIFEST
664
+ ### file is used instead.
665
+ def find_rdocable_files( catalog_file="docs/CATALOG" )
666
+ startlist = []
667
+ if File.exists? catalog_file
668
+ verbose_msg "Using CATALOG file (%s).\n" % catalog_file
669
+ startlist = get_vetted_manifest( catalog_file )
670
+ else
671
+ verbose_msg "Using default MANIFEST\n"
672
+ startlist = get_vetted_manifest()
673
+ end
674
+
675
+ verbose_msg "Looking for RDoc comments in:\n"
676
+ startlist.select {|fn|
677
+ verbose_msg " #{fn}: "
678
+ found = false
679
+ File::open( fn, "r" ) {|fh|
680
+ fh.each {|line|
681
+ if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*}
682
+ found = true
683
+ break
684
+ end
685
+ }
686
+ }
687
+
688
+ verbose_msg( (found ? "yes" : "no") + "\n" )
689
+ found
690
+ }
691
+ end
692
+
693
+
694
+ ### Open a file and filter each of its lines through the given block a
695
+ ### <tt>line</tt> at a time. The return value of the block is used as the
696
+ ### new line, or omitted if the block returns <tt>nil</tt> or
697
+ ### <tt>false</tt>.
698
+ def edit_in_place( file, test_mode=false ) # :yields: line
699
+ raise "No block specified for editing operation" unless block_given?
700
+
701
+ temp_name = "#{file}.#{$$}"
702
+ File::open( temp_name, File::RDWR|File::CREAT, 0600 ) do |tempfile|
703
+ File::open( file, File::RDONLY ) do |fh|
704
+ fh.each do |line|
705
+ newline = yield( line ) or next
706
+ tempfile.print( newline )
707
+ $stderr.puts "%p -> %p" % [ line, newline ] if
708
+ line != newline
709
+ end
710
+ end
711
+ end
712
+
713
+ if test_mode
714
+ File::unlink( temp_name )
715
+ else
716
+ File::rename( temp_name, file )
717
+ end
718
+ end
719
+
720
+
721
+ ### Execute the specified shell <tt>command</tt>, read the results, and
722
+ ### return them. Like a %x{} that returns an Array instead of a String.
723
+ def shell_command( *command )
724
+ raise "Empty command" if command.empty?
725
+
726
+ cmdpipe = IO::open( '|-' ) or exec( *command )
727
+ return cmdpipe.readlines
728
+ end
729
+
730
+
731
+ ### Execute a block with $VERBOSE set to +false+, restoring it to its
732
+ ### previous value before returning.
733
+ def verbose_off
734
+ raise LocalJumpError, "No block given" unless block_given?
735
+
736
+ thrcrit = Thread.critical
737
+ oldverbose = $VERBOSE
738
+ begin
739
+ Thread.critical = true
740
+ $VERBOSE = false
741
+ yield
742
+ ensure
743
+ $VERBOSE = oldverbose
744
+ Thread.critical = false
745
+ end
746
+ end
747
+
748
+
749
+ ### Try the specified code block, printing the given
750
+ def try( msg, bind=TOPLEVEL_BINDING )
751
+ result = ''
752
+ if msg =~ /^to\s/
753
+ message "Trying #{msg}...\n"
754
+ else
755
+ message msg + "\n"
756
+ end
757
+
758
+ begin
759
+ rval = nil
760
+ if block_given?
761
+ rval = yield
762
+ else
763
+ file, line = caller(1)[0].split(/:/,2)
764
+ rval = eval( msg, bind, file, line.to_i )
765
+ end
766
+
767
+ PP.pp( rval, result )
768
+
769
+ rescue Exception => err
770
+ if err.backtrace
771
+ nicetrace = err.backtrace.delete_if {|frame|
772
+ /in `(try|eval)'/ =~ frame
773
+ }.join("\n\t")
774
+ else
775
+ nicetrace = "Exception had no backtrace"
776
+ end
777
+
778
+ result = err.message + "\n\t" + nicetrace
779
+
780
+ ensure
781
+ divider
782
+ message result.chomp + "\n"
783
+ divider
784
+ $stderr.puts
785
+ end
786
+ end
787
+
788
+
789
+ ### Start an IRB session with the specified binding +b+ as the current scope.
790
+ def start_irb_session( b )
791
+ IRB.setup(nil)
792
+
793
+ workspace = IRB::WorkSpace.new( b )
794
+
795
+ if IRB.conf[:SCRIPT]
796
+ irb = IRB::Irb.new( workspace, IRB.conf[:SCRIPT] )
797
+ else
798
+ irb = IRB::Irb.new( workspace )
799
+ end
800
+
801
+ IRB.conf[:IRB_RC].call( irb.context ) if IRB.conf[:IRB_RC]
802
+ IRB.conf[:MAIN_CONTEXT] = irb.context
803
+
804
+ trap("SIGINT") do
805
+ irb.signal_handle
806
+ end
807
+
808
+ catch(:IRB_EXIT) do
809
+ irb.eval_input
810
+ end
811
+ end
812
+
813
+ end # module UtilityFunctions
814
+
815
+
816
+
817
+ if __FILE__ == $0
818
+ # $DEBUG = true
819
+ include UtilityFunctions
820
+
821
+ projname = extract_project_name()
822
+ header "Project: #{projname}"
823
+
824
+ ver = extract_version() || [0,0,1]
825
+ puts "Version: %s\n" % ver.join('.')
826
+
827
+ if File::directory?( "docs" )
828
+ puts "Rdoc:",
829
+ " Title: " + find_rdoc_title(),
830
+ " Main: " + find_rdoc_main(),
831
+ " Upload: " + find_rdoc_upload(),
832
+ " VCS URL: " + find_rdoc_cvs_url(),
833
+ " Accessors: " + find_rdoc_accessors().join(",")
834
+ end
835
+
836
+ puts "Manifest:",
837
+ " " + get_vetted_manifest().join("\n ")
838
+ end