Here's a snippet for realtime stdout with optional stdin data. You need to use IO.select
to peek into the streams to see if they're readable.
require 'open3'
class Runner
class Result
attr_accessor :status
attr_reader :stdout, :stderr
def initialize
@stdout = +''
@stderr = +''
@status = -127
end
def success?
status.zero?
end
end
attr_reader :result
def initialize(cmd, stdin: nil, print_to: nil)
@stdin = stdin
@cmd = cmd
@result = Result.new
@print_to = print_to
end
def run
Open3.popen3(@cmd) do |stdin, stdout, stderr, wait_thr|
# Dump the stdin-data into the command's stdin:
unless stdin.closed?
stdin.write(@stdin) if @stdin
stdin.close
end
until [stdout, stderr].all?(&:eof?)
readable = IO.select([stdout, stderr])
next unless readable&.first
readable.first.each do |stream|
data = +''
begin
stream.read_nonblock(1024, data)
rescue EOFError
# ignore, it's expected for read_nonblock to raise EOFError
# when all is read
end
next if data.empty?
if stream == stdout
result.stdout << data
@print_to << data if @print_to
else
result.stderr << data
@print_to << data if @print_to
end
end
end
result.status = wait_thr.value.exitstatus
end
result
end
end
result = Runner.new('ls -al').run
puts "Exit status: %d" % result.status
puts "Stdout:"
puts result.stdout
puts "Stderr:"
puts result.stderr
# Print as it goes:
result = Runner.new('ls -al', print_to: $stdout).run
If you need to simulate real time stdin (keypresses), then you need to create some sort of matcher for the stdout data stream and write the responses to the command's stdin when the expected prompt comes through the stream. In that case you are probably better off with PTY.spawn
or using a 3rd party gem.
You could also write the stdin data to a tempfile and use the shell's own redirection:
require 'tempfile'
tempfile = Tempfile.new
tempfile.write(stdin_data)
tempfile.close
system(%(do_stuff < "#{tempfile.path}"))
tempfile.unlink