Reputation: 3130
I have a Bash script foo
that accepts a list of words on STDIN. The script should read the words into an array, then ask for user input:
while IFS= read -r; do
words+=( "$REPLY" )
done
read -r ans -p "Do stuff? [Yn] "
echo "ans: |$ans|"
The problem is, Bash immediate reads an empty string into the ans
variable, without waiting for actual user input. That is, I get the following output (with no prompt or delay):
$ cat words.txt | foo
ans: ||
Since everything piped into STDIN has already been consumed by the first read
call, why does the second read
call return without actually reading anything?
Upvotes: 4
Views: 4489
Reputation: 437688
Judging by your symptoms, it looks like you've redirected stdin to provide the list of words to the while
loop either via an input file (foo < file
) or via a pipeline (... | foo
).
If so, your second read
command won't automatically switch back to reading from the terminal; it is still reading from whatever stdin was redirected to, and if that input has been consumed (which is exactly what your while
loop does, as chepner points out in a comment), read
reads nothing, and returns with exit code 1
(which is what terminated the while
loop to begin with).
If you explicitly want the second read
command to get user input from the terminal, use:
read -r -p "Do stuff? [Yn] " ans </dev/tty
Note:
Stdin redirected from a (finite) file (or pipeline or process substitution with finite output) is a finite resource that eventually reports an EOF condition once all input has been consumed:
read
translates the EOF condition into exit code 1
, causing the while
loop to exit:
read
cannot read any more characters, it assigns the null string (empty string) to the specified variable(s) (or $REPLY
if none were specified), and sets the exit code to 1
.read
may set exit code 1
even when it does read characters (and stores them in the specified variable(s) / $REPLY
), namely if the input ends without a delimiter; the delimiter is \n
by default, otherwise the delimiter explicitly specified with -d
.Once all input has been consumed, subsequent read
commands cannot read anything anymore (the EOF condition persists, and the behavior is as described above).
By contrast, interactive stdin input from a terminal is potentially infinite: additional data is provided by whatever the user types interactively whenever stdin input is requested.
The way to simulate an EOF condition during interactive multiline input (i.e, to terminate an input loop) is to press ^D (Control-D):
When ^D is pressed once at the very start of a line, read
returns without reading anything and sets the exit code to 1
, just as if EOF had been encountered.
By contrast, in the interior of an input line, pressing ^D twice is needed to stop reading and set the exit code to 1
, but note that the line typed so far is saved to the target variable(s) / $REPLY
.[1]
Since the stdin input stream wasn't actually closed, subsequent read
commands work normally and continue to solicit interactive user input.
Caveat: If you press ^D at the shell's prompt (as opposed to while a running program is requesting input), you'll terminate the shell itself.
P.S.:
There is one incidental error in the question:
read
command must place operand ans
(the name of the variable to store the input in) after all options in order to work syntactically: read -r -p "Do stuff? [Yn] " ans
[1] As William Pursell points out in a comment on the question: ^D causes the read(2)
system call to return with whatever is in the buffer at that point; the direct value returned is the count of characters read.
A count of 0
is how the EOF condition is signaled, and Bash's read
translates that into exit code 1
, causing termination of the loop.
Thus, pressing ^D at the start of a line, when the input buffer is empty, exits the loop immediately.
By contrast, if characters have already been typed on the line, then the first ^D causes read(2)
to return however many characters were typed so far, upon which Bash's read
reinvokes read(2)
, because the delimiter (a newline by default) hasn't been encountered yet.
An immediately following second ^D then causes read(2)
to return 0
, since no characters were typed, causing Bash's read
to set exit code 1
and exit the loop.
Upvotes: 11