1

I'm trying to pas an input stream through a pipe and want to catch a condition in which a downstream pipeline program might fail in which case I need to restart it. So I put it into a loop:

step1 |while true ; do step2 ; done

but if the pipeline upstream is closed, then I want that loop to exit. I can't tell this from the exit status of step2 else I might have said

step1 |until [ $? -ne 0 ] ; do step2 ; done

I need a test to check if stdin in closed but not actually consume a character from stdin.

I'm trying to think how I would do it in C. Does read(2) of a zero buffer allow me to test this? I don't think it does. How about select(2)? Nope. Or does it? How about fcntl(2)? I can't find anything.

Is there really no other way than to consume a byte and then somehow put it out again? No test for file descriptor is closed?

8
  • Maybe you should be testing EOF
    – fefe
    Commented Dec 28, 2020 at 1:56
  • @fefe how do I do that in a script? Commented Dec 28, 2020 at 2:43
  • I suppose there is an answer here superuser.com/questions/521920/…, it is amazingly complicated, requires reading one character into a temp file and then cat that file along with stdin to pipe to final program. Commented Dec 28, 2020 at 2:47
  • @GuntherSchadow are you looking at the OP's csh/bash version there or glenn jackman's simpler version?
    – muru
    Commented Dec 28, 2020 at 3:01
  • @muru I think the first version is more correct, as an empty line could trick the simpler version. Commented Dec 28, 2020 at 3:42

1 Answer 1

6

On Linux at least, you can tell whether the other end of a pipe has been closed by using poll() with POLLHUP in the event mask.

But note that at that point, there may still be data in the pipe ready to be read, so you'll likely want to check for that as well. On Linux again, that can be done with the FIONREAD ioctl.

So you could define a:

stdin_alive() {
  perl -MIO::Poll -e '
    require "sys/ioctl.ph";
    -p STDIN or die "stdin is not a pipe\n";
    $p = IO::Poll->new;
    $p->mask(STDIN, POLLHUP);
    if ($p->poll(0)) {
      ioctl(STDIN, &FIONREAD, $n) or die "FIONREAD: $!\n";
      $n = unpack "L", $n;
      exit 1 unless $n;
    }'
}

And use it as:

step1 | while stdin_alive; do step2; done

Another approach would be with the ifne command from moreutils:

step1 |
  while
    ifne sh -c 'step2 && exit 42'
    [ "$?" -eq 42 ]
  do
    continue
  done

ifne attempts to read its stdin. If at least one byte was read, then ifne starts the command with its stdin connected to a new pipe and shovels what it reads itself from its stdin through that new pipe to the command, so it's less efficient in that there's that extra shovelling and transiting via an extra pipe, but that means it can work regardless of the type of input (not only pipes).

Another difference from the previous solution is that ifne waits for input on stdin before starting step2, while the poll()+FIONREAD approach just checks if the pipe is live at the exact point in time of the check, whether the pipe has data or not. That could be changed though by adding POLLIN to the list of events being polled.

2
  • So this is really interesting. I started writing myself a little utility in C and it is quite confusing. The poll with event POLLRDNORM|POLLHUP returns both when there are bytes still to read while the pipe has already been closed. Also, it seems to continue to return POLLRDNORM even if all the remaining characters have been read. If I use cat /dev/null as input, the file seems to never be closed. It should work but it doesn't really. Very strange. Commented Dec 29, 2020 at 0:39
  • @GuntherSchadow, yes, upon eof, read() returns and doesn't hang, so poll() tells you so by returning POLLIN. You need that FIONREAD to disambiguate the two. See also mosvy's answer in the linked Q&A for a C variant using POLLIN+FIONREAD. Commented Dec 29, 2020 at 7:23

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .