| 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 |