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