0

So, I have written a little application called cowspeak

It does various jobs on a GNU/Linux system. But the problem is while accepting inputs using pipes.

I have already implemented accepting IO redirection with this code:

#!/usr/bin/ruby -w
require 'timeout'

STDIN.sync = STDOUT.sync = true
pipe = false

begin
    Timeout.timeout(0.000_000_000_001) do pipe = STDIN.gets end
rescue Timeout::Error
end

if pipe
    print pipe
    print pipe while pipe = STDIN.gets
end

It's all fine.

So for echo -e "hello\nworld" | ruby cowspeak, I get

hello
world

But the problem is it doesn't work with programs like irb (yes, I don't need to use IRB with this program, but I want to learn how it should work). A working example is the lolcat gem.

So for irb | ruby cowspeak, I get it running forever in the while loop (implemented in loop loop in cowspeak).

Lolcat works fine in that case as well!

Also, apps like cmatrix doesn't work with my program, but they work with lolcat.

Unix programs like cowsay (which is written in Perl) doesn't also work with pipes well - the same behaviour as cowspeak...

Anyways, I see the question is asked before here: ruby pipes, IO and stderr redirect

But it doesn't answer my question properly.

How can I implement IO redirection with Ruby and overall how the lolcat gem really work with pipes?

Community
  • 1
  • 1
15 Volts
  • 1,946
  • 15
  • 37
  • 1
    Is the problem that you don't know how to reliably detect whether stdin is a pipe? Check out [this answer](https://stackoverflow.com/questions/43789845/how-to-detect-if-a-ruby-script-is-running-through-a-shell-pipe). Don't use a timeout, it's a race condition. – that other guy May 28 '19 at 19:59
  • Why, don't you just read [lolcat's source code](https://github.com/busyloop/lolcat), and learn from it? It's like 300 lines of code total. – TeWu May 28 '19 at 20:00
  • @thatotherguy, nope I am aware of the isatty? method. My problem is my solution (the piece of code I wrote above) doesn't work with programs like cmatrix, and even with the irb shell. – 15 Volts May 28 '19 at 20:03
  • @TeWu, I read the lolcat's source code after I needed the feature. lolcat doesn't use a single file. It's all very messy. Lolcat uses the gem paint and paint reads a gz file for the colours. I don't want that. The application I wrote shouldn't contain any more files, and it should have only Ruby as dependency, it should load as fast as possible. – 15 Volts May 28 '19 at 20:07
  • @S.Goswami I don't understand. You know that your attempt is wrong, and you also say that you're aware of the correct way of doing it. What exactly are you asking then? Are you asking why `irb` reliably loses your race condition while `echo` reliably wins it? – that other guy May 28 '19 at 20:15
  • Actually it's `echo` outputs the output correctly. But, if you run programs like `cmatrix | lolcat` it works, `irb | lolcat` works (and lolcat doesn't complain about TTY or such stuff). However, I have already implemented `STDOUT.tty?` method in the first commit which exits if there's no tty detected - say if you run the program in editors like atom or vscode. Anyways, I am interested in knowing how can I implement my program in such a way that it can work with other programs? – 15 Volts May 28 '19 at 20:35
  • Also, look at `irb | head -n5`, `cmatrix | head -n5` they all work. I think my implementation isn't correct at all! Regardless the Timeout#timeout, is there any gem from the standard library that I can use? Actually I am wondering what could be proper implementation for about a year!! – 15 Volts May 28 '19 at 20:42

1 Answers1

1

To correctly read all data from a pipe, read block by block without timeouts, and to make sure you're able to process any kind of data as it comes in:

#!/usr/bin/ruby -w

if STDIN.isatty
  STDERR.puts "You are not piping, but I will read from stdin anyways"
  STDERR.puts "because that is the canonical Unix behavior."
end

until STDIN.eof?
  STDOUT.write(STDIN.readpartial(4096))
end

This fixes the two issues with your posted code (ignoring all the other programs and combinations you're asking about):

  1. You are using a timeout, so the program fails if input is slow
  2. You are reading line by line, so the program fails if input isn't line based

echo appears to work because this is a shell builtin and is therefore likely to run before ruby finishes loading, but it's still down to chance and scheduling. Here's a race rigged so that echo loses:

$ ( sleep 1; echo Hello ) | ./yourprogram
./yourprogram:9: warning: constant ::TimeoutError is deprecated

You're also waiting for complete lines, but irb only outputs a prompt without linefeeds and cmatrix doesn't output linefeeds at all. This causes a timeout due to the above bug, and would otherwise just read memory until it dies:

$ while echo -n foo; do true; done | ./yourprogram
./yourprogram:9: warning: constant ::TimeoutError is deprecated

The suggestion above has neither of these issues.

that other guy
  • 116,971
  • 11
  • 170
  • 194
  • For some strange reason I wrote TimeoutError rather than Timeout::Error!! Also, we can't use STDIN.getch because it raises `Inappropriate ioctl for device (Errno::ENOTTY)` error. Apart from using Timeout, your answer is correct, and very helpful. Thank you very much for the answer! – 15 Volts May 29 '19 at 02:51
  • Sorry, this might not a useful comment but I would like to say that cowspeak has to check if IO redirection is happening, if not, it will show a random quote. So, I need to use the Timeout gem from standard library to make sure it first checks for a line with `STDIN.gets` and if there's no line, it will not loop in the `... until STDIN.eof?`. – 15 Volts May 29 '19 at 03:18
  • 1
    You do not need a timeout to check if redirection if happening. That is what `isatty` does robustly and reliably. Using a timeout is really bad. – that other guy May 29 '19 at 06:27