brew/Library/Homebrew/system_command.rb
Mike McQuaid d7eca0b57c
Use Bundler to manage vendor directory
Rather than having to manually keep track of what version each thing in
here is and copy files around by hand on update let's use Bundler's
standalone mode and careful use of `.gitignore` to help us do it.

This means a `bundle update --standalone` will allow us to update all
gems in vendor.

We could consider vendoring other gems this way in future but I'd
suggest only doing this for gems with no dependencies or at least gems
with no native extensions. The only gem this applies to that we
currently use is `ruby-prof` and I'm not convinced it's widely used
enough to warrant vendoring for everyone. Perhaps that's another
criteria: it should be functionality that's used by non-developer
commands and/or normal Homebrew usage.
2018-09-13 15:24:18 +01:00

221 lines
5.2 KiB
Ruby

require "open3"
require "ostruct"
require "plist"
require "shellwords"
require "extend/io"
require "extend/hash_validator"
using HashValidator
require "extend/predicable"
def system_command(*args)
SystemCommand.run(*args)
end
def system_command!(*args)
SystemCommand.run!(*args)
end
class SystemCommand
extend Predicable
def self.run(executable, **options)
new(executable, **options).run!
end
def self.run!(command, **options)
run(command, **options, must_succeed: true)
end
def run!
puts command.shelljoin.gsub(/\\=/, "=") if verbose? || ARGV.debug?
@output = []
each_output_line do |type, line|
case type
when :stdout
$stdout << line if print_stdout?
@output << [:stdout, line]
when :stderr
$stderr << line if print_stderr?
@output << [:stderr, line]
end
end
assert_success if must_succeed?
result
end
def initialize(executable, args: [], sudo: false, env: {}, input: [], must_succeed: false,
print_stdout: false, print_stderr: true, verbose: false, **options)
@executable = executable
@args = args
@sudo = sudo
@input = [*input]
@print_stdout = print_stdout
@print_stderr = print_stderr
@verbose = verbose
@must_succeed = must_succeed
options.assert_valid_keys!(:chdir)
@options = options
@env = env
@env.keys.grep_v(/^[\w&&\D]\w*$/) do |name|
raise ArgumentError, "Invalid variable name: '#{name}'"
end
end
def command
[*sudo_prefix, *env_args, executable.to_s, *expanded_args]
end
private
attr_reader :executable, :args, :input, :options, :env
attr_predicate :sudo?, :print_stdout?, :print_stderr?, :verbose?, :must_succeed?
def env_args
set_variables = env.reject { |_, value| value.nil? }
.map do |name, value|
sanitized_name = Shellwords.escape(name)
sanitized_value = Shellwords.escape(value)
"#{sanitized_name}=#{sanitized_value}"
end
return [] if set_variables.empty?
["env", *set_variables]
end
def sudo_prefix
return [] unless sudo?
askpass_flags = ENV.key?("SUDO_ASKPASS") ? ["-A"] : []
["/usr/bin/sudo", *askpass_flags, "-E", "--"]
end
def assert_success
return if @status.success?
raise ErrorDuringExecution.new(command,
status: @status,
output: @output)
end
def expanded_args
@expanded_args ||= args.map do |arg|
if arg.respond_to?(:to_path)
File.absolute_path(arg)
elsif arg.is_a?(Integer) || arg.is_a?(Float) || arg.is_a?(URI)
arg.to_s
else
arg.to_str
end
end
end
def each_output_line(&b)
executable, *args = command
raw_stdin, raw_stdout, raw_stderr, raw_wait_thr =
Open3.popen3(env, [executable, executable], *args, **options)
write_input_to(raw_stdin)
raw_stdin.close_write
each_line_from [raw_stdout, raw_stderr], &b
@status = raw_wait_thr.value
rescue SystemCallError => e
@status = $CHILD_STATUS
@output << [:stderr, e.message]
end
def write_input_to(raw_stdin)
input.each(&raw_stdin.method(:write))
end
def each_line_from(sources)
loop do
readable_sources, = IO.select(sources)
readable_sources = readable_sources.reject(&:eof?)
break if readable_sources.empty?
readable_sources.each do |source|
begin
line = source.readline_nonblock || ""
type = (source == sources[0]) ? :stdout : :stderr
yield(type, line)
rescue IO::WaitReadable, EOFError
next
end
end
end
sources.each(&:close_read)
end
def result
Result.new(command, @output, @status)
end
class Result
attr_accessor :command, :status, :exit_status
def initialize(command, output, status)
@command = command
@output = output
@status = status
@exit_status = status.exitstatus
end
def stdout
@stdout ||= @output.select { |type,| type == :stdout }
.map { |_, line| line }
.join
end
def stderr
@stderr ||= @output.select { |type,| type == :stderr }
.map { |_, line| line }
.join
end
def success?
@exit_status.zero?
end
def to_ary
[stdout, stderr, status]
end
def plist
@plist ||= begin
output = stdout
if /\A(?<garbage>.*?)<\?\s*xml/m =~ output
output = output.sub(/\A#{Regexp.escape(garbage)}/m, "")
warn_plist_garbage(garbage)
end
if %r{<\s*/\s*plist\s*>(?<garbage>.*?)\Z}m =~ output
output = output.sub(/#{Regexp.escape(garbage)}\Z/, "")
warn_plist_garbage(garbage)
end
Plist.parse_xml(output)
end
end
def warn_plist_garbage(garbage)
return unless ARGV.verbose?
return unless garbage =~ /\S/
opoo "Received non-XML output from #{Formatter.identifier(command.first)}:"
$stderr.puts garbage.strip
end
private :warn_plist_garbage
end
end