wordnet 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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