Anyone who has ever tried to control (i.e., tried to send more than one command to) an interpreter using either of these recognise their shortcomings immediately: the child process STDOUT cannot be read until STDIN has been closed.
Fortunately, the pty module (helpfully mentioned in the notable exception) allows the interpreter to be controlled properly as long as the OS supports psuedo-terminals -- that is, as long as it is a UNIX-like OS (i.e. Linux, OS X, *BSD... basically every desktop/server OS but Windows).
It is a bit tricky to get working well, due to the problem of not knowing for sure whether the child process is preparing more data to write to STDOUT, or is waiting for another command on STDIN.
The following implementation wraps the praat speech analysis software. It uses the praat prompt ('Praat > ') to determine when the child process is ready for more input (i.e., when it can stop reading from STDOUT).
#!/usr/bin/env ruby
require 'pty'
module Praat
class Interpreter
attr_reader :stdout, :stdin, :pid
PROMPT='Praat > '
def initialize(program='praat')
@stdout, @stdin, @pid = PTY.spawn( 'praat', '-' )
# Read initial prompt from pipe
read_until_prompt
if block_given?
yield self
@stdin.close
@stdout.close
@stdin = @stdout = @pid = nil
end
end
def read_until_prompt
outbuf = buf = ''
# Read from child STDOUT until > 0 bytes have been read
# (i.e. wait for child process to finish reading input)
while buf.length == 0
begin
IO.select([@stdout]) # block until child process is ready
@stdout.read_nonblock( 1024, buf )
rescue Exception => e
buf = '' # READ failure! Try again.
end
end
# Read from child STDOUT until 0 bytes are read or a line ending in a
# prompt (i.e. next input prompt) was encountered.
while buf.length > 0
outbuf << buf
# complete read if next input prompt is encountered
break if outbuf =~ /#{PROMPT}$/
begin
buf = ''
@stdout.read_nonblock( 1024, buf )
rescue Errno::EAGAIN => e
IO.select([@stdout]) # block until child process is ready
retry
rescue Exception => e
buf='' # READ failure. Exit loop.
end
end
# Return output of interpreter as an array of lines
return outbuf.split("\n").each { |x| x.chomp! }
end
# Send a command to the interpreter. Returns an array of the output.
# If include_prompts is true, lines beginning with a prompt will NOT be
# stripped from the output.
def send( command, include_prompts=false )
@stdin.write(command + "\n")
outbuf = read_until_prompt
# Ignore all ECHOed lines before the first (input) prompt
first_prompt = outbuf.find_index { |x| x =~ /^#{PROMPT}/ }
result = outbuf.slice(first_prompt, outbuf.length-first_prompt)
# Return full results, or the results with prompt lines removed
result = result.select {|x| x !~ /^#{PROMPT}/} if not include_prompts
yield result if block_given?
return result
end
end
end
if __FILE__ == $0
puts 'Testing block implementation'
Praat::Interpreter.new() { |p| puts p.send('echo BLOCK TEST') }
Praat::Interpreter.new() do |p|
p.send('echo Full Output', true).each { |line| puts "\t" + line }
end
puts 'Testing object implementation'
praat = Praat::Interpreter.new()
lines = []
lines.concat praat.send('echo OBJ TEST 1' )
lines.concat praat.send('echo OBJ TEST 2' )
lines.concat praat.send('echo OBJ TEST 3' )
puts lines.inspect
end
require 'pty'
module Praat
class Interpreter
attr_reader :stdout, :stdin, :pid
PROMPT='Praat > '
def initialize(program='praat')
@stdout, @stdin, @pid = PTY.spawn( 'praat', '-' )
# Read initial prompt from pipe
read_until_prompt
if block_given?
yield self
@stdin.close
@stdout.close
@stdin = @stdout = @pid = nil
end
end
def read_until_prompt
outbuf = buf = ''
# Read from child STDOUT until > 0 bytes have been read
# (i.e. wait for child process to finish reading input)
while buf.length == 0
begin
IO.select([@stdout]) # block until child process is ready
@stdout.read_nonblock( 1024, buf )
rescue Exception => e
buf = '' # READ failure! Try again.
end
end
# Read from child STDOUT until 0 bytes are read or a line ending in a
# prompt (i.e. next input prompt) was encountered.
while buf.length > 0
outbuf << buf
# complete read if next input prompt is encountered
break if outbuf =~ /#{PROMPT}$/
begin
buf = ''
@stdout.read_nonblock( 1024, buf )
rescue Errno::EAGAIN => e
IO.select([@stdout]) # block until child process is ready
retry
rescue Exception => e
buf='' # READ failure. Exit loop.
end
end
# Return output of interpreter as an array of lines
return outbuf.split("\n").each { |x| x.chomp! }
end
# Send a command to the interpreter. Returns an array of the output.
# If include_prompts is true, lines beginning with a prompt will NOT be
# stripped from the output.
def send( command, include_prompts=false )
@stdin.write(command + "\n")
outbuf = read_until_prompt
# Ignore all ECHOed lines before the first (input) prompt
first_prompt = outbuf.find_index { |x| x =~ /^#{PROMPT}/ }
result = outbuf.slice(first_prompt, outbuf.length-first_prompt)
# Return full results, or the results with prompt lines removed
result = result.select {|x| x !~ /^#{PROMPT}/} if not include_prompts
yield result if block_given?
return result
end
end
end
if __FILE__ == $0
puts 'Testing block implementation'
Praat::Interpreter.new() { |p| puts p.send('echo BLOCK TEST') }
Praat::Interpreter.new() do |p|
p.send('echo Full Output', true).each { |line| puts "\t" + line }
end
puts 'Testing object implementation'
praat = Praat::Interpreter.new()
lines = []
lines.concat praat.send('echo OBJ TEST 1' )
lines.concat praat.send('echo OBJ TEST 2' )
lines.concat praat.send('echo OBJ TEST 3' )
puts lines.inspect
end
with open4 stdout.read will indeed block until stdin.close, but you could still have used stdout.readline to interact with process, explanation here: https://www.ruby-forum.com/topic/1452556#991753
ReplyDelete