blob: 1af6750c2043bf508b8dd44075701f05f88d5f20 [file] [log] [blame]
module Pod
# Module which provides support for running executables.
#
# In a class it can be used as:
#
# extend Executable
# executable :git
#
# This will create two methods `git` and `git!` both accept a command but
# the later will raise on non successful executions. The methods return the
# output of the command.
#
module Executable
# Creates the methods for the executable with the given name.
#
# @param [Symbol] name
# the name of the executable.
#
# @return [void]
#
def executable(name)
define_method(name) do |*command|
Executable.execute_command(name, Array(command).flatten, false)
end
define_method(name.to_s + '!') do |*command|
Executable.execute_command(name, Array(command).flatten, true)
end
end
# Executes the given command displaying it if in verbose mode.
#
# @param [String] executable
# The binary to use.
#
# @param [Array<#to_s>] command
# The command to send to the binary.
#
# @param [Bool] raise_on_failure
# Whether it should raise if the command fails.
#
# @raise If the executable could not be located.
#
# @raise If the command fails and the `raise_on_failure` is set to true.
#
# @return [String] the output of the command (STDOUT and STDERR).
#
def self.execute_command(executable, command, raise_on_failure = true)
bin = which!(executable)
command = command.map(&:to_s)
full_command = "#{bin} #{command.join(' ')}"
if Config.instance.verbose?
UI.message("$ #{full_command}")
stdout = Indenter.new(STDOUT)
stderr = Indenter.new(STDERR)
else
stdout = Indenter.new
stderr = Indenter.new
end
status = popen3(bin, command, stdout, stderr)
stdout = stdout.join
stderr = stderr.join
output = stdout + stderr
unless status.success?
if raise_on_failure
raise Informative, "#{full_command}\n\n#{output}"
else
UI.message("[!] Failed: #{full_command}".red)
end
end
output
end
# Returns the absolute path to the binary with the given name on the current
# `PATH`, or `nil` if none is found.
#
# @param [String] program
# The name of the program being searched for.
#
# @return [String,Nil] The absolute path to the given program, or `nil` if
# it wasn't found in the current `PATH`.
#
def self.which(program)
program = program.to_s
paths = ENV.fetch('PATH') { '' }.split(File::PATH_SEPARATOR)
paths.unshift('./')
paths.uniq!
paths.each do |path|
bin = File.expand_path(program, path)
if File.file?(bin) && File.executable?(bin)
return bin
end
end
nil
end
# Returns the absolute path to the binary with the given name on the current
# `PATH`, or raises if none is found.
#
# @param [String] program
# The name of the program being searched for.
#
# @return [String] The absolute path to the given program.
#
def self.which!(program)
which(program).tap do |bin|
raise Informative, "Unable to locate the executable `#{program}`" unless bin
end
end
# Runs the given command, capturing the desired output.
#
# @param [String] bin
# The binary to use.
#
# @param [Array<#to_s>] command
# The command to send to the binary.
#
# @param [Symbol] capture
# Whether it should raise if the command fails.
#
# @raise If the executable could not be located.
#
# @return [(String, Process::Status)]
# The desired captured output from the command, and the status from
# running the command.
#
def self.capture_command(executable, command, capture: :merge)
bin = which!(executable)
require 'open3'
command = command.map(&:to_s)
case capture
when :merge then Open3.capture2e(bin, *command)
when :both then Open3.capture3(bin, *command)
when :out then Open3.capture3(bin, *command).values_at(0, -1)
when :err then Open3.capture3(bin, *command).drop(1)
when :none then Open3.capture3(bin, *command).last
end
end
private
def self.popen3(bin, command, stdout, stderr)
require 'open3'
Open3.popen3(bin, *command) do |i, o, e, t|
reader(o, stdout)
reader(e, stderr)
i.close
status = t.value
o.flush
e.flush
sleep(0.01)
status
end
end
def self.reader(input, output)
Thread.new do
buf = ''
begin
loop do
buf << input.readpartial(4096)
loop do
string, separator, buf = buf.partition(/[\r\n]/)
if separator.empty?
buf = string
break
end
output << (string << separator)
end
end
rescue EOFError
output << (buf << $/) unless buf.empty?
end
end
end
#-------------------------------------------------------------------------#
# Helper class that allows to write to an {IO} instance taking into account
# the UI indentation level.
#
class Indenter < ::Array
# @return [Fixnum] The indentation level of the UI.
#
attr_accessor :indent
# @return [IO] the {IO} to which the output should be printed.
#
attr_accessor :io
# Init a new Indenter
#
# @param [IO] io @see io
#
def initialize(io = nil)
@io = io
@indent = ' ' * UI.indentation_level
end
# Stores a portion of the output and prints it to the {IO} instance.
#
# @param [String] value
# the output to print.
#
# @return [void]
#
def <<(value)
super
io << "#{indent}#{value}" if io
end
end
end
end