Jmate
Jmate

Reputation: 83

Capture output of interactive external command run from inside Ruby

I have been searching for a way to run an interactive command (such as ssh) and capture its output. This sounds simple but as I understand it, my use case is slightly different from a lot of the google and s/o results.

To be clear, by interactive I mean, the user who runs my_ruby_script.rb from the terminal should be able to

  1. type into (stdin) the running interactive system command (e.g., type ls through ssh tunnel)
  2. read out from (stdout/stderr) the running interactive system command (again e.g., seeing the output of ls in an ssh tunnel)

On top of that, I would like to read the output of the system command (ssh) so I may, for example, check for errors from inside my Ruby script. The closest that I have gotten has been to simply tee the output to another file.

system('ssh username@ssh_server |& tee ssh_log.txt')

This is not ideal as

  1. It requires saving to/reading from a separate file
  2. It requires a more complicated system call which may or may not be portable

As I understand it, most of the solutions online (backticks, Open3.*, PTY.spawn(), etc. ) redirect stdin/stdout away from the terminal interface and into the ruby program. What I'm looking for is to leave stdin alone, and tee stdout to my Ruby script. Does anybody have any ideas?

I should note that system('ssh username@ssh_server') does exactly what I want, I just need to be able to read the output. Additionally, I would prefer not to use net-ssh nor any other non-stdlib libraries.

Research:

  1. This is probably the most comprehensive page describing running commands in Ruby (When to use each method of launching a subprocess in Ruby and the linked blog posts).
  2. These people have (what seems to me) my exact same issue:
  3. These people are using ssh through Ruby but their scripts aren't "interactive"

I really appreciate any insight you can deliver.

Upvotes: 4

Views: 600

Answers (1)

Jmate
Jmate

Reputation: 83

I found my answer! Big thanks to @Kimmo Lehto for suggesting the IO.copy_stream() idea which led to my final solution. Basically, we use PTY.spawn(cmd){|stdin, stdout, pid| ...} to get unbuffered streams for stdin and stdout.

Then we copy the script's stdin to the command's stdin with IO.copy_stream($stdin.raw!, $stdin) (don't forget to set $stdin.cooked! afterwards).

I couldn't figure out how to use copy_stream on the command's stdout twice (first to the script's stdout, second to a buffer e.g., io = StringIO.new) so I had to manually read each character like so: stdout.readchar(|c| print c; io.write c).

Upvotes: 1

Related Questions