configurability 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+
5
+ # A configuration mixin for Ruby classes.
6
+ #
7
+ # @author Michael Granger <ged@FaerieMUD.org>
8
+ #
9
+ module Configurability
10
+
11
+ # Library version constant
12
+ VERSION = '1.0.0'
13
+
14
+ # Version-control revision constant
15
+ REVISION = %q$Revision: e0fef8dabba4 $
16
+
17
+ require 'configurability/logformatter.rb'
18
+
19
+
20
+ ### The objects that have had Configurability added to them
21
+ @configurable_objects = []
22
+
23
+ ### Logging
24
+ @default_logger = Logger.new( $stderr )
25
+ @default_logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
26
+
27
+ @default_log_formatter = Configurability::LogFormatter.new( @default_logger )
28
+ @default_logger.formatter = @default_log_formatter
29
+
30
+ @logger = @default_logger
31
+
32
+
33
+ class << self
34
+
35
+ # @return [Array] the Array of objects that have had Configurability
36
+ # added to them
37
+ attr_accessor :configurable_objects
38
+
39
+ # @return [Logger::Formatter] the log formatter that will be used when the logging
40
+ # subsystem is reset
41
+ attr_accessor :default_log_formatter
42
+
43
+ # @return [Logger] the logger that will be used when the logging subsystem is reset
44
+ attr_accessor :default_logger
45
+
46
+ # @return [Logger] the logger that's currently in effect
47
+ attr_accessor :logger
48
+ alias_method :log, :logger
49
+ alias_method :log=, :logger=
50
+ end
51
+
52
+
53
+ ### Add configurability to the given +object+.
54
+ def self::extend_object( object )
55
+ self.log.debug "Adding Configurability to %p" % [ object ]
56
+ super
57
+ self.configurable_objects << object
58
+ end
59
+
60
+
61
+ ### Mixin hook: extend including classes instead
62
+ def self::included( mod )
63
+ mod.extend( self )
64
+ end
65
+
66
+
67
+ ### Try to generate a config key from the given object. If it responds_to #name,
68
+ ### the result will be stringified and stripped of non-word characters. If the
69
+ ### object itself doesn't have a name, the name of its class will be used instead.
70
+ def self::make_key_from_object( object )
71
+ if object.respond_to?( :name )
72
+ return object.name.sub( /.*::/, '' ).gsub( /\W+/, '_' ).downcase.to_sym
73
+ elsif object.class.name && !object.class.name.empty?
74
+ return object.class.name.sub( /.*::/, '' ).gsub( /\W+/, '_' ).downcase.to_sym
75
+ else
76
+ return :anonymous
77
+ end
78
+ end
79
+
80
+
81
+ ### Configure objects that have had Configurability added to them with
82
+ ### the sections of the specified +config+ that correspond to their
83
+ ### +config_key+. If the +config+ doesn't #respond_to the object's
84
+ ### +config_key+, the object's #configure method is called with +nil+
85
+ ### instead.
86
+ def self::configure_objects( config )
87
+ self.configurable_objects.each do |obj|
88
+ section = obj.config_key.to_sym
89
+ self.log.debug "Configuring %p with the %p section of the config." %
90
+ [ obj, section ]
91
+
92
+ if config.respond_to?( section )
93
+ self.log.debug " config has a %p method; using that" % [ section ]
94
+ obj.configure( config.send(section) )
95
+ elsif config.respond_to?( :[] )
96
+ self.log.debug " config has a an index operator method; using that"
97
+ obj.configure( config[section] || config[section.to_s] )
98
+ else
99
+ self.log.warn " don't know how to get the %p section of the config from %p" %
100
+ [ section, config ]
101
+ obj.configure( nil )
102
+ end
103
+ end
104
+ end
105
+
106
+
107
+ ### Reset the global logger object to the default
108
+ ### @return [void]
109
+ def self::reset_logger
110
+ self.logger = self.default_logger
111
+ self.logger.level = Logger::WARN
112
+ self.logger.formatter = self.default_log_formatter
113
+ end
114
+
115
+
116
+
117
+ #############################################################
118
+ ### A P P E N D E D M E T H O D S
119
+ #############################################################
120
+
121
+ ### The symbol which corresponds to the section of the configuration
122
+ ### used to configure the object.
123
+ attr_writer :config_key
124
+
125
+ ### Get (and optionally set) the +config_key+.
126
+ ### @param [Symbol] sym the config key
127
+ ### @return [Symbol] the config key
128
+ def config_key( sym=nil )
129
+ @config_key = sym unless sym.nil?
130
+ @config_key ||= Configurability.make_key_from_object( self )
131
+ @config_key
132
+ end
133
+
134
+
135
+ ### Set the config key of the object.
136
+ ### @param [Symbol] sym the config key
137
+ def config_key=( sym )
138
+ @config_key = sym
139
+ end
140
+
141
+
142
+ ### Default configuration method.
143
+ ### @param [Object] configuration section object
144
+ def configure( config )
145
+ @config = config
146
+ end
147
+
148
+
149
+ end # module Configurability
150
+
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'logger'
4
+
5
+ require 'configurability'
6
+
7
+ # A custom log-formatter class for
8
+ class Configurability::LogFormatter < Logger::Formatter
9
+
10
+ # The format to output unless debugging is turned on
11
+ DEFAULT_FORMAT = "[%1$s.%2$06d %3$d/%4$s] %5$5s -- %7$s\n"
12
+
13
+ # The format to output if debugging is turned on
14
+ DEFAULT_DEBUG_FORMAT = "[%1$s.%2$06d %3$d/%4$s] %5$5s {%6$s} -- %7$s\n"
15
+
16
+
17
+ ### Initialize the formatter with a reference to the logger so it can check for log level.
18
+ def initialize( logger, format=DEFAULT_FORMAT, debug=DEFAULT_DEBUG_FORMAT ) # :notnew:
19
+ @logger = logger
20
+ @format = format
21
+ @debug_format = debug
22
+
23
+ super()
24
+ end
25
+
26
+ ######
27
+ public
28
+ ######
29
+
30
+ # The Logger object associated with the formatter
31
+ attr_accessor :logger
32
+
33
+ # The logging format string
34
+ attr_accessor :format
35
+
36
+ # The logging format string that's used when outputting in debug mode
37
+ attr_accessor :debug_format
38
+
39
+
40
+ ### Log using either the DEBUG_FORMAT if the associated logger is at ::DEBUG level or
41
+ ### using FORMAT if it's anything less verbose.
42
+ def call( severity, time, progname, msg )
43
+ args = [
44
+ time.strftime( '%Y-%m-%d %H:%M:%S' ), # %1$s
45
+ time.usec, # %2$d
46
+ Process.pid, # %3$d
47
+ Thread.current == Thread.main ? 'main' : Thread.object_id, # %4$s
48
+ severity, # %5$s
49
+ progname, # %6$s
50
+ msg # %7$s
51
+ ]
52
+
53
+ if @logger.level == Logger::DEBUG
54
+ return self.debug_format % args
55
+ else
56
+ return self.format % args
57
+ end
58
+ end
59
+
60
+ end # class Configurability::LogFormatter
@@ -0,0 +1,26 @@
1
+ # 1.9.1 fixes
2
+
3
+
4
+ # Make Pathname compatible with 1.8.7 Pathname.
5
+ unless Pathname.instance_methods.include?( :=~ )
6
+ class Pathname
7
+ def self::glob( *args ) # :yield: p
8
+ args = args.collect {|p| p.to_s }
9
+ if block_given?
10
+ Dir.glob(*args) {|f| yield self.new(f) }
11
+ else
12
+ Dir.glob(*args).map {|f| self.new(f) }
13
+ end
14
+ end
15
+
16
+ def =~( other )
17
+ self.to_s =~ other
18
+ end
19
+
20
+ def to_str
21
+ self.to_s
22
+ end
23
+ end
24
+ end
25
+
26
+
@@ -0,0 +1,76 @@
1
+ #
2
+ # Dependency-checking and Installation Rake Tasks
3
+
4
+ #
5
+
6
+ require 'rubygems/dependency_installer'
7
+ require 'rubygems/source_index'
8
+ require 'rubygems/requirement'
9
+ require 'rubygems/doc_manager'
10
+
11
+ ### Install the specified +gems+ if they aren't already installed.
12
+ def install_gems( gems )
13
+
14
+ defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
15
+ :generate_rdoc => true,
16
+ :generate_ri => true,
17
+ :install_dir => Gem.dir,
18
+ :format_executable => false,
19
+ :test => false,
20
+ :version => Gem::Requirement.default,
21
+ })
22
+
23
+ # Check for root
24
+ if Process.euid != 0
25
+ $stderr.puts "This probably won't work, as you aren't root, but I'll try anyway"
26
+ end
27
+
28
+ gemindex = Gem::SourceIndex.from_installed_gems
29
+
30
+ gems.each do |gemname, reqstring|
31
+ requirement = Gem::Requirement.new( reqstring )
32
+ trace "requirement is: %p" % [ requirement ]
33
+
34
+ trace "Searching for an installed #{gemname}..."
35
+ specs = gemindex.find_name( gemname )
36
+ trace "...found %d specs: %s" %
37
+ [ specs.length, specs.collect {|s| "%s %s" % [s.name, s.version] }.join(', ') ]
38
+
39
+ if spec = specs.find {|spec| requirement.satisfied_by?(spec.version) }
40
+ log "Version %s of %s is already installed (needs %s); skipping..." %
41
+ [ spec.version, spec.name, requirement ]
42
+ next
43
+ end
44
+
45
+ rgv = Gem::Version.new( Gem::RubyGemsVersion )
46
+ installer = nil
47
+
48
+ log "Trying to install #{gemname.inspect} #{requirement}..."
49
+ if rgv >= Gem::Version.new( '1.1.1' )
50
+ installer = Gem::DependencyInstaller.new
51
+ installer.install( gemname, requirement )
52
+ else
53
+ installer = Gem::DependencyInstaller.new( gemname )
54
+ installer.install
55
+ end
56
+
57
+ installer.installed_gems.each do |spec|
58
+ log "Installed: %s" % [ spec.full_name ]
59
+ end
60
+
61
+ end
62
+ end
63
+
64
+
65
+ ### Task: install runtime dependencies
66
+ desc "Install runtime dependencies as gems"
67
+ task :install_dependencies do
68
+ install_gems( DEPENDENCIES )
69
+ end
70
+
71
+ ### Task: install gems for development tasks
72
+ desc "Install development dependencies as gems"
73
+ task :install_dev_dependencies do
74
+ install_gems( DEVELOPMENT_DEPENDENCIES )
75
+ end
76
+
@@ -0,0 +1,89 @@
1
+ #
2
+ # Documentation Rake tasks
3
+ #
4
+
5
+ require 'rake/clean'
6
+
7
+
8
+ # Append docs/lib to the load path if it exists for documentation
9
+ # helpers.
10
+ DOCSLIB = DOCSDIR + 'lib'
11
+ $LOAD_PATH.unshift( DOCSLIB.to_s ) if DOCSLIB.exist?
12
+
13
+ # Make relative string paths of all the stuff we need to generate docs for
14
+ DOCFILES = Rake::FileList[ LIB_FILES + EXT_FILES + GEMSPEC.extra_rdoc_files ]
15
+
16
+ # Documentation coverage constants
17
+ COVERAGE_DIR = BASEDIR + 'coverage'
18
+ COVERAGE_REPORT = COVERAGE_DIR + 'documentation.txt'
19
+
20
+
21
+ # Prefer YARD, fallback to RDoc
22
+ begin
23
+ require 'yard'
24
+ require 'yard/rake/yardoc_task'
25
+
26
+ # Undo the lazy-assed monkeypatch yard/globals.rb installs and
27
+ # re-install them as mixins as they should have been from the
28
+ # start
29
+ # <metamonkeypatch>
30
+ class Object
31
+ remove_method :log
32
+ remove_method :P
33
+ end
34
+
35
+ module YardGlobals
36
+ def P(namespace, name = nil)
37
+ namespace, name = nil, namespace if name.nil?
38
+ YARD::Registry.resolve(namespace, name, false, true)
39
+ end
40
+
41
+ def log
42
+ YARD::Logger.instance
43
+ end
44
+ end
45
+
46
+ class YARD::CLI::Base; include YardGlobals; end
47
+ class YARD::Parser::SourceParser; extend YardGlobals; include YardGlobals; end
48
+ class YARD::Parser::CParser; include YardGlobals; end
49
+ class YARD::CodeObjects::Base; include YardGlobals; end
50
+ class YARD::Handlers::Base; include YardGlobals; end
51
+ class YARD::Handlers::Processor; include YardGlobals; end
52
+ class YARD::Serializers::Base; include YardGlobals; end
53
+ class YARD::RegistryStore; include YardGlobals; end
54
+ class YARD::Docstring; include YardGlobals; end
55
+ module YARD::Templates::Helpers::ModuleHelper; include YardGlobals; end
56
+ # </metamonkeypatch>
57
+
58
+ YARD_OPTIONS = [] unless defined?( YARD_OPTIONS )
59
+
60
+ yardoctask = YARD::Rake::YardocTask.new( :apidocs ) do |task|
61
+ task.files = DOCFILES
62
+ task.options = YARD_OPTIONS
63
+ task.options << '--debug' << '--verbose' if $trace
64
+ end
65
+ yardoctask.before = lambda {
66
+ trace "Calling yardoc like:",
67
+ " yardoc %s" % [ quotelist(yardoctask.options + yardoctask.files).join(' ') ]
68
+ }
69
+
70
+ YARDOC_CACHE = BASEDIR + '.yardoc'
71
+ CLOBBER.include( YARDOC_CACHE.to_s )
72
+
73
+ rescue LoadError
74
+ require 'rdoc/task'
75
+
76
+ desc "Build API documentation in #{API_DOCSDIR}"
77
+ RDoc::Task.new( :apidocs ) do |task|
78
+ task.main = "README"
79
+ task.rdoc_files.include( DOCFILES )
80
+ task.rdoc_dir = API_DOCSDIR.to_s
81
+ task.options = RDOC_OPTIONS
82
+ end
83
+ end
84
+
85
+ # Need the DOCFILES to exist to build the API docs
86
+ task :apidocs => DOCFILES
87
+
88
+ CLEAN.include( API_DOCSDIR.to_s )
89
+
data/rake/helpers.rb ADDED
@@ -0,0 +1,502 @@
1
+ # encoding: utf-8
2
+
3
+ #####################################################################
4
+ ### G L O B A L H E L P E R F U N C T I O N S
5
+ #####################################################################
6
+
7
+
8
+ require 'pathname'
9
+ require 'uri'
10
+
11
+ begin
12
+ require 'readline'
13
+ include Readline
14
+ rescue LoadError
15
+ # Fall back to a plain prompt
16
+ def readline( text )
17
+ $stderr.print( text.chomp )
18
+ return $stdin.gets
19
+ end
20
+ end
21
+
22
+ module RakefileHelpers
23
+ # Set some ANSI escape code constants (Shamelessly stolen from Perl's
24
+ # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
25
+ ANSI_ATTRIBUTES = {
26
+ 'clear' => 0,
27
+ 'reset' => 0,
28
+ 'bold' => 1,
29
+ 'dark' => 2,
30
+ 'underline' => 4,
31
+ 'underscore' => 4,
32
+ 'blink' => 5,
33
+ 'reverse' => 7,
34
+ 'concealed' => 8,
35
+
36
+ 'black' => 30, 'on_black' => 40,
37
+ 'red' => 31, 'on_red' => 41,
38
+ 'green' => 32, 'on_green' => 42,
39
+ 'yellow' => 33, 'on_yellow' => 43,
40
+ 'blue' => 34, 'on_blue' => 44,
41
+ 'magenta' => 35, 'on_magenta' => 45,
42
+ 'cyan' => 36, 'on_cyan' => 46,
43
+ 'white' => 37, 'on_white' => 47
44
+ }
45
+
46
+
47
+ MULTILINE_PROMPT = <<-'EOF'
48
+ Enter one or more values for '%s'.
49
+ A blank line finishes input.
50
+ EOF
51
+
52
+
53
+ CLEAR_TO_EOL = "\e[K"
54
+ CLEAR_CURRENT_LINE = "\e[2K"
55
+
56
+
57
+ TAR_OPTS = { :compression => :gzip }
58
+
59
+
60
+ ###############
61
+ module_function
62
+ ###############
63
+
64
+ ### Output a logging message
65
+ def log( *msg )
66
+ output = colorize( msg.flatten.join(' '), 'cyan' )
67
+ $stderr.puts( output )
68
+ end
69
+
70
+
71
+ ### Output a logging message if tracing is on
72
+ def trace( *msg )
73
+ return unless $trace
74
+ output = colorize( msg.flatten.join(' '), 'yellow' )
75
+ $stderr.puts( output )
76
+ end
77
+
78
+
79
+ ### Return the specified args as a string, quoting any that have a space.
80
+ def quotelist( *args )
81
+ return args.flatten.collect {|part| part =~ /\s/ ? part.inspect : part}
82
+ end
83
+
84
+
85
+ ### Run the specified command +cmd+ with system(), failing if the execution
86
+ ### fails.
87
+ def run( *cmd )
88
+ cmd.flatten!
89
+
90
+ if cmd.length > 1
91
+ trace( quotelist(*cmd) )
92
+ else
93
+ trace( cmd )
94
+ end
95
+
96
+ if $dryrun
97
+ $stderr.puts "(dry run mode)"
98
+ else
99
+ system( *cmd )
100
+ unless $?.success?
101
+ fail "Command failed: [%s]" % [cmd.join(' ')]
102
+ end
103
+ end
104
+ end
105
+
106
+
107
+ ### Run the given +cmd+ with the specified +args+ without interpolation by the shell and
108
+ ### return anything written to its STDOUT.
109
+ def read_command_output( cmd, *args )
110
+ trace "Reading output from: %s" % [ cmd, quotelist(cmd, *args) ]
111
+ output = IO.read( '|-' ) or exec cmd, *args
112
+ return output
113
+ end
114
+
115
+
116
+ ### Run a subordinate Rake process with the same options and the specified +targets+.
117
+ def rake( *targets )
118
+ opts = ARGV.select {|arg| arg[0,1] == '-' }
119
+ args = opts + targets.map {|t| t.to_s }
120
+ run 'rake', '-N', *args
121
+ end
122
+
123
+
124
+ ### Open a pipe to a process running the given +cmd+ and call the given block with it.
125
+ def pipeto( *cmd )
126
+ $DEBUG = true
127
+
128
+ cmd.flatten!
129
+ log( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
130
+ if $dryrun
131
+ $stderr.puts "(dry run mode)"
132
+ else
133
+ open( '|-', 'w+' ) do |io|
134
+
135
+ # Parent
136
+ if io
137
+ yield( io )
138
+
139
+ # Child
140
+ else
141
+ exec( *cmd )
142
+ fail "Command failed: [%s]" % [cmd.join(' ')]
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+
149
+ ### Download the file at +sourceuri+ via HTTP and write it to +targetfile+.
150
+ def download( sourceuri, targetfile=nil )
151
+ oldsync = $stdout.sync
152
+ $stdout.sync = true
153
+ require 'open-uri'
154
+
155
+ targetpath = Pathname.new( targetfile )
156
+
157
+ log "Downloading %s to %s" % [sourceuri, targetfile]
158
+ trace " connecting..."
159
+ ifh = open( sourceuri ) do |ifh|
160
+ trace " connected..."
161
+ targetpath.open( File::WRONLY|File::TRUNC|File::CREAT, 0644 ) do |ofh|
162
+ log "Downloading..."
163
+ buf = ''
164
+
165
+ while ifh.read( 16384, buf )
166
+ until buf.empty?
167
+ bytes = ofh.write( buf )
168
+ buf.slice!( 0, bytes )
169
+ end
170
+ end
171
+
172
+ log "Done."
173
+ end
174
+
175
+ end
176
+
177
+ return targetpath
178
+ ensure
179
+ $stdout.sync = oldsync
180
+ end
181
+
182
+
183
+ ### Return the fully-qualified path to the specified +program+ in the PATH.
184
+ def which( program )
185
+ return nil unless ENV['PATH']
186
+ ENV['PATH'].split(/:/).
187
+ collect {|dir| Pathname.new(dir) + program }.
188
+ find {|path| path.exist? && path.executable? }
189
+ end
190
+
191
+
192
+ ### Create a string that contains the ANSI codes specified and return it
193
+ def ansi_code( *attributes )
194
+ attributes.flatten!
195
+ attributes.collect! {|at| at.to_s }
196
+ # $stderr.puts "Returning ansicode for TERM = %p: %p" %
197
+ # [ ENV['TERM'], attributes ]
198
+ return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
199
+ attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
200
+
201
+ # $stderr.puts " attr is: %p" % [attributes]
202
+ if attributes.empty?
203
+ return ''
204
+ else
205
+ return "\e[%sm" % attributes
206
+ end
207
+ end
208
+
209
+
210
+ ### Colorize the given +string+ with the specified +attributes+ and return it, handling
211
+ ### line-endings, color reset, etc.
212
+ def colorize( *args )
213
+ string = ''
214
+
215
+ if block_given?
216
+ string = yield
217
+ else
218
+ string = args.shift
219
+ end
220
+
221
+ ending = string[/(\s)$/] || ''
222
+ string = string.rstrip
223
+
224
+ return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
225
+ end
226
+
227
+
228
+ ### Output the specified <tt>msg</tt> as an ANSI-colored error message
229
+ ### (white on red).
230
+ def error_message( msg, details='' )
231
+ $stderr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + details
232
+ end
233
+ alias :error :error_message
234
+
235
+
236
+ ### Highlight and embed a prompt control character in the given +string+ and return it.
237
+ def make_prompt_string( string )
238
+ return CLEAR_CURRENT_LINE + colorize( 'bold', 'green' ) { string + ' ' }
239
+ end
240
+
241
+
242
+ ### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
243
+ ### return the user's input with leading and trailing spaces removed. If a
244
+ ### test is provided, the prompt will repeat until the test returns true.
245
+ ### An optional failure message can also be passed in.
246
+ def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
247
+ prompt_string.chomp!
248
+ prompt_string << ":" unless /\W$/.match( prompt_string )
249
+ response = nil
250
+
251
+ begin
252
+ prompt = make_prompt_string( prompt_string )
253
+ response = readline( prompt ) || ''
254
+ response.strip!
255
+ if block_given? && ! yield( response )
256
+ error_message( failure_msg + "\n\n" )
257
+ response = nil
258
+ end
259
+ end while response.nil?
260
+
261
+ return response
262
+ end
263
+
264
+
265
+ ### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
266
+ ### substituting the given <tt>default</tt> if the user doesn't input
267
+ ### anything. If a test is provided, the prompt will repeat until the test
268
+ ### returns true. An optional failure message can also be passed in.
269
+ def prompt_with_default( prompt_string, default, failure_msg="Try again." )
270
+ response = nil
271
+
272
+ begin
273
+ default ||= '~'
274
+ response = prompt( "%s [%s]" % [ prompt_string, default ] )
275
+ response = default.to_s if !response.nil? && response.empty?
276
+
277
+ trace "Validating response %p" % [ response ]
278
+
279
+ # the block is a validator. We need to make sure that the user didn't
280
+ # enter '~', because if they did, it's nil and we should move on. If
281
+ # they didn't, then call the block.
282
+ if block_given? && response != '~' && ! yield( response )
283
+ error_message( failure_msg + "\n\n" )
284
+ response = nil
285
+ end
286
+ end while response.nil?
287
+
288
+ return nil if response == '~'
289
+ return response
290
+ end
291
+
292
+
293
+ ### Prompt for an array of values
294
+ def prompt_for_multiple_values( label, default=nil )
295
+ $stderr.puts( MULTILINE_PROMPT % [label] )
296
+ if default
297
+ $stderr.puts "Enter a single blank line to keep the default:\n %p" % [ default ]
298
+ end
299
+
300
+ results = []
301
+ result = nil
302
+
303
+ begin
304
+ result = readline( make_prompt_string("> ") )
305
+ if result.nil? || result.empty?
306
+ results << default if default && results.empty?
307
+ else
308
+ results << result
309
+ end
310
+ end until result.nil? || result.empty?
311
+
312
+ return results.flatten
313
+ end
314
+
315
+
316
+ ### Turn echo and masking of input on/off.
317
+ def noecho( masked=false )
318
+ require 'termios'
319
+
320
+ rval = nil
321
+ term = Termios.getattr( $stdin )
322
+
323
+ begin
324
+ newt = term.dup
325
+ newt.c_lflag &= ~Termios::ECHO
326
+ newt.c_lflag &= ~Termios::ICANON if masked
327
+
328
+ Termios.tcsetattr( $stdin, Termios::TCSANOW, newt )
329
+
330
+ rval = yield
331
+ ensure
332
+ Termios.tcsetattr( $stdin, Termios::TCSANOW, term )
333
+ end
334
+
335
+ return rval
336
+ end
337
+
338
+
339
+ ### Prompt the user for her password, turning off echo if the 'termios' module is
340
+ ### available.
341
+ def prompt_for_password( prompt="Password: " )
342
+ return noecho( true ) do
343
+ $stderr.print( prompt )
344
+ ($stdin.gets || '').chomp
345
+ end
346
+ end
347
+
348
+
349
+ ### Display a description of a potentially-dangerous task, and prompt
350
+ ### for confirmation. If the user answers with anything that begins
351
+ ### with 'y', yield to the block. If +abort_on_decline+ is +true+,
352
+ ### any non-'y' answer will fail with an error message.
353
+ def ask_for_confirmation( description, abort_on_decline=true )
354
+ puts description
355
+
356
+ answer = prompt_with_default( "Continue?", 'n' ) do |input|
357
+ input =~ /^[yn]/i
358
+ end
359
+
360
+ if answer =~ /^y/i
361
+ return yield
362
+ elsif abort_on_decline
363
+ error "Aborted."
364
+ fail
365
+ end
366
+
367
+ return false
368
+ end
369
+ alias :prompt_for_confirmation :ask_for_confirmation
370
+
371
+
372
+ ### Search line-by-line in the specified +file+ for the given +regexp+, returning the
373
+ ### first match, or nil if no match was found. If the +regexp+ has any capture groups,
374
+ ### those will be returned in an Array, else the whole matching line is returned.
375
+ def find_pattern_in_file( regexp, file )
376
+ rval = nil
377
+
378
+ File.open( file, 'r' ).each do |line|
379
+ if (( match = regexp.match(line) ))
380
+ rval = match.captures.empty? ? match[0] : match.captures
381
+ break
382
+ end
383
+ end
384
+
385
+ return rval
386
+ end
387
+
388
+
389
+ ### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
390
+ ### returning the first match, or nil if no match was found. If the +regexp+ has any
391
+ ### capture groups, those will be returned in an Array, else the whole matching line
392
+ ### is returned.
393
+ def find_pattern_in_pipe( regexp, *cmd )
394
+ require 'open3'
395
+ output = []
396
+
397
+ log( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
398
+ Open3.popen3( *cmd ) do |stdin, stdout, stderr|
399
+ stdin.close
400
+
401
+ output << stdout.gets until stdout.eof?
402
+ output << stderr.gets until stderr.eof?
403
+ end
404
+
405
+ result = output.find { |line| regexp.match(line) }
406
+ return $1 || result
407
+ end
408
+
409
+
410
+ ### Invoke the user's editor on the given +filename+ and return the exit code
411
+ ### from doing so.
412
+ def edit( filename )
413
+ editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
414
+ system editor, filename
415
+ unless $?.success? || editor =~ /vim/i
416
+ fail "Editor exited uncleanly."
417
+ end
418
+ end
419
+
420
+
421
+ ### Extract all the non Rake-target arguments from ARGV and return them.
422
+ def get_target_args
423
+ args = ARGV.reject {|arg| arg =~ /^-/ || Rake::Task.task_defined?(arg) }
424
+ return args
425
+ end
426
+
427
+
428
+ ### Log a subdirectory change, execute a block, and exit the subdirectory
429
+ def in_subdirectory( subdir )
430
+ block = Proc.new
431
+
432
+ log "Entering #{subdir}"
433
+ Dir.chdir( subdir, &block )
434
+ log "Leaving #{subdir}"
435
+ end
436
+
437
+
438
+ ### Make an easily-comparable version vector out of +ver+ and return it.
439
+ def vvec( ver )
440
+ return ver.split('.').collect {|char| char.to_i }.pack('N*')
441
+ end
442
+
443
+
444
+ ### Archive::Tar::Reader#extract (as of 0.9.0) is broken w.r.t.
445
+ ### permissions, so we have to do this ourselves.
446
+ def untar( tarfile, targetdir )
447
+ require 'archive/tar'
448
+ targetdir = Pathname( targetdir )
449
+ raise "No such directory: #{targetdir}" unless targetdir.directory?
450
+
451
+ reader = Archive::Tar::Reader.new( tarfile.to_s, TAR_OPTS )
452
+
453
+ mkdir_p( targetdir )
454
+ reader.each( true ) do |header, body|
455
+ path = targetdir + header[:path]
456
+ # trace "Header is: %p" % [ header ]
457
+
458
+ case header[:type]
459
+ when :file
460
+ trace " #{path}"
461
+ path.open( File::WRONLY|File::EXCL|File::CREAT|File::TRUNC, header[:mode] ) do |fio|
462
+ bytesize = header[:size]
463
+ fio.write( body[0,bytesize] )
464
+ end
465
+
466
+ when :directory
467
+ trace " #{path}"
468
+ path.mkpath
469
+
470
+ when :link
471
+ linktarget = targetdir + header[:dest]
472
+ trace " #{path} => #{linktarget}"
473
+ path.make_link( linktarget.to_s )
474
+
475
+ when :symlink
476
+ linktarget = targetdir + header[:dest]
477
+ trace " #{path} -> #{linktarget}"
478
+ path.make_symlink( linktarget )
479
+ end
480
+ end
481
+
482
+ end
483
+
484
+
485
+ ### Extract the contents of the specified +zipfile+ into the given +targetdir+.
486
+ def unzip( zipfile, targetdir, *files )
487
+ require 'zip/zip'
488
+ targetdir = Pathname( targetdir )
489
+ raise "No such directory: #{targetdir}" unless targetdir.directory?
490
+
491
+ Zip::ZipFile.foreach( zipfile ) do |entry|
492
+ # trace " entry is: %p" % [ entry ]
493
+ next unless files.empty? || files.include?( entry.name )
494
+ target_path = targetdir + entry.name
495
+ # trace " would extract to: %s" % [ target_path ]
496
+ entry.extract( target_path ) { true }
497
+ end
498
+ end
499
+
500
+ end # module Rakefile::Helpers
501
+
502
+