lib/bio/command.rb in bio-1.2.1 vs lib/bio/command.rb in bio-1.3.0

- old
+ new

@@ -1,20 +1,23 @@ # # = bio/command.rb - general methods for external command execution # -# Copyright:: Copyright (C) 2003-2006 +# Copyright:: Copyright (C) 2003-2008 # Naohisa Goto <ng@bioruby.org>, # Toshiaki Katayama <k@bioruby.org> # License:: The Ruby License # -# $Id: command.rb,v 1.17 2007/04/05 23:35:39 trevor Exp $ +# $Id:$ # require 'open3' require 'uri' require 'open-uri' +require 'cgi' require 'net/http' +require 'tmpdir' +require 'fileutils' module Bio # = Bio::Command # @@ -30,10 +33,14 @@ UNESCAPABLE_CHARS = /[\x00-\x08\x10-\x1a\x1c-\x1f\x7f\xff]/n module_function # Escape special characters in command line string for cmd.exe on Windows. + # --- + # *Arguments*: + # * (required) _str_: String + # *Returns*:: String object def escape_shell_windows(str) str = str.to_s raise 'cannot escape control characters' if UNESCAPABLE_CHARS =~ str if QUOTE_CHARS_WINDOWS =~ str then '"' + str.gsub(/\"/, '""') + '"' @@ -41,27 +48,39 @@ String.new(str) end end # Escape special characters in command line string for UNIX shells. + # --- + # *Arguments*: + # * (required) _str_: String + # *Returns*:: String object def escape_shell_unix(str) str = str.to_s raise 'cannot escape control characters' if UNESCAPABLE_CHARS =~ str str.gsub(UNSAFE_CHARS_UNIX) { |x| "\\#{x}" } end # Escape special characters in command line string. + # --- + # *Arguments*: + # * (required) _str_: String + # *Returns*:: String object def escape_shell(str) case RUBY_PLATFORM when /mswin32|bccwin32/ escape_shell_windows(str) else escape_shell_unix(str) end end # Generate command line string with special characters escaped. + # --- + # *Arguments*: + # * (required) _ary_: Array containing String objects + # *Returns*:: String object def make_command_line(ary) case RUBY_PLATFORM when /mswin32|bccwin32/ make_command_line_windows(ary) else @@ -69,36 +88,96 @@ end end # Generate command line string with special characters escaped # for cmd.exe on Windows. + # --- + # *Arguments*: + # * (required) _ary_: Array containing String objects + # *Returns*:: String object def make_command_line_windows(ary) ary.collect { |str| escape_shell_windows(str) }.join(" ") end # Generate command line string with special characters escaped # for UNIX shells. + # --- + # *Arguments*: + # * (required) _ary_: Array containing String objects + # *Returns*:: String object def make_command_line_unix(ary) ary.collect { |str| escape_shell_unix(str) }.join(" ") end + # Returns an Array of command-line command and arguments + # that can be safely passed to Kernel.exec etc. + # If the given array is already safe (or empty), returns the given array. + # --- + # *Arguments*: + # * (required) _ary_: Array + # *Returns*:: Array + def safe_command_line_array(ary) + ary = ary.to_ary + return ary if ary.size >= 2 or ary.empty? + if ary.size != 1 then + raise 'Bug: assersion of ary.size == 1 failed' + end + arg0 = ary[0] + begin + arg0 = arg0.to_ary + rescue NoMethodError + arg0 = [ arg0, arg0 ] + end + [ arg0 ] + end + # Executes the program. Automatically select popen for Windows # environment and fork for the others. # A block must be given. An IO object is passed to the block. - def call_command(cmd, &block) + # + # Available options: + # :chdir => "path" : changes working directory to the specified path. + # + # --- + # *Arguments*: + # * (required) _cmd_: Array containing String objects + # * (optional) _options_: Hash + # *Returns*:: (undefined) + def call_command(cmd, options = {}, &block) #:yields: io case RUBY_PLATFORM when /mswin32|bccwin32/ - call_command_popen(cmd, &block) + call_command_popen(cmd, options, &block) else - call_command_fork(cmd, &block) + call_command_fork(cmd, options, &block) end end # Executes the program via IO.popen for OS which doesn't support fork. # A block must be given. An IO object is passed to the block. - def call_command_popen(cmd) + # --- + # *Arguments*: + # * (required) _cmd_: Array containing String objects + # * (optional) _options_: Hash + # *Returns*:: (undefined) + def call_command_popen(cmd, options = {}) str = make_command_line(cmd) + # processing options + if dir = options[:chdir] then + case RUBY_PLATFORM + when /mswin32|bccwin32/ + # Unix-like dir separator is changed to Windows dir separator + # by using String#gsub. + dirstr = dir.gsub(/\//, "\\") + chdirstr = make_command_line([ 'cd', '/D', dirstr ]) + str = chdirstr + ' && ' + str + else + # UNIX shell + chdirstr = make_command_line([ 'cd', dir ]) + str = chdirstr + ' && ' + str + end + end + # call command by using IO.popen IO.popen(str, "w+") do |io| io.sync = true yield io end end @@ -106,18 +185,33 @@ # Executes the program via fork (by using IO.popen("-")) and exec. # A block must be given. An IO object is passed to the block. # # From the view point of security, this method is recommended # rather than call_command_popen. - def call_command_fork(cmd) + # + # --- + # *Arguments*: + # * (required) _cmd_: Array containing String objects + # * (optional) _options_: Hash + # *Returns*:: (undefined) + def call_command_fork(cmd, options = {}) + dir = options[:chdir] + cmd = safe_command_line_array(cmd) IO.popen("-", "r+") do |io| if io then # parent yield io else # child + # chdir to options[:chdir] if available begin + Dir.chdir(dir) if dir + rescue Exception + Process.exit!(1) + end + # executing the command + begin Kernel.exec(*cmd) rescue Errno::ENOENT, Errno::EACCES Process.exit!(127) rescue Exception end @@ -128,84 +222,109 @@ # Executes the program via Open3.popen3 # A block must be given. IO objects are passed to the block. # # You would use this method only when you really need to get stderr. + # + # --- + # *Arguments*: + # * (required) _cmd_: Array containing String objects + # *Returns*:: (undefined) def call_command_open3(cmd) - cmd = cmd.collect { |x| x.to_s } + cmd = safe_command_line_array(cmd) Open3.popen3(*cmd) do |pin, pout, perr| yield pin, pout, perr end end # Executes the program with the query (String) given to the standard input, # waits the program termination, and returns the output data printed to the # standard output as a string. # # Automatically select popen for Windows environment and fork for the others. - def query_command(cmd, query = nil) + # + # Available options: + # :chdir => "path" : changes working directory to the specified path. + # + # --- + # *Arguments*: + # * (required) _cmd_: Array containing String objects + # * (optional) _query_: String + # * (optional) _options_: Hash + # *Returns*:: String or nil + def query_command(cmd, query = nil, options = {}) case RUBY_PLATFORM when /mswin32|bccwin32/ - query_command_popen(cmd, query) + query_command_popen(cmd, query, options) else - query_command_fork(cmd, query) + query_command_fork(cmd, query, options) end end # Executes the program with the query (String) given to the standard input, # waits the program termination, and returns the output data printed to the # standard output as a string. # # IO.popen is used for OS which doesn't support fork. - def query_command_popen(cmd, query = nil) - str = make_command_line(cmd) - IO.popen(str, "w+") do |io| + # + # --- + # *Arguments*: + # * (required) _cmd_: Array containing String objects + # * (optional) _query_: String + # * (optional) _options_: Hash + # *Returns*:: String or nil + def query_command_popen(cmd, query = nil, options = {}) + ret = nil + call_command_popen(cmd, options) do |io| io.sync = true io.print query if query io.close_write - io.read + ret = io.read end + ret end # Executes the program with the query (String) given to the standard input, # waits the program termination, and returns the output data printed to the # standard output as a string. # # Fork (by using IO.popen("-")) and exec is used to execute the program. # # From the view point of security, this method is recommended - # rather than query_popen. - def query_command_fork(cmd, query = nil) - IO.popen("-", "r+") do |io| - if io then - # parent - io.sync = true - io.print query if query - io.close_write - io.read - else - # child - begin - Kernel.exec(*cmd) - rescue Errno::ENOENT, Errno::EACCES - Process.exit!(127) - rescue Exception - end - Process.exit!(1) - end + # rather than query_command_popen. + # + # --- + # *Arguments*: + # * (required) _cmd_: Array containing String objects + # * (optional) _query_: String + # * (optional) _options_: Hash + # *Returns*:: String or nil + def query_command_fork(cmd, query = nil, options = {}) + ret = nil + call_command_fork(cmd, options) do |io| + io.sync = true + io.print query if query + io.close_write + ret = io.read end + ret end # Executes the program via Open3.popen3 with the query (String) given # to the stain, waits the program termination, and # returns the data from stdout and stderr as an array of the strings. # - # From the view point of security, this method is recommended - # rather than exec_local_popen. + # You would use this method only when you really need to get stderr. + # + # --- + # *Arguments*: + # * (required) _cmd_: Array containing String objects + # * (optional) _query_: String + # *Returns*:: Array containing 2 objects: output string (or nil) and stderr string (or nil) def query_command_open3(cmd, query = nil) errorlog = nil - cmd = cmd.collect { |x| x.to_s } + cmd = safe_command_line_array(cmd) Open3.popen3(*cmd) do |pin, pout, perr| perr.sync = true t = Thread.start { errorlog = perr.read } begin pin.print query if query @@ -216,21 +335,97 @@ end [ output, errorlog ] end end - # Same as OpenURI.open_uri(uri).read. + # Same as FileUtils.remove_entry_secure after Ruby 1.8.3. + # In Ruby 1.8.2 or previous version, it only shows warning message + # and does nothing. + # + # It is strongly recommended using Ruby 1.8.5 or later. + # --- + # *Arguments*: + # * (required) _path_: String + # * (optional) _force_: boolean + def remove_entry_secure(path, force = false) + begin + FileUtils.remove_entry_secure(path, force) + rescue NoMethodError + warn "The temporary file or directory is not removed because of the lack of FileUtils.remove_entry_secure. Use Ruby 1.8.3 or later (1.8.5 or later is strongly recommended): #{path}" + nil + end + end + + # Backport of Dir.mktmpdir in Ruby 1.9. + # + # Same as Dir.mktmpdir(prefix_suffix) in Ruby 1.9 except that + # prefix must be a String, nil, or omitted. + # + # --- + # *Arguments*: + # * (optional) _prefix_: String + # + def mktmpdir(prefix = 'd', tmpdir = nil, &block) + prefix = prefix.to_str + begin + Dir.mktmpdir(prefix, tmpdir, &block) + rescue NoMethodError + suffix = '' + # backported from Ruby 1.9.0. + # ***** Below is excerpted from Ruby 1.9.0's lib/tmpdir.rb **** + # ***** Be careful about copyright. **** + tmpdir ||= Dir.tmpdir + t = Time.now.strftime("%Y%m%d") + n = nil + begin + path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}" + path << "-#{n}" if n + path << suffix + Dir.mkdir(path, 0700) + rescue Errno::EEXIST + n ||= 0 + n += 1 + retry + end + + if block_given? + begin + yield path + ensure + remove_entry_secure path + end + else + path + end + # ***** Above is excerpted from Ruby 1.9.0's lib/tmpdir.rb **** + end + end + + # Same as OpenURI.open_uri(uri).read + # and + # it uses proxy if an environment variable (same as OpenURI.open_uri) + # is set. + # + # --- + # *Arguments*: + # * (required) _uri_: URI object or String + # *Returns*:: String def read_uri(uri) OpenURI.open_uri(uri).read end # Same as: # Net::HTTP.start(address, port) # and # it uses proxy if an environment variable (same as OpenURI.open_uri) # is set. # + # --- + # *Arguments*: + # * (required) _address_: String containing host name or IP address + # * (optional) _port_: port (sanme as Net::HTTP::start) + # *Returns*:: (same as Net::HTTP::start except for proxy support) def start_http(address, port = 80, &block) uri = URI.parse("http://#{address}:#{port}") # Note: URI#find_proxy is an unofficial method defined in open-uri.rb. # If the spec of open-uri.rb would be changed, we should change below. if proxyuri = uri.find_proxy then @@ -246,10 +441,15 @@ # Net::HTTP.new(address, port) # and # it uses proxy if an environment variable (same as OpenURI.open_uri) # is set. # + # --- + # *Arguments*: + # * (required) _address_: String containing host name or IP address + # * (optional) _port_: port (sanme as Net::HTTP::start) + # *Returns*:: (same as Net::HTTP.new except for proxy support) def new_http(address, port = 80) uri = URI.parse("http://#{address}:#{port}") # Note: URI#find_proxy is an unofficial method defined in open-uri.rb. # If the spec of open-uri.rb would be changed, we should change below. if proxyuri = uri.find_proxy then @@ -259,20 +459,56 @@ Net::HTTP.new(address, port) end end # Same as: + # http = Net::HTTP.new(...); http.post_form(path, params) + # and + # it uses proxy if an environment variable (same as OpenURI.open_uri) + # is set. + # In addition, +header+ can be set. + # (Note that Content-Type and Content-Length are automatically + # set by default.) + # +uri+ must be a URI object, +params+ must be a hash, and + # +header+ must be a hash. + # + # --- + # *Arguments*: + # * (required) _http_: Net::HTTP object or compatible object + # * (required) _path_: String + # * (optional) _params_: Hash containing parameters + # * (optional) _header_: Hash containing header strings + # *Returns*:: (same as Net::HTTP::post_form) + def http_post_form(http, path, params = nil, header = {}) + data = make_cgi_params(params) + + hash = { + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Content-Length' => data.length.to_s + } + hash.update(header) + + http.post(path, data, hash) + end + + # Same as: # Net::HTTP.post_form(uri, params) # and # it uses proxy if an environment variable (same as OpenURI.open_uri) # is set. # In addition, +header+ can be set. # (Note that Content-Type and Content-Length are automatically # set by default.) # +uri+ must be a URI object, +params+ must be a hash, and # +header+ must be a hash. # + # --- + # *Arguments*: + # * (required) _uri_: URI object or String + # * (optional) _params_: Hash containing parameters + # * (optional) _header_: Hash containing header strings + # *Returns*:: (same as Net::HTTP::post_form) def post_form(uri, params = nil, header = {}) unless uri.is_a?(URI) uri = URI.parse(uri) end @@ -287,10 +523,17 @@ start_http(uri.host, uri.port) do |http| http.post(uri.path, data, hash) end end + # Builds parameter string for from Hash of parameters for + # application/x-www-form-urlencoded. + # + # --- + # *Arguments*: + # * (required) _params_: Hash containing parameters + # *Returns*:: String def make_cgi_params(params) data = "" case params when Hash data = params.map do |key, val| @@ -308,27 +551,40 @@ data = params.map do |key, val| make_cgi_params_key_value(key, val) end.join('&') when String data = params.map do |str| - URI.escape(str.strip) + key, val = str.split(/\=/, 2) + if val then + make_cgi_params_key_value(key, val) + else + CGI.escape(str) + end end.join('&') end when String data = URI.escape(params.strip) end return data end + # Builds parameter string for from a key string and a value (or values) + # for application/x-www-form-urlencoded. + # + # --- + # *Arguments*: + # * (required) _key_: String + # * (required) _value_: String or Array containing String + # *Returns*:: String def make_cgi_params_key_value(key, value) result = [] case value when Array value.each do |val| - result << [key, val].map {|x| URI.escape(x.to_s) }.join('=') + result << [key, val].map {|x| CGI.escape(x.to_s) }.join('=') end else - result << [key, value].map {|x| URI.escape(x.to_s) }.join('=') + result << [key, value].map {|x| CGI.escape(x.to_s) }.join('=') end return result end end # module Command