3

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.

Jmate
  • 73
  • 6
  • Couldn't you just `print` the stdout output to the user in your script? – Kimmo Lehto Sep 26 '18 at 07:58
  • (after capturing + analyzing) – Kimmo Lehto Sep 26 '18 at 08:12
  • Thanks for responding! This would work if I could read the command's `stdout` char-by-char to print it in the terminal (like a manual `tee`) but I think I can't do that since the output will be buffered. This is particularly problematic when there's a line without a `\n` like a terminal prompt e.g., `jmate@ssh_server~$ `. – Jmate Sep 26 '18 at 08:16
  • I think with popen3 you get an IO object that you can read unbuffered or with a timeout. Also you can then do something like `stdin.copy_stream($stdin)`. – Kimmo Lehto Sep 26 '18 at 12:57

1 Answers1

1

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).

Jmate
  • 73
  • 6
  • Hi, would you mind posting your final solution? Thanks! – fiedl Jan 04 '22 at 12:56
  • Wow it's great to see people are still seeing this. Unfortunately, the code for this is owned by Bezos now and I haven't written ruby in years so I'd have a hard time reproducing it. I will say, the approach I outlined in this post worked perfectly for my purposes. – Jmate Jul 29 '23 at 03:26