]> git.donarmstrong.com Git - biopieces.git/blob - code_ruby/Maasha/lib/biopieces.rb
added read_454 and write_454 (and ruby stuff)
[biopieces.git] / code_ruby / Maasha / lib / biopieces.rb
1 require 'optparse'
2 require 'open3'
3 require 'pp'
4
5 # Error class for all exceptions to do with option casts.
6 class CastError < StandardError
7 end
8
9 # Class using OptionParser to parse command line options according to a list of
10 # casts. Each cast prescribes the long and short name of the option, the type,
11 # if it is mandatory, the default value, and allowed and disallowed values. An
12 # optional list of extra casts can be supplied, and the integrity of the casts
13 # are checked. Following the command line parsing, the options are checked
14 # according to the casts.
15 class Biopieces
16   TYPES        = %w[flag string list int uint float file file! files files! dir dir! genome]
17   MANDATORY    = %w[long short type mandatory default allowed disallowed]
18   REGEX_LIST   = /^(list|files|files!)$/
19   REGEX_INT    = /^(int|uint)$/
20   REGEX_STRING = /^(string|file|file!|dir|dir!|genome)$/
21
22   # Check the integrity of a list of casts, followed by parsion options from argv
23   # and finally checking the options according to the casts. Returns nil if
24   # argv is empty, otherwise an options hash.
25   def parse(argv,casts=[],script_path=$0)
26     @casts       = casts
27     @script_path = script_path
28
29     cast_ubiquitous
30     cast_check
31
32     @options = {}
33
34     options_template = OptionParser.new do |option|
35       @casts.each do |cast|
36         if cast[:type] == 'flag'
37           option.on("-#{cast[:short]}", "--#{cast[:long]}") do |o|
38             @options[cast[:long]] = o
39           end
40         elsif cast[:type] =~ REGEX_LIST
41           option.on( "-#{cast[:short]}", "--#{cast[:long]} A", Array) do |a|
42             @options[cast[:long]] = a
43           end
44         elsif cast[:type] =~ REGEX_INT
45           option.on("-#{cast[:short]}", "--#{cast[:long]} I", Integer) do |i|
46             @options[cast[:long]] = i
47           end
48         elsif cast[:type] =~ REGEX_STRING
49           option.on("-#{cast[:short]}", "--#{cast[:long]} S", String) do |s|
50             @options[cast[:long]] = s
51           end
52         elsif cast[:type] == 'float'
53           option.on("-#{cast[:short]}", "--#{cast[:long]} F", Float) do |f|
54             @options[cast[:long]] = f
55           end
56         else
57           raise ArgumentError, "Unknown option type: '#{cast[:type]}'"
58         end
59       end
60     end
61
62     options_template.parse!(argv)
63
64     if print_usage_full?
65       print_usage_and_exit(true)
66       return # break for unit testing.
67     elsif print_usage_short?
68       print_usage_and_exit
69       return # break for unit testing.
70     end
71
72     options_default
73     options_glob
74     options_check
75
76     @options
77   end
78
79   private
80
81   # Given the script name determine the path of the wiki file with the usage info.
82   def wiki_path
83     path = ENV["BP_DIR"] + "/bp_usage/" + File.basename(@script_path, ".rb") + ".wiki"
84     raise "No such wiki file: #{path}" unless File.file? path
85
86     path
87   end
88
89   # Add ubiquitous options casts.
90   def cast_ubiquitous
91     @casts << {:long => 'help',       :short => '?', :type => 'flag',   :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
92     @casts << {:long => 'stream_in',  :short => 'I', :type => 'files!', :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
93     @casts << {:long => 'stream_out', :short => 'O', :type => 'file',   :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
94     @casts << {:long => 'verbose',    :short => 'v', :type => 'flag',   :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
95   end
96
97   # Check integrity of the casts.
98   def cast_check
99     cast_check_keys
100     cast_check_values
101     cast_check_duplicates
102   end
103   
104   # Check if all mandatory keys are present in casts and raise if not.
105   def cast_check_keys
106     @casts.each do |cast|
107       MANDATORY.each do |mandatory|
108         raise CastError, "Missing symbol in cast: '#{mandatory.to_sym}'" unless cast.has_key? mandatory.to_sym
109       end
110     end
111   end
112
113   # Check if all values in casts are valid.
114   def cast_check_values
115     @casts.each do |cast|
116       cast_check_val_long(cast)
117       cast_check_val_short(cast)
118       cast_check_val_type(cast)
119       cast_check_val_mandatory(cast)
120       cast_check_val_default(cast)
121       cast_check_val_allowed(cast)
122       cast_check_val_disallowed(cast)
123     end
124   end
125
126   # Check if the values to long are legal and raise if not.
127   def cast_check_val_long(cast)
128     unless cast[:long].is_a? String and cast[:long].length > 1
129       raise CastError, "Illegal cast of long: '#{cast[:long]}'"
130     end
131   end
132   
133   # Check if the values to short are legal and raise if not.
134   def cast_check_val_short(cast)
135     unless cast[:short].is_a? String and cast[:short].length == 1
136       raise CastError, "Illegal cast of short: '#{cast[:short]}'"
137     end
138   end
139
140   # Check if values to type are legal and raise if not.
141   def cast_check_val_type(cast)
142     type_hash = {}
143     TYPES.each do |type|
144       type_hash[type] = true
145     end
146
147     unless type_hash.has_key? cast[:type]
148       raise CastError, "Illegal cast of type: '#{cast[:type]}'"
149     end
150   end
151
152   # Check if values to mandatory are legal and raise if not.
153   def cast_check_val_mandatory(cast)
154     unless cast[:mandatory] == true or cast[:mandatory] == false
155       raise CastError, "Illegal cast of mandatory: '#{cast[:mandatory]}'"
156     end
157   end
158
159   # Check if values to default are legal and raise if not.
160   def cast_check_val_default(cast)
161     unless cast[:default].nil? or
162            cast[:default].is_a? String or
163            cast[:default].is_a? Integer or
164            cast[:default].is_a? Float
165       raise CastError, "Illegal cast of default: '#{cast[:default]}'"
166     end
167   end
168
169   # Check if values to allowed are legal and raise if not.
170   def cast_check_val_allowed(cast)
171     unless cast[:allowed].is_a? String or cast[:allowed].nil?
172       raise CastError, "Illegal cast of allowed: '#{cast[:allowed]}'"
173     end
174   end
175
176   # Check if values to disallowed are legal and raise if not.
177   def cast_check_val_disallowed(cast)
178     unless cast[:disallowed].is_a? String or cast[:disallowed].nil?
179       raise CastError, "Illegal cast of disallowed: '#{cast[:disallowed]}'"
180     end
181   end
182
183   # Check cast for duplicate long or short options names.
184   def cast_check_duplicates
185     check_hash = {}
186     @casts.each do |cast|
187       raise CastError, "Duplicate argument: '--#{cast[:long]}'" if check_hash.has_key? cast[:long]
188       raise CastError, "Duplicate argument: '-#{cast[:short]}'" if check_hash.has_key? cast[:short]
189       check_hash[cast[:long]]  = true
190       check_hash[cast[:short]] = true
191     end
192   end
193
194   # Check if full "usage info" should be printed.
195   def print_usage_full?
196     @options["help"]
197   end
198
199   # Check if short "usage info" should be printed.
200   def print_usage_short?
201     if not $stdin.tty?
202       return false
203     elsif @options["stream_in"]
204       return false
205     elsif @options["data_in"]
206       return false
207     elsif wiki_path =~ /^(list_biopieces|list_genomes|list_mysql_databases|biostat)$/  # TODO get rid of this!
208       return false
209     else
210       return true
211     end
212   end
213
214   # Print usage info by Calling an external script 'print_wiki'
215   # using a system() call and exit. An optional 'full' flag
216   # outputs the full usage info.
217   def print_usage_and_exit(full=nil)
218     if full
219       system("print_wiki --data_in #{wiki_path} --help")
220     else
221       system("print_wiki --data_in #{wiki_path}")
222     end
223
224     raise "Failed printing wiki: #{wiki_path}" unless $?.success?
225
226     exit
227   end
228
229   # Set default options value from cast unless a value is set.
230   def options_default
231     @casts.each do |cast|
232       if cast[:default]
233         @options[cast[:long]] = cast[:default] unless @options.has_key? cast[:long]
234       end
235     end
236   end
237
238   # Expands glob expressions to a full list of paths.
239   # Examples: "*.fna" or "foo.fna,*.fna" or "foo.fna,/bar/*.fna"
240   def options_glob
241     @casts.each do |cast|
242       if cast[:type] == 'files' or cast[:type] == 'files!'
243         if @options.has_key? cast[:long]
244           files = []
245         
246           @options[cast[:long]].each do |path|
247             if path.include? "*"
248               Dir.glob(path).each do |file|
249                 files << file if File.file? file
250               end
251             else
252               files << path
253             end
254           end
255
256           @options[cast[:long]] = files
257         end
258       end
259     end
260   end
261
262   # Check all options according to casts.
263   def options_check
264     @casts.each do |cast|
265       options_check_mandatory(cast)
266       options_check_int(cast)
267       options_check_uint(cast)
268       options_check_file(cast)
269       options_check_files(cast)
270       options_check_dir(cast)
271       options_check_allowed(cast)
272       options_check_disallowed(cast)
273     end
274   end
275   
276   # Check if a mandatory option is set and raise if it isn't.
277   def options_check_mandatory(cast)
278     if cast[:mandatory]
279       raise ArgumentError, "Mandatory argument: --#{cast[:long]}" unless @options.has_key? cast[:long]
280     end
281   end
282
283   # Check int type option and raise if not an integer.
284   def options_check_int(cast)
285     if cast[:type] == 'int' and @options.has_key? cast[:long]
286       unless @options[cast[:long]].is_a? Integer
287         raise ArgumentError, "Argument to --#{cast[:long]} must be an integer, not '#{@options[cast[:long]]}'"
288       end
289     end
290   end
291   
292   # Check uint type option and raise if not an unsinged integer.
293   def options_check_uint(cast)
294     if cast[:type] == 'uint' and @options.has_key? cast[:long]
295       unless @options[cast[:long]].is_a? Integer and @options[cast[:long]] >= 0
296         raise ArgumentError, "Argument to --#{cast[:long]} must be an unsigned integer, not '#{@options[cast[:long]]}'"
297       end
298     end
299   end
300
301   # Check file! type argument and raise if file don't exists.
302   def options_check_file(cast)
303     if cast[:type] == 'file!' and @options.has_key? cast[:long]
304       raise ArgumentError, "No such file: '#{@options[cast[:long]]}'" unless File.file? @options[cast[:long]]
305     end
306   end
307
308   # Check files! type argument and raise if files don't exists.
309   def options_check_files(cast)
310     if cast[:type] == 'files!' and @options.has_key? cast[:long]
311       @options[cast[:long]].each do |path|
312         raise ArgumentError, "No such file: '#{path}'" unless File.file? path
313       end
314     end
315   end
316   
317   # Check dir! type argument and raise if directory don't exist.
318   def options_check_dir(cast)
319     if cast[:type] == 'dir!' and @options.has_key? cast[:long]
320       raise ArgumentError, "No such directory: '#{@options[cast[:long]]}'" unless File.directory? @options[cast[:long]]
321     end
322   end
323   
324   # Check options and raise unless allowed.
325   def options_check_allowed(cast)
326     if cast[:allowed] and @options.has_key? cast[:long]
327       allowed_hash = {}
328       cast[:allowed].split(',').each { |a| allowed_hash[a] = 1 }
329   
330       raise ArgumentError, "Argument '#{@options[cast[:long]]}' to --#{cast[:long]} not allowed" unless allowed_hash.has_key? @options[cast[:long]]
331     end
332   end
333   
334   # Check disallowed argument values and raise if disallowed.
335   def options_check_disallowed(cast)
336     if cast[:disallowed] and @options.has_key? cast[:long]
337       cast[:disallowed].split(',').each do |val|
338         raise ArgumentError, "Argument '#{@options[cast[:long]]}' to --#{cast[:long]} is disallowed" if val == @options[cast[:long]]
339       end
340     end
341   end
342 end
343
344
345 class Stream
346   def initialize(options)
347     @options = options
348     @in      = stream_in_open
349     @out     = stream_out_open
350   end
351
352   def each_record
353     record = {}
354
355     @in.each_line do |line|
356       case line
357       when /^([^:]+): (.*)$/
358         record[$1] = $2
359       when /^---$/
360         yield record unless record.empty?
361         record = {}
362       else
363         raise "Bad record format: #{line}"
364       end
365     end
366
367     yield record unless record.empty?
368
369     self # conventionally
370   end
371
372   alias :each :each_record
373
374   def puts(record)
375     record.each do |key,value|
376       @out.print "#{key}: #{value}\n"
377     end
378
379     @out.print "---\n"
380   end
381
382   private
383
384   # Open Biopieces input data stream for reading from either
385   # stdin or from a list of files specified in options["stream_in"].
386   def stream_in_open
387     if not $stdin.tty?
388       stream = $stdin
389     else
390       stream = read(@options["stream_in"])
391     end
392
393     stream
394   end
395
396   # Open Biopieces output data stream for writing to stdout
397   # or a file specified in options["stream_out"].
398   def stream_out_open
399     if @options["stream_out"]
400       stream = write(@options["stream_out"], @options["compress"])
401     else
402       stream = $stdout
403     end
404
405     stream
406   end
407
408   # Opens a reads stream to a list of files.
409   def read(files)
410     if zipped?(files)
411       stream = zread(files)
412     else
413       stream = nread(files)
414     end
415
416     stream
417   end
418
419   # Opens a write stream to a file and returns a _io_ object.
420   def write(file, zip=nil)
421     zip ? zwrite(file) : nwrite(file)
422   end
423
424   # Test if a list of files are gzipped or not.
425   # Raises if files are mixed zipped and unzipped.
426   def zipped?(files)
427     type_hash = {}
428
429     files.each do |file|
430       type = `file #{file}`
431
432       if type =~ /gzip compressed/
433         type_hash[:gzip] = true
434       else
435         type_hash[:ascii] = true
436       end
437     end
438
439     raise "Mixture of zipped and unzipped files" if type_hash.size == 2
440
441     type_hash[:gzip]
442   end
443
444   # Opens a list of gzipped files for reading and return an _io_ object.
445   def zread(files)
446     stdin, stdout, stderr = Open3.popen3("zcat " + files.join(' '));
447     stdin.close
448     stderr.close
449     stdout
450   end
451
452   # Opens a file for gzipped writing and return an _io_ object.
453   def zwrite(file)
454     stdin, stdout, stderr = Open3.popen3("gzip -f > #{file}")
455     stderr.close
456     stdout.close
457     stdin
458   end
459
460   # Opens a list of files for reading and return an _io_ object.
461   def nread(files)
462     stdin, stdout, stderr = Open3.popen3("cat " + files.join(' '));
463     stdin.close
464     stderr.close
465     stdout
466   end
467
468   # Opens a file for writing and return an _io_ object.
469   def nwrite(file)
470     File.open(file, mode="w")
471   end
472 end
473
474
475 class Status
476   def initialize
477     status_set
478   end
479
480   def status_set
481     now    = Time.new
482     time   = now.strftime("%Y-%m-%d %X")
483     user   = ENV["USER"]
484     script = File.basename($0, ".rb")
485     pid    = $$
486     path   = ENV["BP_TMP"] + "/" + [user, script, pid, "status"].join(".")
487
488     File.open(path, mode="w") { |file| file.puts [time, ARGV.join(" ")].join(";") }
489   end
490
491   def log(status="OK")
492     now     = Time.new
493     time1   = now.strftime("%Y-%m-%d %X")
494     user    = ENV["USER"]
495     script  = File.basename($0, ".rb")
496     pid     = $$
497     path    = ENV["BP_TMP"] + "/" + [user, script, pid, "status"].join(".")
498
499     stream = File.open(path)
500     time0, args, tmp_dir = stream.first.split(";")
501
502     # Dir.rmdir(tmp_dir) unless tmp_dir.nil? and File.directory? tmp_dir   #TODO fix this!
503
504     elap     = time_diff(time0, time1)
505     command  = [script, args].join(" ") 
506     log_file = ENV["BP_LOG"] + "/biopieces.log"
507
508     File.open(log_file, mode="a") { |file| file.puts [time0, time1, elap, user, status, command].join("\t") }
509   end
510
511   private
512
513   def time_diff(t0, t1)
514     t0 =~ /(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/
515     year0 = $1.to_i
516     mon0  = $2.to_i
517     day0  = $3.to_i
518     hour0 = $4.to_i
519     min0  = $5.to_i
520     sec0  = $6.to_i
521
522     sec0 += day0  * 24 * 60 * 60
523     sec0 += hour0 * 60 * 60
524     sec0 += min0  * 60
525
526     t1 =~ /(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/
527     year1 = $1.to_i
528     mon1  = $2.to_i
529     day1  = $3.to_i
530     hour1 = $4.to_i
531     min1  = $5.to_i
532     sec1  = $6.to_i
533
534     sec1 += day1  * 24 * 60 * 60
535     sec1 += hour1 * 60 * 60
536     sec1 += min1  * 60
537
538     year = year1 - year0
539     mon  = mon1  - mon0
540     day  = day1  - day0 
541
542     sec  = sec1 - sec0
543
544     hour = ( sec / ( 60 * 60 ) ).to_i
545     sec -= hour * 60 * 60
546
547     min  = ( sec / 60 ).to_i
548     sec -= min * 60
549
550     [sprintf("%02d", hour), sprintf("%02d", min), sprintf("%02d", sec)].join(":")
551   end
552 end
553
554
555 __END__