tool_executor.rb 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. require 'constants'
  2. class ShellExecutionException < RuntimeError
  3. attr_reader :shell_result
  4. def initialize(shell_result)
  5. @shell_result = shell_result
  6. end
  7. end
  8. class ToolExecutor
  9. constructor :configurator, :tool_executor_helper, :streaminator, :system_wrapper
  10. def setup
  11. @tool_name = ''
  12. @executable = ''
  13. end
  14. # build up a command line from yaml provided config
  15. def build_command_line(tool_config, *args)
  16. @tool_name = tool_config[:name]
  17. @executable = tool_config[:executable]
  18. command = {}
  19. # basic premise is to iterate top to bottom through arguments using '$' as
  20. # a string replacement indicator to expand globals or inline yaml arrays
  21. # into command line arguments via substitution strings
  22. command[:line] = [
  23. @tool_executor_helper.osify_path_separators( expandify_element(@executable, *args) ),
  24. build_arguments(tool_config[:arguments], *args),
  25. ].join(' ').strip
  26. command[:options] = {
  27. :stderr_redirect => @tool_executor_helper.stderr_redirection(tool_config, @configurator.project_logging),
  28. :background_exec => tool_config[:background_exec]
  29. }
  30. return command
  31. end
  32. # shell out, execute command, and return response
  33. def exec(command, options={}, args=[])
  34. options[:boom] = true if (options[:boom].nil?)
  35. options[:stderr_redirect] = StdErrRedirect::NONE if (options[:stderr_redirect].nil?)
  36. options[:background_exec] = BackgroundExec::NONE if (options[:background_exec].nil?)
  37. # build command line
  38. command_line = [
  39. @tool_executor_helper.background_exec_cmdline_prepend( options ),
  40. command.strip,
  41. args,
  42. @tool_executor_helper.stderr_redirect_cmdline_append( options ),
  43. @tool_executor_helper.background_exec_cmdline_append( options ),
  44. ].flatten.compact.join(' ')
  45. shell_result = {}
  46. # depending on background exec option, we shell out differently
  47. if (options[:background_exec] != BackgroundExec::NONE)
  48. shell_result = @system_wrapper.shell_system( command_line )
  49. else
  50. shell_result = @system_wrapper.shell_backticks( command_line )
  51. end
  52. @tool_executor_helper.print_happy_results( command_line, shell_result, options[:boom] )
  53. @tool_executor_helper.print_error_results( command_line, shell_result, options[:boom] )
  54. # go boom if exit code isn't 0 (but in some cases we don't want a non-0 exit code to raise)
  55. raise ShellExecutionException.new(shell_result) if ((shell_result[:exit_code] != 0) and options[:boom])
  56. return shell_result
  57. end
  58. private #############################
  59. def build_arguments(config, *args)
  60. build_string = ''
  61. return nil if (config.nil?)
  62. # iterate through each argument
  63. # the yaml blob array needs to be flattened so that yaml substitution
  64. # is handled correctly, since it creates a nested array when an anchor is
  65. # dereferenced
  66. config.flatten.each do |element|
  67. argument = ''
  68. case(element)
  69. # if we find a simple string then look for string replacement operators
  70. # and expand with the parameters in this method's argument list
  71. when String then argument = expandify_element(element, *args)
  72. # if we find a hash, then we grab the key as a substitution string and expand the
  73. # hash's value(s) within that substitution string
  74. when Hash then argument = dehashify_argument_elements(element)
  75. end
  76. build_string.concat("#{argument} ") if (argument.length > 0)
  77. end
  78. build_string.strip!
  79. return build_string if (build_string.length > 0)
  80. return nil
  81. end
  82. # handle simple text string argument & argument array string replacement operators
  83. def expandify_element(element, *args)
  84. match = //
  85. to_process = nil
  86. args_index = 0
  87. # handle ${#} input replacement
  88. if (element =~ TOOL_EXECUTOR_ARGUMENT_REPLACEMENT_PATTERN)
  89. args_index = ($2.to_i - 1)
  90. if (args.nil? or args[args_index].nil?)
  91. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' expected valid argument data to accompany replacement operator #{$1}.", Verbosity::ERRORS)
  92. raise
  93. end
  94. match = /#{Regexp.escape($1)}/
  95. to_process = args[args_index]
  96. end
  97. # simple string argument: replace escaped '\$' and strip
  98. element.sub!(/\\\$/, '$')
  99. element.strip!
  100. # handle inline ruby execution
  101. if (element =~ RUBY_EVAL_REPLACEMENT_PATTERN)
  102. element.replace(eval($1))
  103. end
  104. build_string = ''
  105. # handle array or anything else passed into method to be expanded in place of replacement operators
  106. case (to_process)
  107. when Array then to_process.each {|value| build_string.concat( "#{element.sub(match, value.to_s)} " ) } if (to_process.size > 0)
  108. else build_string.concat( element.sub(match, to_process.to_s) )
  109. end
  110. # handle inline ruby string substitution
  111. if (build_string =~ RUBY_STRING_REPLACEMENT_PATTERN)
  112. build_string.replace(@system_wrapper.module_eval(build_string))
  113. end
  114. return build_string.strip
  115. end
  116. # handle argument hash: keys are substitution strings, values are data to be expanded within substitution strings
  117. def dehashify_argument_elements(hash)
  118. build_string = ''
  119. elements = []
  120. # grab the substitution string (hash key)
  121. substitution = hash.keys[0].to_s
  122. # grab the string(s) to squirt into the substitution string (hash value)
  123. expand = hash[hash.keys[0]]
  124. if (expand.nil?)
  125. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' could not expand nil elements for substitution string '#{substitution}'.", Verbosity::ERRORS)
  126. raise
  127. end
  128. # array-ify expansion input if only a single string
  129. expansion = ((expand.class == String) ? [expand] : expand)
  130. expansion.each do |item|
  131. # code eval substitution
  132. if (item =~ RUBY_EVAL_REPLACEMENT_PATTERN)
  133. elements << eval($1)
  134. # string eval substitution
  135. elsif (item =~ RUBY_STRING_REPLACEMENT_PATTERN)
  136. elements << @system_wrapper.module_eval(item)
  137. # global constants
  138. elsif (@system_wrapper.constants_include?(item))
  139. const = Object.const_get(item)
  140. if (const.nil?)
  141. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' found constant '#{item}' to be nil.", Verbosity::ERRORS)
  142. raise
  143. else
  144. elements << const
  145. end
  146. elsif (item.class == Array)
  147. elements << item
  148. elsif (item.class == String)
  149. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' cannot expand nonexistent value '#{item}' for substitution string '#{substitution}'.", Verbosity::ERRORS)
  150. raise
  151. else
  152. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' cannot expand value having type '#{item.class}' for substitution string '#{substitution}'.", Verbosity::ERRORS)
  153. raise
  154. end
  155. end
  156. # expand elements (whether string or array) into substitution string & replace escaped '\$'
  157. elements.flatten!
  158. elements.each do |element|
  159. build_string.concat( substitution.sub(/([^\\]*)\$/, "\\1#{element}") ) # don't replace escaped '\$' but allow us to replace just a lonesome '$'
  160. build_string.gsub!(/\\\$/, '$')
  161. build_string.concat(' ')
  162. end
  163. return build_string.strip
  164. end
  165. end