7

I have a file input:

$ cat input
1echo 12345

and I have the following program

1st version

#include <stdio.h>
#include <stdlib.h>

int main() {
  system("/bin/bash -i");
  return 0;
}

Now If I run it,

$ gcc -o program program.c
$ ./program < input
bash: line 1: 1echo: command not found
$ exit

Everything works as expected.

Now I want to ignore the 1st character of the file input, so I make a call to getchar() before invoking system().

2nd version:

#include <stdio.h>
#include <stdlib.h>

int main() {
  getchar();
  system("/bin/bash -i");
  return 0;
}

Surprisingly, the bash exits instantly like there is no input.

$ gcc -o program program.c
$ ./program < input
$ exit

Question why bash is not receiving the input ?

NOTE I tried some stuff and I figured out that forking a new child for the main process solves the problem:

3rd Version

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
  getchar();
  if (fork() > 0) {
    system("/bin/bash -i");
    wait(NULL);
  }
  return 0;
}

$ gcc -o program program.c
$ ./program < input
$ 12345
$ exit

OS Ubuntu 16.04 64bit, gcc 5.4

14

A file stream is defined to be:

fully buffered if and only if it can be determined not to refer to an interactive device

Since you're redirecting into standard input, stdin is non-interactive and so it's buffered.

getchar is a stream function, and will cause the buffer to be filled from the stream, consuming those bytes, and then return a single byte to you. system just runs fork-exec, so the subprocess inherits all your open file descriptors as-is. When bash tries to read from its standard input, it will find that it's already at the end of file because all the content has been read by your parent process already.


In your case, you want to consume only that single byte before handing over to the child process, so:

The setvbuf() function may be used after the stream pointed to by stream is associated with an open file but before any other operation [...] is performed on the stream.

Thus adding a suitable call before the getchar():

#include <stdio.h>
#include <stdlib.h>

int main() {
  setvbuf(stdin, NULL, _IONBF, 0 );
  getchar();
  system("/bin/bash -i");
  return 0;
}

will do what you want, by setting stdin to be unbuffered (_IONBF). getchar will cause only a single byte to be read, and the rest of the input will be available to the subprocess. It might be better to use read instead, avoiding the whole streams interface, in this case.


POSIX mandates certain behaviours when the handle can be accessed from both processes after a fork, but explicitly notes that

If the only action performed by one of the processes is one of the exec functions [...], the handle is never accessed in that process.

which means that system() doesn't (have to) do anything special with it, since it's just fork-exec.

This is probably what your fork workaround is hitting. If the handle is accessed on both sides, then for the first one:

If the stream is open with a mode that allows reading and the underlying open file description refers to a device that is capable of seeking, the application shall either perform an fflush(), or the stream shall be closed.

Calling fflush() on a read stream will mean that:

the file offset of the underlying open file description shall be set to the file position of the stream

so the descriptor position should be reset back to 1 byte, the same as the stream's, and the subsequent subprocess will get its standard input starting from that point.

Additionally, for the second (child's) handle:

If any previous active handle has been used by a function that explicitly changed the file offset, except as required above for the first handle, the application shall perform an lseek() or fseek() (as appropriate to the type of handle) to an appropriate location.

and I suppose "an appropriate location" might be the same (though it isn't further specified). The getchar() call "explicitly changed the file offset", so this case should apply. The intention of the passage is that working in either branch of the fork should have the same effect, so both fork() > 0 and fork() == 0 should work the same. Since nothing actually happens in this branch, though, it's arguable that none of these rules should be used at all for either parent or child.

The exact result is probably platform-dependent - at least, exactly what counts as "can ever be accessed" isn't directly specified, nor which handle is first and second. There is also an earlier, overriding case for the parent process:

If the only further action to be performed on any handle to this open file descriptor is to close it, no action need be taken.

which arguably applies for your program since it just terminates afterwards. If it does then all the remaining cases, including the fflush(), should be skipped, and the behaviour you're seeing would be a deviation from the specification. It's arguable that calling fork() constitutes performing an action on the handle, but not explicit or obvious, so I wouldn't trust that. There's also enough "either" and "or" in the requirements that a lot of variation seems allowable.

For multiple reasons I think the behaviour you're seeing may be a bug, or at least a generous interpretation of the specification. My overall reading is that, since in every case one branch of the fork doesn't do anything, none of these rules should have been applied and the descriptor position should have been ignored where it was. I can't be definitive about that, but it seems like the most straightforward reading.


I wouldn't rely on the fork technique working. Your third version doesn't work for me here. Use setbuf/setvbuf instead. If possible, I'd even use popen or similar to set up the process with the necessary filtering explicitly, rather than relying on the vagaries of stream and file descriptor interactions.

  • Please see my answer and give any response you care to give. – G-Man Dec 22 '17 at 4:31
2

I applaud Michael Homer for his answer, and for finding that POSIX reference.  But I believe that I understand at least 70% of this stuff, and I don’t fully understand his answer — so I prepared this TL;DR version of what I believe he is saying.

  • getchar (a.k.a. getc), when reading from a file, will actually read a block from the file (or the entire file, whichever is less).  It reads data into a buffer.  It will then give you the first character from the buffer.

    • Subsequent calls will return subsequent characters from the buffer, until the buffer is exhausted.  It will then (attempt to) read another block from the file.  This “buffered I/O” makes it much more efficient for a program to read a file and deal with it a little bit at a time.


    Even though the buffered file stream has a logical I/O pointer that is one character into the file, this causes the file description’s I/O pointer to be moved one block into the file (or, in the case of your small file, to the end of the file).  When system forks, the child process inherits the file description, but not the file stream, so bash inherits a file I/O pointer that is at the end of the file.  Therefore, when it reads from the file, it gets only an EOF.

  • In the third version of your program, main is calling fork directly (rather than just calling system, which calls fork).  And, of course, as in the second version of your program, main calls getcharThe POSIX document that Michael found explicitly says that it is describing an extension to the ISO C standard.  As far as I can tell from its cryptic language, it is saying that main (or, I suppose, the C compiler) is responsible for noticing the above problem when fork is called directly by the routine that calls the stdio function(s), and for resolving it.  Therefore, apparently, fork (or some other mechanism?) interacts with the stdio family to get the file description’s physical I/O pointer moved back to synchronize with the file pointer’s logical I/O pointer; i.e., one character into the file.  The second fork — the hidden one, called by system — serendipitously benefits from this, as it results in the shell being able to read the file starting at the second byte.

I made a cursory search of relevant man pages, and I couldn’t find this behavior described.  And I couldn’t reproduce your result, either.  So this may be the POSIX-specified behavior, but it clearly isn’t universally implemented yet.

  • I don't think it's required that the same routine use both the stdio and fork functions, just that use of stdio both precedes and follows the fork. It isn't necessarily the case that buffered reading takes a whole block, though that's a plausible buffer size. – Michael Homer Dec 22 '17 at 5:04
  • I've edited my answer to show the situation for the parent process too, which really should be more germane. My reading of the specification is that, since nothing actually accesses the stream again in the parent, nothing should actually be done to it and so the querent's observed behaviour might be a bug. It's arguable that calling fork again is using the handle, perhaps. – Michael Homer Dec 22 '17 at 5:05
  • In fact, thinking further, it looks to me like - since in both forks nothing ever happens to the handle in one branch - none of those rules should be applied and the position should have been left as it was. – Michael Homer Dec 22 '17 at 5:14
1

Nasal demons

man 3 getchar says:

The getchar() function shall be equivalent to getc(stdin).

And man 3 getc says:

It is not advisable to mix calls to  input  functions  from  the  stdio
library  with  low-level  calls  to  read(2)  for  the  file descriptor
associated with the input stream; the results  will  be  undefined  and
very probably not what you want.

Bash in interactive mode is likely using read, etc. (via readline) to directly access the input. So we have nasal demons.

  • 2
    This isn't quite correct - even if bash used the stdio library, it would be accessing different buffers than the parent process, and so the result is still undefined. – nneonneo Dec 16 '17 at 3:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.