5 # Error class for all exceptions to do with option casts.
6 class CastError < StandardError
9 # Biopieces are command line scripts and uses OptionParser to parse command line
10 # options according to a list of casts. Each cast prescribes the long and short
11 # name of the option, the type, if it is mandatory, the default value, and allowed
12 # and disallowed values. An optional list of extra casts can be supplied, and the
13 # integrity of the casts are checked. Following the command line parsing, the
14 # options are checked according to the casts. Methods are also included for handling
15 # the parsing and emitting of Biopiece records, which are ASCII text records consisting
16 # of lines with a key/value pair seperated by a colon and a white space ': '.
17 # Each record is separated by a line with three dashes '---'.
19 TYPES = %w[flag string list int uint float file file! files files! dir dir! genome]
20 MANDATORY = %w[long short type mandatory default allowed disallowed]
21 REGEX_LIST = /^(list|files|files!)$/
22 REGEX_INT = /^(int|uint)$/
23 REGEX_STRING = /^(string|file|file!|dir|dir!|genome)$/
25 # Initialize a Biopiece and write the status to file.
26 def initialize(no_status=nil)
27 status_set unless no_status
30 # Check the integrity of a list of casts, followed by parsion options from argv
31 # and finally checking the options according to the casts. Returns nil if
32 # argv is empty, otherwise an options hash.
33 def parse(argv,casts=[],script_path=$0)
35 @script_path = script_path
42 options_template = OptionParser.new do |option|
44 if cast[:type] == 'flag'
45 option.on("-#{cast[:short]}", "--#{cast[:long]}") do |o|
46 @options[cast[:long]] = o
48 elsif cast[:type] =~ REGEX_LIST
49 option.on( "-#{cast[:short]}", "--#{cast[:long]} A", Array) do |a|
50 @options[cast[:long]] = a
52 elsif cast[:type] =~ REGEX_INT
53 option.on("-#{cast[:short]}", "--#{cast[:long]} I", Integer) do |i|
54 @options[cast[:long]] = i
56 elsif cast[:type] =~ REGEX_STRING
57 option.on("-#{cast[:short]}", "--#{cast[:long]} S", String) do |s|
58 @options[cast[:long]] = s
60 elsif cast[:type] == 'float'
61 option.on("-#{cast[:short]}", "--#{cast[:long]} F", Float) do |f|
62 @options[cast[:long]] = f
65 raise ArgumentError, "Unknown option type: '#{cast[:type]}'"
70 options_template.parse!(argv)
73 print_usage_and_exit(true)
74 return # break for unit testing.
75 elsif print_usage_short?
77 return # break for unit testing.
87 # Open Biopiece input stream if not open and iterate over all Biopiece
88 # records in the stream.
90 @in = stream_in_open unless @in.is_a? IO
94 @in.each_line do |line|
96 when /^([^:]+): (.*)$/
99 yield record unless record.empty?
102 raise "Bad record format: #{line}"
106 yield record unless record.empty?
108 self # conventionally
111 alias :each :each_record
113 # Open Biopiece output stream if not open and puts record to the stream.
115 @out = stream_out_open unless @out.is_a? IO
117 record.each do |key,value|
118 @out.print "#{key}: #{value}\n"
124 # Close Biopiece streams, remove tmp_dir, end log status.
126 @in.close if @in.respond_to? :close
127 @out.close if @out.respond_to? :close
128 # remove tmpdir if found
135 # Given the script name determine the path of the wiki file with the usage info.
137 path = ENV["BP_DIR"] + "/bp_usage/" + File.basename(@script_path, ".rb") + ".wiki"
138 raise "No such wiki file: #{path}" unless File.file? path
143 # Add ubiquitous options casts.
145 @casts << {:long => 'help', :short => '?', :type => 'flag', :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
146 @casts << {:long => 'stream_in', :short => 'I', :type => 'files!', :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
147 @casts << {:long => 'stream_out', :short => 'O', :type => 'file', :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
148 @casts << {:long => 'verbose', :short => 'v', :type => 'flag', :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
151 # Check integrity of the casts.
155 cast_check_duplicates
158 # Check if all mandatory keys are present in casts and raise if not.
160 @casts.each do |cast|
161 MANDATORY.each do |mandatory|
162 raise CastError, "Missing symbol in cast: '#{mandatory.to_sym}'" unless cast.has_key? mandatory.to_sym
167 # Check if all values in casts are valid.
168 def cast_check_values
169 @casts.each do |cast|
170 cast_check_val_long(cast)
171 cast_check_val_short(cast)
172 cast_check_val_type(cast)
173 cast_check_val_mandatory(cast)
174 cast_check_val_default(cast)
175 cast_check_val_allowed(cast)
176 cast_check_val_disallowed(cast)
180 # Check if the values to long are legal and raise if not.
181 def cast_check_val_long(cast)
182 unless cast[:long].is_a? String and cast[:long].length > 1
183 raise CastError, "Illegal cast of long: '#{cast[:long]}'"
187 # Check if the values to short are legal and raise if not.
188 def cast_check_val_short(cast)
189 unless cast[:short].is_a? String and cast[:short].length == 1
190 raise CastError, "Illegal cast of short: '#{cast[:short]}'"
194 # Check if values to type are legal and raise if not.
195 def cast_check_val_type(cast)
198 type_hash[type] = true
201 unless type_hash.has_key? cast[:type]
202 raise CastError, "Illegal cast of type: '#{cast[:type]}'"
206 # Check if values to mandatory are legal and raise if not.
207 def cast_check_val_mandatory(cast)
208 unless cast[:mandatory] == true or cast[:mandatory] == false
209 raise CastError, "Illegal cast of mandatory: '#{cast[:mandatory]}'"
213 # Check if values to default are legal and raise if not.
214 def cast_check_val_default(cast)
215 unless cast[:default].nil? or
216 cast[:default].is_a? String or
217 cast[:default].is_a? Integer or
218 cast[:default].is_a? Float
219 raise CastError, "Illegal cast of default: '#{cast[:default]}'"
223 # Check if values to allowed are legal and raise if not.
224 def cast_check_val_allowed(cast)
225 unless cast[:allowed].is_a? String or cast[:allowed].nil?
226 raise CastError, "Illegal cast of allowed: '#{cast[:allowed]}'"
230 # Check if values to disallowed are legal and raise if not.
231 def cast_check_val_disallowed(cast)
232 unless cast[:disallowed].is_a? String or cast[:disallowed].nil?
233 raise CastError, "Illegal cast of disallowed: '#{cast[:disallowed]}'"
237 # Check cast for duplicate long or short options names.
238 def cast_check_duplicates
240 @casts.each do |cast|
241 raise CastError, "Duplicate argument: '--#{cast[:long]}'" if check_hash.has_key? cast[:long]
242 raise CastError, "Duplicate argument: '-#{cast[:short]}'" if check_hash.has_key? cast[:short]
243 check_hash[cast[:long]] = true
244 check_hash[cast[:short]] = true
248 # Check if full "usage info" should be printed.
249 def print_usage_full?
253 # Check if short "usage info" should be printed.
254 def print_usage_short?
257 elsif @options["stream_in"]
259 elsif @options["data_in"]
261 elsif wiki_path =~ /^(list_biopieces|list_genomes|list_mysql_databases|biostat)$/ # TODO get rid of this!
268 # Print usage info by Calling an external script 'print_wiki'
269 # using a system() call and exit. An optional 'full' flag
270 # outputs the full usage info.
271 def print_usage_and_exit(full=nil)
273 system("print_wiki --data_in #{wiki_path} --help")
275 system("print_wiki --data_in #{wiki_path}")
278 raise "Failed printing wiki: #{wiki_path}" unless $?.success?
283 # Set default options value from cast unless a value is set.
285 @casts.each do |cast|
287 @options[cast[:long]] = cast[:default] unless @options.has_key? cast[:long]
292 # Expands glob expressions to a full list of paths.
293 # Examples: "*.fna" or "foo.fna,*.fna" or "foo.fna,/bar/*.fna"
295 @casts.each do |cast|
296 if cast[:type] == 'files' or cast[:type] == 'files!'
297 if @options.has_key? cast[:long]
300 @options[cast[:long]].each do |path|
302 Dir.glob(path).each do |file|
303 files << file if File.file? file
310 @options[cast[:long]] = files
316 # Check all options according to casts.
318 @casts.each do |cast|
319 options_check_mandatory(cast)
320 options_check_int(cast)
321 options_check_uint(cast)
322 options_check_file(cast)
323 options_check_files(cast)
324 options_check_dir(cast)
325 options_check_allowed(cast)
326 options_check_disallowed(cast)
330 # Check if a mandatory option is set and raise if it isn't.
331 def options_check_mandatory(cast)
333 raise ArgumentError, "Mandatory argument: --#{cast[:long]}" unless @options.has_key? cast[:long]
337 # Check int type option and raise if not an integer.
338 def options_check_int(cast)
339 if cast[:type] == 'int' and @options.has_key? cast[:long]
340 unless @options[cast[:long]].is_a? Integer
341 raise ArgumentError, "Argument to --#{cast[:long]} must be an integer, not '#{@options[cast[:long]]}'"
346 # Check uint type option and raise if not an unsinged integer.
347 def options_check_uint(cast)
348 if cast[:type] == 'uint' and @options.has_key? cast[:long]
349 unless @options[cast[:long]].is_a? Integer and @options[cast[:long]] >= 0
350 raise ArgumentError, "Argument to --#{cast[:long]} must be an unsigned integer, not '#{@options[cast[:long]]}'"
355 # Check file! type argument and raise if file don't exists.
356 def options_check_file(cast)
357 if cast[:type] == 'file!' and @options.has_key? cast[:long]
358 raise ArgumentError, "No such file: '#{@options[cast[:long]]}'" unless File.file? @options[cast[:long]]
362 # Check files! type argument and raise if files don't exists.
363 def options_check_files(cast)
364 if cast[:type] == 'files!' and @options.has_key? cast[:long]
365 @options[cast[:long]].each do |path|
366 raise ArgumentError, "No such file: '#{path}'" unless File.file? path
371 # Check dir! type argument and raise if directory don't exist.
372 def options_check_dir(cast)
373 if cast[:type] == 'dir!' and @options.has_key? cast[:long]
374 raise ArgumentError, "No such directory: '#{@options[cast[:long]]}'" unless File.directory? @options[cast[:long]]
378 # Check options and raise unless allowed.
379 def options_check_allowed(cast)
380 if cast[:allowed] and @options.has_key? cast[:long]
382 cast[:allowed].split(',').each { |a| allowed_hash[a] = 1 }
384 raise ArgumentError, "Argument '#{@options[cast[:long]]}' to --#{cast[:long]} not allowed" unless allowed_hash.has_key? @options[cast[:long]]
388 # Check disallowed argument values and raise if disallowed.
389 def options_check_disallowed(cast)
390 if cast[:disallowed] and @options.has_key? cast[:long]
391 cast[:disallowed].split(',').each do |val|
392 raise ArgumentError, "Argument '#{@options[cast[:long]]}' to --#{cast[:long]} is disallowed" if val == @options[cast[:long]]
397 # Open Biopieces input data stream for reading from either
398 # stdin or from a list of files specified in options["stream_in"].
403 stream = read(@options["stream_in"])
409 # Open Biopieces output data stream for writing to stdout
410 # or a file specified in options["stream_out"].
412 if @options["stream_out"]
413 stream = write(@options["stream_out"], @options["compress"])
421 # Opens a reads stream to a list of files.
424 stream = zread(files)
426 stream = nread(files)
432 # Opens a write stream to a file and returns a _io_ object.
433 def write(file, zip=nil)
434 zip ? zwrite(file) : nwrite(file)
437 # Test if a list of files are gzipped or not.
438 # Raises if files are mixed zipped and unzipped.
443 type = `file #{file}`
445 if type =~ /gzip compressed/
446 type_hash[:gzip] = true
448 type_hash[:ascii] = true
452 raise "Mixture of zipped and unzipped files" if type_hash.size == 2
457 # Opens a list of gzipped files for reading and return an _io_ object.
459 stdin, stdout, stderr = Open3.popen3("zcat " + files.join(' '));
465 # Opens a file for gzipped writing and return an _io_ object.
467 stdin, stdout, stderr = Open3.popen3("gzip -f > #{file}")
473 # Opens a list of files for reading and return an _io_ object.
475 stdin, stdout, stderr = Open3.popen3("cat " + files.join(' '));
481 # Opens a file for writing and return an _io_ object.
483 File.open(file, mode="w")
486 # Write the status to a status file.
489 time = now.strftime("%Y-%m-%d %X")
491 script = File.basename($0, ".rb")
493 path = ENV["BP_TMP"] + "/" + [user, script, pid, "status"].join(".")
495 File.open(path, mode="w") { |file| file.puts [time, ARGV.join(" ")].join(";") }
498 # Write the Biopiece status to the log file.
499 def status_log(status="OK")
501 time1 = now.strftime("%Y-%m-%d %X")
503 script = File.basename($0, ".rb")
505 path = ENV["BP_TMP"] + "/" + [user, script, pid, "status"].join(".")
507 stream = File.open(path)
508 time0, args, tmp_dir = stream.first.split(";")
510 # Dir.rmdir(tmp_dir) unless tmp_dir.nil? and File.directory? tmp_dir #TODO fix this!
512 elap = time_diff(time0, time1)
513 command = [script, args].join(" ")
514 log_file = ENV["BP_LOG"] + "/biopieces.log"
516 File.open(log_file, mode="a") { |file| file.puts [time0, time1, elap, user, status, command].join("\t") }
519 # Get the elapsed time from the difference between two time stamps.
520 def time_diff(t0, t1)
521 t0 =~ /(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/
529 sec0 += day0 * 24 * 60 * 60
530 sec0 += hour0 * 60 * 60
533 t1 =~ /(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/
541 sec1 += day1 * 24 * 60 * 60
542 sec1 += hour1 * 60 * 60
551 hour = ( sec / ( 60 * 60 ) ).to_i
552 sec -= hour * 60 * 60
554 min = ( sec / 60 ).to_i
557 [sprintf("%02d", hour), sprintf("%02d", min), sprintf("%02d", sec)].join(":")