Runy Open3.popen3 Putting input into a subprocess from the command line
Purpose: I am writing a workflow command line program in ruby ββthat executes other programs in a UNIX shell sequentially, some of which require user input.
Problem: While I can successfully process stdout
and stderr
thanks to this helpful blog post by Nick Charlton , I am nonetheless stuck in capturing user input and passing it to subprocesses via the command line. The code looks like this:
Method
module CMD
def run(cmd, &block)
Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
Thread.new do # STDOUT
until (line = stdout.gets).nil? do
yield nil, line, nil, thread if block_given?
end
end
Thread.new do # STDERR
until (line = stderr.gets).nil? do
yield nil, nil, line, thread if block_given?
end
end
Thread.new do # STDIN
# ????? How to handle
end
thread.join
end
end
end
Method call
This example invokes a shell command units
in which the user enters a unit of measure and then prompts for a device to convert. This is how it will look in the shell
> units 586 units, 56 prefixes # stdout You have: 1 litre # user input You want: gallons # user input * 0.26417205 # stdout / 3.7854118 # stdout
When I run this from my program, I expect to be able to interact with it the same way.
unix_cmd = 'units'
run unix_cmd do | stdin, stdout, stderr, thread|
puts "stdout #{stdout.strip}" if stdout
puts "stderr #{stderr.strip}" if stderr
# I'm unsure how I would allow the user to
# interact with STDIN here?
end
Note. Calling a method run
in this manner allows the user to parse the output, control the process flow, and add custom logging.
From what I put together in STDIN, below snippet, as I figured out how to handle STDIN, there are clear flaws in my knowledge because I am still not sure how to integrate this into mine run
above and pass the input to the child process.
# STDIN: Constant declared in ruby
# stdin: Parameter declared in Open3.popen3
Thread.new do
# Read each line from the console
STDIN.each_line do |line|
puts "STDIN: #{line}" # print captured input
stdin.write line # write input into stdin
stdin.sync # sync the input into the sub process
break if line == "\n"
end
end
Summary: I want to understand how to handle command line input using a method Open3.popen3
so that I can allow users to enter data in various sequences of subcommands called from my program.
source to share
After reading a lot about STDIN, as well as some good old trial and error, I found that the implementation is no different from Charles Finkel, but with some minor differences.
require "open3"
module Cmd
def run(cmd, &block)
Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
# We only need to check if the block is provided once
# rather than every cycle of the loop as we were doing
# in the original question.
if block_given?
Thread.new do
until (line = stdout.gets).nil? do
yield line, nil, thread
end
end
Thread.new do
until (line = stderr.gets).nil? do
yield nil, line, thread
end
end
end
# $stdin.gets reads from the console
#
# stdin.puts writes to child process
#
# while thread.alive? means that we keep on
# reading input until the child process ends
Thread.new do
stdin.puts $stdin.gets while thread.alive?
end
thread.join
end
end
end
include Cmd
Calling the method like this:
run './test_script.sh' do | stdout, stderr, thread|
puts "#{thread.pid} stdout: #{stdout}" if stdout
puts "#{thread.pid} stderr: #{stderr}" if stderr
end
Where test_script.sh
looks like this:
echo "Message to STDOUT"
>&2 echo "Message to STDERR"
echo "enter username: "
read username
echo "enter a greeting"
read greeting
echo "$greeting $username"
exit 0
Produces the following successful output:
25380 stdout: Message to STDOUT 25380 stdout: enter username: 25380 stderr: Message to STDERR > Wayne 25380 stdout: enter a greeting > Hello 25380 stdout: Hello Wayne
Note: You will notice that stdout and stderr are not displayed in order, this is a limitation that I have not yet resolved.
If you are interested in learning more about stdin, the following answer to the question is worth reading - What is the difference between STDIN and $ stdin in Ruby?
source to share
Something should work here:
module CMD
def run(cmd, &block)
Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
Thread.new do # STDOUT
until (line = stdout.gets).nil? do
yield nil, line, nil, thread if block_given?
end
end
Thread.new do # STDERR
until (line = stderr.gets).nil? do
yield nil, nil, line, thread if block_given?
end
end
t = Thread.new { loop { stdin.puts gets } }
thread.join
t.kill
end
end
end
I just added two lines to the original method run
: t = Thread.new { loop { stdin.puts gets } }
and t.kill
.
source to share