]> git.cworth.org Git - sup/blob - lib/sup/util.rb
fix email tab completion (i think!)
[sup] / lib / sup / util.rb
1 require 'lockfile'
2 require 'mime/types'
3 require 'pathname'
4
5 ## time for some monkeypatching!
6 class Lockfile
7   def gen_lock_id
8     Hash[
9          'host' => "#{ Socket.gethostname }",
10          'pid' => "#{ Process.pid }",
11          'ppid' => "#{ Process.ppid }",
12          'time' => timestamp,
13          'pname' => $0,
14          'user' => ENV["USER"]
15         ]
16   end
17
18   def dump_lock_id lock_id = @lock_id
19       "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
20         lock_id.values_at('host','pid','ppid','time','user', 'pname')
21     end
22
23   def lockinfo_on_disk
24     h = load_lock_id IO.read(path)
25     h['mtime'] = File.mtime path
26     h
27   end
28
29   def touch_yourself; touch path end
30 end
31
32 class Pathname
33   def human_size
34     s =
35       begin
36         size
37       rescue SystemCallError
38         return "?"
39       end
40
41     if s < 1024
42       s.to_s + "b"
43     elsif s < (1024 * 1024)
44       (s / 1024).to_s + "k"
45     elsif s < (1024 * 1024 * 1024)
46       (s / 1024 / 1024).to_s + "m"
47     else
48       (s / 1024 / 1024 / 1024).to_s + "g"
49     end
50   end
51
52   def human_time
53     begin
54       ctime.strftime("%Y-%m-%d %H:%M")
55     rescue SystemCallError
56       "?"
57     end
58   end
59 end
60
61 ## more monkeypatching!
62 module RMail
63   class EncodingUnsupportedError < StandardError; end
64
65   class Message
66     def add_file_attachment fn
67       bfn = File.basename fn
68       a = Message.new
69       t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
70
71       a.header.add "Content-Disposition", "attachment; filename=#{bfn.to_s.inspect}"
72       a.header.add "Content-Type", "#{t.content_type}; name=#{bfn.to_s.inspect}"
73       a.header.add "Content-Transfer-Encoding", t.encoding
74       a.body =
75         case t.encoding
76         when "base64"
77           [IO.read(fn)].pack "m"
78         when "quoted-printable"
79           [IO.read(fn)].pack "M"
80         when "7bit", "8bit"
81           IO.read(fn)
82         else
83           raise EncodingUnsupportedError, t.encoding
84         end
85
86       add_part a
87     end
88
89     def charset
90       if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/
91         $1
92       end
93     end
94   end
95 end
96
97 class Range
98   ## only valid for integer ranges (unless I guess it's exclusive)
99   def size 
100     last - first + (exclude_end? ? 0 : 1)
101   end
102 end
103
104 class Module
105   def bool_reader *args
106     args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
107   end
108   def bool_writer *args; attr_writer(*args); end
109   def bool_accessor *args
110     bool_reader(*args)
111     bool_writer(*args)
112   end
113
114   def defer_all_other_method_calls_to obj
115     class_eval %{
116       def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
117       def respond_to? meth; @#{obj}.respond_to?(meth); end
118     }
119   end
120 end
121
122 class Object
123   def ancestors
124     ret = []
125     klass = self.class
126
127     until klass == Object
128       ret << klass
129       klass = klass.superclass
130     end
131     ret
132   end
133
134   ## "k combinator"
135   def returning x; yield x; x; end
136
137   ## clone of java-style whole-method synchronization
138   ## assumes a @mutex variable
139   ## TODO: clean up, try harder to avoid namespace collisions
140   def synchronized *meth
141     meth.each do
142       class_eval <<-EOF
143         alias unsynchronized_#{meth} #{meth}
144         def #{meth}(*a, &b)
145           @mutex.synchronize { unsynchronized_#{meth}(*a, &b) }
146         end
147       EOF
148     end
149   end
150
151   def ignore_concurrent_calls *meth
152     meth.each do
153       mutex = "@__concurrent_protector_#{meth}"
154       flag = "@__concurrent_flag_#{meth}"
155       oldmeth = "__unprotected_#{meth}"
156       class_eval <<-EOF
157         alias #{oldmeth} #{meth}
158         def #{meth}(*a, &b)
159           #{mutex} = Mutex.new unless defined? #{mutex}
160           #{flag} = true unless defined? #{flag}
161           run = #{mutex}.synchronize do
162             if #{flag}
163               #{flag} = false
164               true
165             end
166           end
167           if run
168             ret = #{oldmeth}(*a, &b)
169             #{mutex}.synchronize { #{flag} = true }
170             ret
171           end
172         end
173       EOF
174     end
175   end
176 end
177
178 class String
179   def camel_to_hyphy
180     self.gsub(/([a-z])([A-Z0-9])/, '\1-\2').downcase
181   end
182
183   def find_all_positions x
184     ret = []
185     start = 0
186     while start < length
187       pos = index x, start
188       break if pos.nil?
189       ret << pos
190       start = pos + 1
191     end
192     ret
193   end
194
195   ## one of the few things i miss from perl
196   def ucfirst
197     self[0 .. 0].upcase + self[1 .. -1]
198   end
199
200   ## a very complicated regex found on teh internets to split on
201   ## commas, unless they occurr within double quotes.
202   def split_on_commas
203     split(/,\s*(?=(?:[^"]*"[^"]*")*(?![^"]*"))/)
204   end
205
206   ## ok, here we do it the hard way. got to have a remainder for purposes of
207   ## tab-completing full email addresses
208   def split_on_commas_with_remainder
209     ret = []
210     state = :outstring
211     pos = 0
212     region_start = 0
213     while pos <= length
214       newpos = case state
215         when :escaped_instring, :escaped_outstring: pos
216         else index(/[,"\\]/, pos)
217       end 
218       
219       if newpos
220         char = self[newpos]
221       else
222         char = nil
223         newpos = length
224       end
225
226       case char
227       when ?"
228         state = case state
229           when :outstring: :instring
230           when :instring: :outstring
231           when :escaped_instring: :instring
232           when :escaped_outstring: :outstring
233         end
234       when ?,, nil
235         state = case state
236           when :outstring, :escaped_outstring:
237             ret << self[region_start ... newpos].gsub(/^\s+|\s+$/, "")
238             region_start = newpos + 1
239             :outstring
240           when :instring: :instring
241           when :escaped_instring: :instring
242         end
243       when ?\\
244         state = case state
245           when :instring: :escaped_instring
246           when :outstring: :escaped_outstring
247           when :escaped_instring: :instring
248           when :escaped_outstring: :outstring
249         end
250       end
251       pos = newpos + 1
252     end
253
254     remainder = case state
255       when :instring
256         self[region_start .. -1].gsub(/^\s+/, "")
257       else
258         nil
259       end
260
261     [ret, remainder]
262   end
263
264   def wrap len
265     ret = []
266     s = self
267     while s.length > len
268       cut = s[0 ... len].rindex(/\s/)
269       if cut
270         ret << s[0 ... cut]
271         s = s[(cut + 1) .. -1]
272       else
273         ret << s[0 ... len]
274         s = s[len .. -1]
275       end
276     end
277     ret << s
278   end
279
280   def normalize_whitespace
281     gsub(/\t/, "    ").gsub(/\r/, "")
282   end
283 end
284
285 class Numeric
286   def clamp min, max
287     if self < min
288       min
289     elsif self > max
290       max
291     else
292       self
293     end
294   end
295
296   def in? range; range.member? self; end
297 end
298
299 class Fixnum
300   def num_digits base=10
301     return 1 if self == 0
302     1 + (Math.log(self) / Math.log(10)).floor
303   end
304   
305   def to_character
306     if self < 128 && self >= 0
307       chr
308     else
309       "<#{self}>"
310     end
311   end
312
313   def pluralize s
314     to_s + " " + (self == 1 ? s : s + "s")
315   end
316 end
317
318 class Hash
319   def - o
320     Hash[*self.map { |k, v| [k, v] unless o.include? k }.compact.flatten_one_level]
321   end
322
323   def select_by_value v=true
324     select { |k, vv| vv == v }.map { |x| x.first }
325   end
326 end
327
328 module Enumerable
329   def map_with_index
330     ret = []
331     each_with_index { |x, i| ret << yield(x, i) }
332     ret
333   end
334
335   def sum; inject(0) { |x, y| x + y }; end
336   
337   def map_to_hash
338     ret = {}
339     each { |x| ret[x] = yield(x) }
340     ret
341   end
342
343   # like find, except returns the value of the block rather than the
344   # element itself.
345   def argfind
346     ret = nil
347     find { |e| ret ||= yield(e) }
348     ret || nil # force
349   end
350
351   def argmin
352     best, bestval = nil, nil
353     each do |e|
354       val = yield e
355       if bestval.nil? || val < bestval
356         best, bestval = e, val
357       end
358     end
359     best
360   end
361
362   ## returns the maximum shared prefix of an array of strings
363   ## optinally excluding a prefix
364   def shared_prefix caseless=false, exclude=""
365     return "" if empty?
366     prefix = ""
367     (0 ... first.length).each do |i|
368       c = (caseless ? first.downcase : first)[i]
369       break unless all? { |s| (caseless ? s.downcase : s)[i] == c }
370       next if exclude[i] == c
371       prefix += first[i].chr
372     end
373     prefix
374   end
375
376   def max_of
377     map { |e| yield e }.max
378   end
379 end
380
381 class Array
382   def flatten_one_level
383     inject([]) { |a, e| a + e }
384   end
385
386   def to_h; Hash[*flatten]; end
387   def rest; self[1..-1]; end
388
389   def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
390
391   def last= e; self[-1] = e end
392 end
393
394 class Time
395   def to_indexable_s
396     sprintf "%012d", self
397   end
398
399   def nearest_hour
400     if min < 30
401       self
402     else
403       self + (60 - min) * 60
404     end
405   end
406
407   def midnight # within a second
408     self - (hour * 60 * 60) - (min * 60) - sec
409   end
410
411   def is_the_same_day? other
412     (midnight - other.midnight).abs < 1
413   end
414
415   def is_the_day_before? other
416     other.midnight - midnight <=  24 * 60 * 60 + 1
417   end
418
419   def to_nice_distance_s from=Time.now
420     later_than = (self < from)
421     diff = (self.to_i - from.to_i).abs.to_f
422     text = 
423       [ ["second", 60],
424         ["minute", 60],
425         ["hour", 24],
426         ["day", 7],
427         ["week", 4.345], # heh heh
428         ["month", 12],
429         ["year", nil],
430       ].argfind do |unit, size|
431         if diff.round <= 1
432           "one #{unit}"
433         elsif size.nil? || diff.round < size
434           "#{diff.round} #{unit}s"
435         else
436           diff /= size.to_f
437           false
438         end
439       end
440     if later_than
441       text + " ago"
442     else
443       "in " + text
444     end  
445   end
446
447   TO_NICE_S_MAX_LEN = 9 # e.g. "Yest.10am"
448   def to_nice_s from=Time.now
449     if year != from.year
450       strftime "%b %Y"
451     elsif month != from.month
452       strftime "%b %e"
453     else
454       if is_the_same_day? from
455         strftime("%l:%M%P")
456       elsif is_the_day_before? from
457         "Yest."  + nearest_hour.strftime("%l%P")
458       else
459         strftime "%b %e"
460       end
461     end
462   end
463 end
464
465 ## simple singleton module. far less complete and insane than the ruby
466 ## standard library one, but automatically forwards methods calls and
467 ## allows for constructors that take arguments.
468 ##
469 ## You must have #initialize call "self.class.i_am_the_instance self"
470 ## at some point or everything will fail horribly.
471 module Singleton
472   module ClassMethods
473     def instance; @instance; end
474     def instantiated?; defined?(@instance) && !@instance.nil?; end
475     def deinstantiate!; @instance = nil; end
476     def method_missing meth, *a, &b
477       raise "no instance defined!" unless defined? @instance
478
479       ## if we've been deinstantiated, just drop all calls. this is
480       ## useful because threads that might be active during the
481       ## cleanup process (e.g. polling) would otherwise have to
482       ## special-case every call to a Singleton object
483       return nil if @instance.nil?
484
485       @instance.send meth, *a, &b
486     end
487     def i_am_the_instance o
488       raise "there can be only one! (instance)" if defined? @instance
489       @instance = o
490     end
491   end
492
493   def self.included klass
494     klass.extend ClassMethods
495   end
496 end
497
498 ## wraps an object. if it throws an exception, keeps a copy, and
499 ## rethrows it for any further method calls.
500 class Recoverable
501   def initialize o
502     @o = o
503     @e = nil
504   end
505
506   def clear_error!; @e = nil; end
507   def has_errors?; !@e.nil?; end
508   def error; @e; end
509
510   def method_missing m, *a, &b; __pass m, *a, &b; end
511   
512   def id; __pass :id; end
513   def to_s; __pass :to_s; end
514   def to_yaml x; __pass :to_yaml, x; end
515   def is_a? c; @o.is_a? c; end
516
517   def respond_to? m; @o.respond_to? m end
518
519   def __pass m, *a, &b
520     begin
521       @o.send(m, *a, &b)
522     rescue Exception => e
523       @e = e
524       raise e
525     end
526   end
527 end
528
529 ## acts like a hash with an initialization block, but saves any
530 ## newly-created value even upon lookup.
531 ##
532 ## for example:
533 ##
534 ## class C
535 ##   attr_accessor :val
536 ##   def initialize; @val = 0 end
537 ## end
538 ## 
539 ## h = Hash.new { C.new }
540 ## h[:a].val # => 0
541 ## h[:a].val = 1
542 ## h[:a].val # => 0
543 ##
544 ## h2 = SavingHash.new { C.new }
545 ## h2[:a].val # => 0
546 ## h2[:a].val = 1
547 ## h2[:a].val # => 1
548 ##
549 ## important note: you REALLY want to use #member? to test existence,
550 ## because just checking h[anything] will always evaluate to true
551 ## (except for degenerate constructor blocks that return nil or false)
552 class SavingHash
553   def initialize &b
554     @constructor = b
555     @hash = Hash.new
556   end
557
558   def [] k
559     @hash[k] ||= @constructor.call(k)
560   end
561
562   defer_all_other_method_calls_to :hash
563 end