9 %w( INT TERM QUIT ).each do |signal|
10 Signal.trap(signal) do
20 # Biopieces are command line scripts and uses OptionParser to parse command line
21 # options according to a list of casts. Each cast prescribes the long and short
22 # name of the option, the type, if it is mandatory, the default value, and allowed
23 # and disallowed values. An optional list of extra casts can be supplied, and the
24 # integrity of the casts are checked. Following the command line parsing, the
25 # options are checked according to the casts. Methods are also included for handling
26 # the parsing and emitting of Biopiece records, which are ASCII text records consisting
27 # of lines with a key/value pair seperated by a colon and a white space ': '.
28 # Each record is separated by a line with three dashes '---'.
30 # Initialize a Biopiece and write the status to file.
31 # Options are for testing purposes only.
32 def initialize(test=nil, input=STDIN, output=STDOUT)
37 @status.set unless @test
41 # Check the integrity of a list of casts, followed by parsion options from argv
42 # and finally checking the options according to the casts. Returns nil if argv
43 # is empty, otherwise an options hash.
44 def parse(argv, cast_list=[], script_path=$0)
45 casts = Casts.new(cast_list)
46 option_handler = OptionHandler.new(argv, casts, script_path, @test)
47 @options = option_handler.options_parse
50 # Open Biopiece input stream if not open and iterate over all Biopiece
51 # records in the stream.
53 @in = Stream::open(@options, mode="r", @input) unless @in.is_a? IO
57 @in.each_line do |line|
59 when /^([^:]+): (.*)$/
62 yield record unless record.empty?
65 raise "Bad record format: #{line}"
69 yield record unless record.empty?
74 alias :each :each_record
76 # Open Biopiece output stream if not open and puts record to the stream.
78 @out = Stream::open(@options, mode="w", @output) unless @out.is_a? IO
80 record.each do |key,value|
81 @out.print "#{key}: #{value}\n"
87 # Create a temporary directory inside the ENV["BP_TMP"] dir.
92 path = ENV["BP_TMP"] + "/" + [user, time + pid, pid, "bp_tmp"].join("_")
94 @status.set_tmpdir(path)
101 # Install signal handlers
102 %w( INT TERM QUIT ).each do |signal|
103 Signal.trap(signal) do
111 @in.close if @in.respond_to? :close
112 @out.close if @out.respond_to? :close
114 rescue Exception => exception
116 when 'INT' then exit_status = 'INTERRUPTED'
117 when 'TERM' then exit_status = 'TERMINATED'
119 exit_status = "DIED" unless exit_status =~ /INT|TERM|QUIT/
120 $stderr.puts exit_status
121 $stderr.puts exception.backtrace
125 FileUtils.remove_entry_secure(status.get_tmpdir)
126 status.log(exit_status)
134 # Error class for all exceptions to do with option casts.
135 class CastError < StandardError; end
138 # Class to handle casts of command line options. Each cast prescribes the long and
139 # short name of the option, the type, if it is mandatory, the default value, and
140 # allowed and disallowed values. An optional list of extra casts can be supplied,
141 # and the integrity of the casts are checked.
143 TYPES = %w[flag string list int uint float file file! files files! dir dir! genome]
144 MANDATORY = %w[long short type mandatory default allowed disallowed]
146 # Initialize cast object with an optional options cast list to which
147 # ubiquitous casts are added after which all casts are checked.
148 def initialize(cast_list=[])
149 @cast_list = cast_list
152 self.push *@cast_list
157 # Add ubiquitous options casts.
159 @cast_list << {:long => 'help', :short => '?', :type => 'flag', :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
160 @cast_list << {:long => 'stream_in', :short => 'I', :type => 'files!', :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
161 @cast_list << {:long => 'stream_out', :short => 'O', :type => 'file', :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
162 @cast_list << {:long => 'verbose', :short => 'v', :type => 'flag', :mandatory => false, :default => nil, :allowed => nil, :disallowed => nil}
165 # Check integrity of the casts.
172 # Check if all mandatory keys are present in casts and raise if not.
174 @cast_list.each do |cast|
175 MANDATORY.each do |mandatory|
176 raise CastError, "Missing symbol in cast: '#{mandatory.to_sym}'" unless cast.has_key? mandatory.to_sym
181 # Check if all values in casts are valid.
183 @cast_list.each do |cast|
185 check_val_short(cast)
187 check_val_mandatory(cast)
188 check_val_default(cast)
189 check_val_allowed(cast)
190 check_val_disallowed(cast)
194 # Check if the values to long are legal and raise if not.
195 def check_val_long(cast)
196 unless cast[:long].is_a? String and cast[:long].length > 1
197 raise CastError, "Illegal cast of long: '#{cast[:long]}'"
201 # Check if the values to short are legal and raise if not.
202 def check_val_short(cast)
203 unless cast[:short].is_a? String and cast[:short].length == 1
204 raise CastError, "Illegal cast of short: '#{cast[:short]}'"
208 # Check if values to type are legal and raise if not.
209 def check_val_type(cast)
212 type_hash[type] = true
215 unless type_hash.has_key? cast[:type]
216 raise CastError, "Illegal cast of type: '#{cast[:type]}'"
220 # Check if values to mandatory are legal and raise if not.
221 def check_val_mandatory(cast)
222 unless cast[:mandatory] == true or cast[:mandatory] == false
223 raise CastError, "Illegal cast of mandatory: '#{cast[:mandatory]}'"
227 # Check if values to default are legal and raise if not.
228 def check_val_default(cast)
229 unless cast[:default].nil? or
230 cast[:default].is_a? String or
231 cast[:default].is_a? Integer or
232 cast[:default].is_a? Float
233 raise CastError, "Illegal cast of default: '#{cast[:default]}'"
237 # Check if values to allowed are legal and raise if not.
238 def check_val_allowed(cast)
239 unless cast[:allowed].is_a? String or cast[:allowed].nil?
240 raise CastError, "Illegal cast of allowed: '#{cast[:allowed]}'"
244 # Check if values to disallowed are legal and raise if not.
245 def check_val_disallowed(cast)
246 unless cast[:disallowed].is_a? String or cast[:disallowed].nil?
247 raise CastError, "Illegal cast of disallowed: '#{cast[:disallowed]}'"
251 # Check cast for duplicate long or short options names.
254 @cast_list.each do |cast|
255 raise CastError, "Duplicate argument: '--#{cast[:long]}'" if check_hash.has_key? cast[:long]
256 raise CastError, "Duplicate argument: '-#{cast[:short]}'" if check_hash.has_key? cast[:short]
257 check_hash[cast[:long]] = true
258 check_hash[cast[:short]] = true
264 # Class for parsing argv using OptionParser according to given casts.
265 # Default options are set, file glob expressions expanded, and options are
266 # checked according to the casts. Usage information is printed and exit called
269 REGEX_LIST = /^(list|files|files!)$/
270 REGEX_INT = /^(int|uint)$/
271 REGEX_STRING = /^(string|file|file!|dir|dir!|genome)$/
273 def initialize(argv, casts, script_path, test=nil)
276 @script_path = script_path
280 # Parse options from argv using OptionParser and casts denoting long and
281 # short option names. Usage information is printed and exit called.
282 # A hash with options is returned.
286 option_parser = OptionParser.new do |option|
287 @casts.each do |cast|
288 if cast[:type] == 'flag'
289 option.on("-#{cast[:short]}", "--#{cast[:long]}") do |o|
290 @options[cast[:long]] = o
292 elsif cast[:type] =~ REGEX_LIST
293 option.on( "-#{cast[:short]}", "--#{cast[:long]} A", Array) do |a|
294 @options[cast[:long]] = a
296 elsif cast[:type] =~ REGEX_INT
297 option.on("-#{cast[:short]}", "--#{cast[:long]} I", Integer) do |i|
298 @options[cast[:long]] = i
300 elsif cast[:type] =~ REGEX_STRING
301 option.on("-#{cast[:short]}", "--#{cast[:long]} S", String) do |s|
302 @options[cast[:long]] = s
304 elsif cast[:type] == 'float'
305 option.on("-#{cast[:short]}", "--#{cast[:long]} F", Float) do |f|
306 @options[cast[:long]] = f
309 raise ArgumentError, "Unknown option type: '#{cast[:type]}'"
314 option_parser.parse!(@argv)
317 print_usage_and_exit(true)
318 elsif print_usage_short?
329 # Given the script name determine the path of the wiki file with the usage info.
331 path = ENV["BP_DIR"] + "/bp_usage/" + File.basename(@script_path) + ".wiki"
332 raise "No such wiki file: #{path}" unless File.file? path
336 # Check if full "usage info" should be printed.
337 def print_usage_full?
341 # Check if short "usage info" should be printed.
342 def print_usage_short?
345 elsif @options["stream_in"]
347 elsif @options["data_in"]
349 elsif wiki_path =~ /^(list_biopieces|list_genomes|list_mysql_databases|biostat)$/ # TODO get rid of this!
356 # Print usage info by Calling an external script 'print_wiki'
357 # using a system() call and exit. An optional 'full' flag
358 # outputs the full usage info.
359 def print_usage_and_exit(full=nil)
364 system("print_wiki --data_in #{wiki_path} --help")
366 system("print_wiki --data_in #{wiki_path}")
369 raise "Failed printing wiki: #{wiki_path}" unless $?.success?
375 # Set default options value from cast unless a value is set.
377 @casts.each do |cast|
379 @options[cast[:long]] = cast[:default] unless @options.has_key? cast[:long]
384 # Expands glob expressions to a full list of paths.
385 # Examples: "*.fna" or "foo.fna,*.fna" or "foo.fna,/bar/*.fna"
387 @casts.each do |cast|
388 if cast[:type] == 'files' or cast[:type] == 'files!'
389 if @options.has_key? cast[:long]
392 @options[cast[:long]].each do |path|
394 Dir.glob(path).each do |file|
395 files << file if File.file? file
402 @options[cast[:long]] = files
408 # Check all options according to casts.
410 @casts.each do |cast|
411 options_check_mandatory(cast)
412 options_check_int(cast)
413 options_check_uint(cast)
414 options_check_file(cast)
415 options_check_files(cast)
416 options_check_dir(cast)
417 options_check_allowed(cast)
418 options_check_disallowed(cast)
422 # Check if a mandatory option is set and raise if it isn't.
423 def options_check_mandatory(cast)
425 raise ArgumentError, "Mandatory argument: --#{cast[:long]}" unless @options.has_key? cast[:long]
429 # Check int type option and raise if not an integer.
430 def options_check_int(cast)
431 if cast[:type] == 'int' and @options.has_key? cast[:long]
432 unless @options[cast[:long]].is_a? Integer
433 raise ArgumentError, "Argument to --#{cast[:long]} must be an integer, not '#{@options[cast[:long]]}'"
438 # Check uint type option and raise if not an unsinged integer.
439 def options_check_uint(cast)
440 if cast[:type] == 'uint' and @options.has_key? cast[:long]
441 unless @options[cast[:long]].is_a? Integer and @options[cast[:long]] >= 0
442 raise ArgumentError, "Argument to --#{cast[:long]} must be an unsigned integer, not '#{@options[cast[:long]]}'"
447 # Check file! type argument and raise if file don't exists.
448 def options_check_file(cast)
449 if cast[:type] == 'file!' and @options.has_key? cast[:long]
450 raise ArgumentError, "No such file: '#{@options[cast[:long]]}'" unless File.file? @options[cast[:long]]
454 # Check files! type argument and raise if files don't exists.
455 def options_check_files(cast)
456 if cast[:type] == 'files!' and @options.has_key? cast[:long]
457 @options[cast[:long]].each do |path|
458 raise ArgumentError, "No such file: '#{path}'" unless File.file? path
463 # Check dir! type argument and raise if directory don't exist.
464 def options_check_dir(cast)
465 if cast[:type] == 'dir!' and @options.has_key? cast[:long]
466 raise ArgumentError, "No such directory: '#{@options[cast[:long]]}'" unless File.directory? @options[cast[:long]]
470 # Check options and raise unless allowed.
471 def options_check_allowed(cast)
472 if cast[:allowed] and @options.has_key? cast[:long]
474 cast[:allowed].split(',').each { |a| allowed_hash[a] = 1 }
476 raise ArgumentError, "Argument '#{@options[cast[:long]]}' to --#{cast[:long]} not allowed" unless allowed_hash.has_key? @options[cast[:long]]
480 # Check disallowed argument values and raise if disallowed.
481 def options_check_disallowed(cast)
482 if cast[:disallowed] and @options.has_key? cast[:long]
483 cast[:disallowed].split(',').each do |val|
484 raise ArgumentError, "Argument '#{@options[cast[:long]]}' to --#{cast[:long]} is disallowed" if val == @options[cast[:long]]
490 # Class for manipulating the execution status of Biopieces by setting a
491 # status file with a time stamp, process id, and command arguments. The
492 # status file is used for creating log entries and for displaying the
493 # runtime status of Biopieces.
495 # Write the status to a status file.
497 time0 = Time.new.strftime("%Y-%m-%d %X")
499 File.open(path, mode="w") { |file| file.puts [time0, ARGV.join(" ")].join(";") }
502 # Append the a temporary directory path to the status file.
503 def set_tmpdir(tmp_path)
504 status = File.open(path, mode="r").read.chomp
506 File.open(path, mode="w") do |fh|
507 fh << [status, tmp_path].join(";") + "\n"
511 # Extract the temporary directory path from the status file,
512 # and return this or nil if not found.
514 tmp_path = File.open(path, mode="r").read.chomp.split(";").last
516 File.directory?(tmp_path) ? tmp_path : nil
519 # Write the Biopiece status to the log file.
521 time1 = Time.new.strftime("%Y-%m-%d %X")
523 script = File.basename($0)
525 stream = File.open(path)
526 time0, args, tmp_dir = stream.first.split(";")
529 elap = time_diff(time0, time1)
530 command = [script, args].join(" ")
531 log_file = ENV["BP_LOG"] + "/biopieces.log"
533 File.open(log_file, mode="a") { |file| file.puts [time0, time1, elap, user, exit_status, command].join("\t") }
536 # Delete status file.
543 # Path to status file
546 script = File.basename($0)
548 path = ENV["BP_TMP"] + "/" + [user, script, pid, "status"].join(".")
551 # Get the elapsed time from the difference between two time stamps.
552 def time_diff(t0, t1)
553 Time.at((DateTime.parse(t1).to_time - DateTime.parse(t0).to_time).to_i).gmtime.strftime('%X')
559 # Open Biopieces output data stream for reading from stdin or a file
560 # specified in options["stream_in"] OR writing to stdout or a file
561 # specified in options["stream_out"].
562 def self.open(options, mode, stdio)
564 $stdin.tty? ? read(options["stream_in"]) : stdio
566 options["stream_out"] ? self.write(options["stream_out"], options["compress"]) : stdio
568 raise "Bad mode #{mode}"
574 # Opens a reads stream to a list of files.
576 self.zipped?(files) ? self.zread(files) : self.nread(files)
579 # Opens a write stream to a file and returns a _io_ object.
580 def self.write(file, zip=nil)
581 zip ? self.zwrite(file) : self.nwrite(file)
584 # Opens a list of gzipped files for reading and return an _io_ object.
585 def self.zread(files)
586 stdin, stdout, stderr = Open3.popen3("zcat " + files.join(' '));
592 # Opens a file for gzipped writing and return an _io_ object.
593 def self.zwrite(file)
594 stdin, stdout, stderr = Open3.popen3("gzip -f > #{file}")
600 # Opens a list of files for reading and return an _io_ object.
601 def self.nread(files)
602 stdin, stdout, stderr = Open3.popen3("cat " + files.join(' '));
608 # Opens a file for writing and return an _io_ object.
609 def self.nwrite(file)
610 File.open(file, mode="w")
613 # Test if a list of files are gzipped or not.
614 # Raises if files are mixed zipped and unzipped.
615 def self.zipped?(files)
619 type = `file #{file}`
621 if type =~ /gzip compressed/
622 type_hash[:gzip] = true
624 type_hash[:ascii] = true
628 raise "Mixture of zipped and unzipped files" if type_hash.size == 2
635 # Stuff to be done at begin such as setting the exit status
636 # and signal handlers
642 # Install signal handlers
643 %w( INT TERM QUIT ).each do |signal|
644 Signal.trap(signal) do
653 # Make sure that file streams are closed, tmpdir removed, and
654 # exit status is written to log file.
655 def do_at_exit(exit_status)
658 @in.close if @in.respond_to? :close
659 @out.close if @out.respond_to? :close
660 rescue Exception => exception
661 $stderr.puts "Exception caught!" # DEBUG
663 when 'INT' then exit_status = 'INTERRUPTED'
664 when 'TERM' then exit_status = 'TERMINATED'
666 exit_status = "DIED" unless exit_status =~ /INT|TERM|QUIT/
667 $stderr.puts exit_status
668 $stderr.puts exception.backtrace
672 FileUtils.remove_entry_secure(status.get_tmpdir)
673 status.log(exit_status)
675 puts "EXIT STATUS: #{exit_status}"
680 exit_status = do_at_begin
681 do_at_exit(exit_status)