|
1 ##################################################################### |
|
2 ### G L O B A L H E L P E R F U N C T I O N S |
|
3 ##################################################################### |
|
4 |
|
5 require 'pathname' |
|
6 require 'readline' |
|
7 require 'open3' |
|
8 |
|
9 # Set some ANSI escape code constants (Shamelessly stolen from Perl's |
|
10 # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com> |
|
11 ANSI_ATTRIBUTES = { |
|
12 'clear' => 0, |
|
13 'reset' => 0, |
|
14 'bold' => 1, |
|
15 'dark' => 2, |
|
16 'underline' => 4, |
|
17 'underscore' => 4, |
|
18 'blink' => 5, |
|
19 'reverse' => 7, |
|
20 'concealed' => 8, |
|
21 |
|
22 'black' => 30, 'on_black' => 40, |
|
23 'red' => 31, 'on_red' => 41, |
|
24 'green' => 32, 'on_green' => 42, |
|
25 'yellow' => 33, 'on_yellow' => 43, |
|
26 'blue' => 34, 'on_blue' => 44, |
|
27 'magenta' => 35, 'on_magenta' => 45, |
|
28 'cyan' => 36, 'on_cyan' => 46, |
|
29 'white' => 37, 'on_white' => 47 |
|
30 } |
|
31 |
|
32 |
|
33 CLEAR_TO_EOL = "\e[K" |
|
34 CLEAR_CURRENT_LINE = "\e[2K" |
|
35 |
|
36 |
|
37 ### Output a logging message |
|
38 def log( *msg ) |
|
39 output = colorize( msg.flatten.join(' '), 'cyan' ) |
|
40 $deferr.puts( output ) |
|
41 end |
|
42 |
|
43 |
|
44 ### Output a logging message if tracing is on |
|
45 def trace( *msg ) |
|
46 return unless $trace |
|
47 output = colorize( msg.flatten.join(' '), 'yellow' ) |
|
48 $deferr.puts( output ) |
|
49 end |
|
50 |
|
51 |
|
52 ### Run the specified command +cmd+ with system(), failing if the execution |
|
53 ### fails. |
|
54 def run( *cmd ) |
|
55 cmd.flatten! |
|
56 |
|
57 log( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) |
|
58 if $dryrun |
|
59 $deferr.puts "(dry run mode)" |
|
60 else |
|
61 system( *cmd ) |
|
62 unless $?.success? |
|
63 fail "Command failed: [%s]" % [cmd.join(' ')] |
|
64 end |
|
65 end |
|
66 end |
|
67 |
|
68 |
|
69 ### Open a pipe to a process running the given +cmd+ and call the given block with it. |
|
70 def pipeto( *cmd ) |
|
71 $DEBUG = true |
|
72 |
|
73 cmd.flatten! |
|
74 log( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) |
|
75 if $dryrun |
|
76 $deferr.puts "(dry run mode)" |
|
77 else |
|
78 open( '|-', 'w+' ) do |io| |
|
79 |
|
80 # Parent |
|
81 if io |
|
82 yield( io ) |
|
83 |
|
84 # Child |
|
85 else |
|
86 exec( *cmd ) |
|
87 fail "Command failed: [%s]" % [cmd.join(' ')] |
|
88 end |
|
89 end |
|
90 end |
|
91 end |
|
92 |
|
93 |
|
94 ### Download the file at +sourceuri+ via HTTP and write it to +targetfile+. |
|
95 def download( sourceuri, targetfile ) |
|
96 oldsync = $defout.sync |
|
97 $defout.sync = true |
|
98 require 'net/http' |
|
99 require 'uri' |
|
100 |
|
101 targetpath = Pathname.new( targetfile ) |
|
102 |
|
103 log "Downloading %s to %s" % [sourceuri, targetfile] |
|
104 targetpath.open( File::WRONLY|File::TRUNC|File::CREAT, 0644 ) do |ofh| |
|
105 |
|
106 url = URI.parse( sourceuri ) |
|
107 downloaded = false |
|
108 limit = 5 |
|
109 |
|
110 until downloaded or limit.zero? |
|
111 Net::HTTP.start( url.host, url.port ) do |http| |
|
112 req = Net::HTTP::Get.new( url.path ) |
|
113 |
|
114 http.request( req ) do |res| |
|
115 if res.is_a?( Net::HTTPSuccess ) |
|
116 log "Downloading..." |
|
117 res.read_body do |buf| |
|
118 ofh.print( buf ) |
|
119 end |
|
120 downloaded = true |
|
121 puts "done." |
|
122 |
|
123 elsif res.is_a?( Net::HTTPRedirection ) |
|
124 url = URI.parse( res['location'] ) |
|
125 log "...following redirection to: %s" % [ url ] |
|
126 limit -= 1 |
|
127 sleep 0.2 |
|
128 next |
|
129 |
|
130 else |
|
131 res.error! |
|
132 end |
|
133 end |
|
134 end |
|
135 end |
|
136 end |
|
137 |
|
138 return targetpath |
|
139 ensure |
|
140 $defout.sync = oldsync |
|
141 end |
|
142 |
|
143 |
|
144 ### Return the fully-qualified path to the specified +program+ in the PATH. |
|
145 def which( program ) |
|
146 ENV['PATH'].split(/:/). |
|
147 collect {|dir| Pathname.new(dir) + program }. |
|
148 find {|path| path.exist? && path.executable? } |
|
149 end |
|
150 |
|
151 |
|
152 ### Create a string that contains the ANSI codes specified and return it |
|
153 def ansi_code( *attributes ) |
|
154 attributes.flatten! |
|
155 attributes.collect! {|at| at.to_s } |
|
156 # $deferr.puts "Returning ansicode for TERM = %p: %p" % |
|
157 # [ ENV['TERM'], attributes ] |
|
158 return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM'] |
|
159 attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';') |
|
160 |
|
161 # $deferr.puts " attr is: %p" % [attributes] |
|
162 if attributes.empty? |
|
163 return '' |
|
164 else |
|
165 return "\e[%sm" % attributes |
|
166 end |
|
167 end |
|
168 |
|
169 |
|
170 ### Colorize the given +string+ with the specified +attributes+ and return it, handling |
|
171 ### line-endings, color reset, etc. |
|
172 def colorize( *args ) |
|
173 string = '' |
|
174 |
|
175 if block_given? |
|
176 string = yield |
|
177 else |
|
178 string = args.shift |
|
179 end |
|
180 |
|
181 ending = string[/(\s)$/] || '' |
|
182 string = string.rstrip |
|
183 |
|
184 return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending |
|
185 end |
|
186 |
|
187 |
|
188 ### Output the specified <tt>msg</tt> as an ANSI-colored error message |
|
189 ### (white on red). |
|
190 def error_message( msg, details='' ) |
|
191 $deferr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + details |
|
192 end |
|
193 alias :error :error_message |
|
194 |
|
195 |
|
196 ### Highlight and embed a prompt control character in the given +string+ and return it. |
|
197 def make_prompt_string( string ) |
|
198 return CLEAR_CURRENT_LINE + colorize( 'bold', 'green' ) { string + ' ' } |
|
199 end |
|
200 |
|
201 |
|
202 ### Output the specified <tt>prompt_string</tt> as a prompt (in green) and |
|
203 ### return the user's input with leading and trailing spaces removed. If a |
|
204 ### test is provided, the prompt will repeat until the test returns true. |
|
205 ### An optional failure message can also be passed in. |
|
206 def prompt( prompt_string, failure_msg="Try again." ) # :yields: response |
|
207 prompt_string.chomp! |
|
208 prompt_string << ":" unless /\W$/.match( prompt_string ) |
|
209 response = nil |
|
210 |
|
211 begin |
|
212 prompt = make_prompt_string( prompt_string ) |
|
213 response = Readline.readline( prompt ) || '' |
|
214 response.strip! |
|
215 if block_given? && ! yield( response ) |
|
216 error_message( failure_msg + "\n\n" ) |
|
217 response = nil |
|
218 end |
|
219 end while response.nil? |
|
220 |
|
221 return response |
|
222 end |
|
223 |
|
224 |
|
225 ### Prompt the user with the given <tt>prompt_string</tt> via #prompt, |
|
226 ### substituting the given <tt>default</tt> if the user doesn't input |
|
227 ### anything. If a test is provided, the prompt will repeat until the test |
|
228 ### returns true. An optional failure message can also be passed in. |
|
229 def prompt_with_default( prompt_string, default, failure_msg="Try again." ) |
|
230 response = nil |
|
231 |
|
232 begin |
|
233 response = prompt( "%s [%s]" % [ prompt_string, default ] ) |
|
234 response = default if response.empty? |
|
235 |
|
236 if block_given? && ! yield( response ) |
|
237 error_message( failure_msg + "\n\n" ) |
|
238 response = nil |
|
239 end |
|
240 end while response.nil? |
|
241 |
|
242 return response |
|
243 end |
|
244 |
|
245 |
|
246 ### Display a description of a potentially-dangerous task, and prompt |
|
247 ### for confirmation. If the user answers with anything that begins |
|
248 ### with 'y', yield to the block, else raise with an error. |
|
249 def ask_for_confirmation( description ) |
|
250 puts description |
|
251 |
|
252 answer = prompt_with_default( "Continue?", 'n' ) do |input| |
|
253 input =~ /^[yn]/i |
|
254 end |
|
255 |
|
256 case answer |
|
257 when /^y/i |
|
258 yield |
|
259 else |
|
260 error "Aborted." |
|
261 fail |
|
262 end |
|
263 end |
|
264 |
|
265 |
|
266 ### Search line-by-line in the specified +file+ for the given +regexp+, returning the |
|
267 ### first match, or nil if no match was found. If the +regexp+ has any capture groups, |
|
268 ### those will be returned in an Array, else the whole matching line is returned. |
|
269 def find_pattern_in_file( regexp, file ) |
|
270 rval = nil |
|
271 |
|
272 File.open( file, 'r' ).each do |line| |
|
273 if (( match = regexp.match(line) )) |
|
274 rval = match.captures.empty? ? match[0] : match.captures |
|
275 break |
|
276 end |
|
277 end |
|
278 |
|
279 return rval |
|
280 end |
|
281 |
|
282 |
|
283 ### Search line-by-line in the output of the specified +cmd+ for the given +regexp+, |
|
284 ### returning the first match, or nil if no match was found. If the +regexp+ has any |
|
285 ### capture groups, those will be returned in an Array, else the whole matching line |
|
286 ### is returned. |
|
287 def find_pattern_in_pipe( regexp, *cmd ) |
|
288 output = [] |
|
289 |
|
290 Open3.popen3( *cmd ) do |stdin, stdout, stderr| |
|
291 stdin.close |
|
292 |
|
293 output << stdout.gets until stdout.eof? |
|
294 output << stderr.gets until stderr.eof? |
|
295 end |
|
296 |
|
297 result = output.find { |line| regexp.match(line) } |
|
298 return $1 || result |
|
299 end |
|
300 |
|
301 |
|
302 ### Extract all the non Rake-target arguments from ARGV and return them. |
|
303 def get_target_args |
|
304 args = ARGV.reject {|arg| Rake::Task.task_defined?(arg) } |
|
305 return args |
|
306 end |
|
307 |
|
308 |
|
309 |
|
310 require 'rubygems/dependency_installer' |
|
311 require 'rubygems/source_index' |
|
312 require 'rubygems/requirement' |
|
313 require 'rubygems/doc_manager' |
|
314 |
|
315 ### Install the specified +gems+ if they aren't already installed. |
|
316 def install_gems( *gems ) |
|
317 gems.flatten! |
|
318 |
|
319 defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({ |
|
320 :generate_rdoc => true, |
|
321 :generate_ri => true, |
|
322 :install_dir => Gem.dir, |
|
323 :format_executable => false, |
|
324 :test => false, |
|
325 :version => Gem::Requirement.default, |
|
326 }) |
|
327 |
|
328 # Check for root |
|
329 if Process.euid != 0 |
|
330 $stderr.puts "This probably won't work, as you aren't root, but I'll try anyway" |
|
331 end |
|
332 |
|
333 gemindex = Gem::SourceIndex.from_installed_gems |
|
334 |
|
335 gems.each do |gemname| |
|
336 if (( specs = gemindex.search(gemname) )) && ! specs.empty? |
|
337 log "Version %s of %s is already installed; skipping..." % |
|
338 [ specs.first.version, specs.first.name ] |
|
339 next |
|
340 end |
|
341 |
|
342 log "Trying to install #{gemname.inspect}..." |
|
343 installer = Gem::DependencyInstaller.new |
|
344 installer.install( gemname ) |
|
345 |
|
346 installer.installed_gems.each do |spec| |
|
347 log "Installed: %s" % [ spec.full_name ] |
|
348 end |
|
349 |
|
350 end |
|
351 end |
|
352 |
|
353 |