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.

+3


source to share


2 answers


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?

+1


source


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

.

0


source







All Articles