From: Ersek, Laszlo on
In article <%DBin.45925$Ym4.25977(a)text.news.virginmedia.com>, Poster Matt <postermatt(a)no_spam_for_me.org> writes:

> My code allows a user to redirect a file to stdin so that some text data can be
> read, no problem.
>
> Later a user confirmation to proceed is requested, using getchar(). This is not
> working because of what's left in stdin.

After reading the replies superficially, I'd say this:
1. Read the data to process from stdin until you see EOF
2. Open /dev/tty for interactive confirmation. You can try to pre-open
/dev/tty before step 1 to see if there is a controlling terminal at
all, and act accordingly.

If one simply runs your program on a terminal, without any redirection,
ie. all of stdin, stdout, stderr connected to the controlling terminal,
opening /dev/tty will simply return another reference to that terminal.

stdio stream
-> file descriptor
-> file description
-> file (inode)

Since the "file (inode)" is a character device, there are two further
levels of indirection, determined by the major/minor device numbers,

-> statbuf.st_rdev
-> kernel object

Under these circumstances, stdin, stdout and stderr share the terminal
device on the file (inode) level. (Access mode, ie. O_RDONLY / O_WRONLY
is a file description level attribute, and that's unlikely to be shared
by STDIN_FILENO and STDOUT_FILENO, for example.) When you open /dev/tty
manually (which has a different inode and a different st_rdev too, on
Linux at least), a reference chain terminating in the same terminal
device kernel object will be created.

$ tty
/dev/pts/43

$ ls -lgoi /dev/pts/43
45 crw--w---- 1 136, 43 2010-03-02 15:07:11 +0100 /dev/pts/43

$ exec 4</dev/tty

$ ls -lLgoi /proc/$$/fd/[0124]

45 crw--w---- 1 136, 43 2010-03-02 15:07:28 +0100 /proc/17734/fd/0
45 crw--w---- 1 136, 43 2010-03-02 15:07:28 +0100 /proc/17734/fd/1
45 crw--w---- 1 136, 43 2010-03-02 15:07:28 +0100 /proc/17734/fd/2
496 crw-rw-rw- 1 5, 0 2010-03-02 12:46:57 +0100 /proc/17734/fd/4

That is,

stdio d.-or d.-ion inode st_rdev kernel obj
----- ----- ------ ----- ------- ----------
stdin -> fd:0 -> desc:0 -> st_ino:45 -> st_rdev:136,43 -> terminal
stdout -> fd:1 -> desc:1 -> st_ino:45 -> st_rdev:136,43 -> terminal
stderr -> fd:2 -> desc:2 -> st_ino:45 -> st_rdev:136,43 -> terminal
N/A -> fd:4 -> desc:3 -> st_ino:496 -> st_rdev:5,0 -> terminal

(See <http://www.opengroup.org/onlinepubs/007908775/xbd/termios.html>
and <http://www.opengroup.org/onlinepubs/007908775/xsh/stdio.html> for
the following.)

When you start your program like described above, the terminal should be
reset to Canonical Mode Input Processing by the shell. In this
(line-oriented) mode, the terminal will buffer up characters until you
flush them via an EOL character, an NL character, or an EOF character.
NL is newline (you press enter). EOL is usually unconfigured (see "stty
-a | grep eol"), and EOF is usually ^D.

So your program starts. stdin is preopened. Since isatty() returns true
for it, it is set to a non-fully-buffered mode (ie. line-buffered or
nonbuffered). This is a user-space (libc) buffering. Then fgets() calls
read(), read() blocks. You enter ten characters in Canonical Mode. The
terminal driver buffers them up (in kernel space) and read() keeps
blocking.

Now you press Enter. The terminal driver unblocks read(), read returns
stuff to fgets(). Supposing the buffer passed to fgets() was big enough,
fgets() returns to your program.

You type ten more characters, then press ^D *once* without typing NL.
The terminal driver unblocks read(), read() returns ten characters to
fgets(). fgets() sees no newline, it keeps calling read(). You type five
further characters, then press Enter. fgets() completes the line and
returns it to your program.

Now you type five characters, and press ^D *once*. fgets() does not
return yet. You press ^D *again*, without typing any more characters
between this and the previous ^D. The terminal driver unblocks read()
and transfers 0 (zero) characters. read() returns 0 to fgets(), which is
EOF. fgets() sets the end-of-file indicator for stdin, and returns the
line without a terminating newline. You call fgets() again, it sees the
EOF indicator for the stream, and returns immediately with NULL.

(A variation for the last paragraph is this. After entering the previous
line with an NL at its end, you simply press ^D once, instead of typing
five characters first. The terminal driver unblocks read(). read()
transfers zero bytes and returns 0 to fgets(). fgets() sees the EOF,
sets the end-of-file indicator for the stream, and returns NULL.)

Okay, so you have a NULL from fgets() (or, you have a 0 from read(), if
you work on the file descriptor level). Now you turn to the file
descriptor you have pre-opened for /dev/tty, and either read() from it
directly, or fdopen() a standard IO stream on top of it. You keep typing
on the terminal in the same Canonical Mode Input Processing.

(I'm not sure if fdopen() checks isatty() internally in order to set up
the _IOLBF or _IONBF stdio buffering for this new stream -- I recommend
you do it yourself with setvbuf() right after fdopen(). If you want
fgetc() to return a character as soon as it is typed, I'd recommend
setting _IONBF with setvbuf() on the stream created by fdopen(), *and*
kicking the terminal out of Canonical Mode Input Processing by way of
tcsetattr(). Even an unbuffered stdio stream cannot help you if the
terminal driver doesn't unblock read() until you press Enterr or ^D.)

Thus what does the user see? He/she types some data to process, then
finishes the input with a newline + ^D combo (for a newline-terminated
input), or a ^D + ^D combo (for an input whose last line is not \n
terminated). Then you print a message to /dev/tty (or stderr), and
he/she can simply keep on typing, and terminate the interactive
confirmation with newline + ^D or ^D + ^D.

If the user redirects stdin from a pipe or regular file, your first loop
will simply read until EOF, and then the interactive confirmation will
take place on /dev/tty.

A good example for this is gnupg (with the order of the interactive
phase and the data phase reversed). Try

$ gpg -o /dev/null -c
[type password + NL]
[repeat passsword + NL]
[type type type]
[type type type]
^D

and

$ gpg -o /dev/null -c </some/file
[type password + NL]
[repeat password + NL]

For the interactive (password entry) phase, gnupg turns off at least
the ECHO local mode flag. I reckon curses programs usually turn off
at least ECHO and ICANON.


More on /dev/tty:
<http://www.opengroup.org/onlinepubs/007908775/xbd/files.html>


As a final tip, if you are sometimes bored that you can't use "rm -i"
with xargs: you can.

$ generate_list_of_files \
| xargs sh -c 'rm -i "$@" </dev/tty' dummy_script_name

(1) With GNU xargs, that would be "xargs -r -0 ...".
(2) An alternative is "xargs -p -n 1 rm", but that calls rm for each
single file.


Sorry for any inaccuracies, I'm sure I've committed many. (I'm too lazy
to re-read this post now, sorry.)

Cheers,
lacos