mysociety/alaveteli

View on GitHub
lib/alaveteli_external_command.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'external_command'

class AlaveteliExternalCommand
  def self.run(program_name, *args)
    new(program_name).run(*args)
  end

  attr_reader :program_name, :command_args

  def initialize(program_name)
    @program_name = program_name
  end

  def add_args(*command_args)
    @command_args = command_args
    self
  end

  # Final argument can be a hash of options.
  # Valid options are:
  # :append_to - string to append the output of the process to
  # :append_errors_to - string to append the errors produced by the process to
  # :stdin_string - stdin string to pass to the process
  # :binary_output - boolean flag for treating the output as binary or text encoded with
  #                   the default external encoding (only significant in ruby 1.9 and above)
  # :binary_input - boolean flag for treating the input as binary or as text encoded with
  #                   the default external encoding (only significant in ruby 1.9 and above)
  # :memory_limit - maximum amount of memory (in bytes) available to the process
  # :timeout - maximum amount of time (in s) to allow the process to run for
  # :env - hash of environment variables to set for the process
  def run(*args)
    # Run an external program, and return its output.
    # Standard error is suppressed unless the program
    # fails (i.e. returns a non-zero exit status).
    # If the program fails, returns nil and writes any error to stderr.
    # TODO: calling code should be able to specify error stream - may want to log it or
    # otherwise act upon it.
    opts = {}
    opts = args.last if !args.empty? && args.last.is_a?(Hash)

    program_path = find_program!
    xc = ExternalCommand.new(program_path, *command_args, *args)
    begin
      xc.run
    rescue ExternalCommand::ChildUnterminated => e
      $stderr.puts(e.message)
      return nil
    end

    if !xc.exited
      # Crash or timeout
      if xc.timed_out
        $stderr.puts(%Q[External Command: "#{program_name} #{args.join(' ')}" timed out at #{opts[:timeout]}s])
      else
        $stderr.puts(%Q[External Command: "#{program_name} #{args.join(' ')}" exited abnormally])
      end
      $stderr.print(xc.err)
      nil

    elsif xc.status != 0
      # Error
      $stderr.puts(%Q[External Command: Error from command "#{program_name} #{args.join(' ')}":])
      $stderr.print(xc.err)
      nil
    elsif opts.key? :append_to
      opts[:append_to] << "\n\n"
    else

      xc.out
    end
  end

  def exist?
    find_program.present?
  end

  private

  def find_program
    return program_name if program_name =~ %r(^/)

    search_paths.each do |d|
      path = File.join(d, program_name)
      return path if File.file?(path) && File.executable?(path)
    end

    false
  end

  def find_program!
    find_program || raise(
      "Could not find #{program_name} in any of #{search_paths.join(', ')}"
    )
  end

  def search_paths
    paths = AlaveteliConfiguration.utility_search_path
    paths = ENV['PATH'].split(':') if paths.empty?
    paths
  end
end