Why Redirect Stdin Inside a While Read Loop in Bash

Why redirect stdin inside a while read loop in bash?

The clear intent here is to prevent do_something from reading from the sample.text stream, by ensuring that its stdin is coming from elsewhere. If you're not seeing differences in behavior with or without the redirection, that's because do_something isn't actually reading from stdin in your tests.

If you had both read and do_something reading from the same stream, then any content consumed by do_something wouldn't be available to a subsequent instance of read -- and, of course, you'd have illegitimate contents fed on input to do_something, resulting in consequences such as a bad encryption key being attempted (if the real-world use case were something like cryptmount), &c.

cat sample.text | while read arg1 arg2 arg3 arg4 arg5; do
ret=0
do_something "$arg1" "$sarg2" "$arg3" "$arg4" "$arg5" <&3 || ret=$?
done 3<&1

Now, it's buggy -- 3<&1 is bad practice compared to 3<&0, inasmuch as it assumes without foundation that stdout is something that can also be used as input -- but it does succeed in that goal.


By the way, I would write this more as follows:

exec 3</dev/tty || exec 3<&0     ## make FD 3 point to the TTY or stdin (as fallback)

while read -a args; do ## |- loop over lines read from FD 0
do_something "${args[@]}" <&3 ## |- run do_something with its stdin copied from FD 3
done <sample.text ## \-> ...while the loop is run with sample.txt on FD 0

exec 3<&- ## close FD 3 when done.

It's a little more verbose, needing to explicitly close FD 3, but it means that our code is no longer broken if we're run with stdout attached to the write-only side of a FIFO (or any other write-only interface) rather than directly to a TTY.


As for the bug that this practice prevents, it's a very common one. See for example the following StackOverflow questions regarding it:

  • Shell script while read line loop stops after the first line
  • ssh breaks out of while-loop in bash
  • Bash while loop stops for no reason?

etc.

bash while read loop and stdin - force nested command's stdin to tty?

I'm replacing cat with something a little less problematic in the examples below:

read_a_line() { local line; read -r line; echo "Read line: $line"; }

That way it only reads one line of input per loop invocation, rather than reading all the way to EOF. Otherwise, though, I'm trying to keep changes minimal to focus on the immediate problem.

See BashFAQ #24 for a discussion of why it's preferable to redirect from a process substitution into your loop rather than to pipe to a loop.


First, you can simply redirect from /dev/tty

find . | while read file; 
do
echo "==[$file]=="
read_a_line </dev/tty
done

Second, you can copy stdin to a different file descriptor, and reuse it later:

exec 3<&0  # make FD 3 a copy of FD 0
find . | while read file; do
echo "==[$file]=="
read_a_line <&3
done
exec 3<&- # close FD 3 now that we're done with it

Third, you can try to do both -- attempting to make FD 3 (or any other FD of your choice above 2) be open to /dev/tty, but making it a backup of your original stdin if that fails.

exec 3</dev/tty || exec 3<&0
find . | while read file; do
echo "==[$file]=="
read_a_line <&3
done
exec 3<&-

I/O redirection in a while loop

You can write it that way, but it always reads the first line.

Think of the while loop like this:

  1. In the first case:

    1. Connect the file to stdin
    2. Read until a newline
    3. Loop (read the next line)
    4. Disconnect the file
  2. In the second case:

    1. Connect the file to stdin
    2. Read until a newline
    3. Disconnect the file
    4. Loop (reconnect the file, read a line, disconnect the file)

How to read input inside a while loop while reading from a file?

Use a different file descriptor for the named file. You know where that data is coming from; you don't know where standard input might be redirected from, so leave it alone.

while IFS= read -r -u 3 repo   # Read from file descriptor 3
do
read -p "Do you want to delete $repo" ip # Read from whatever standard input happens to be
echo "$ip"
if [ "$ip" = "y" ]
then
#do something
fi
done 3< "$filename" # Supply $filename on file descriptor 3

-u is bash-specific, but I note you are already using another bash-specific feature, the -p option to read. The POSIX way to read from something other than standard input is IFS= read -r repo <&3 (which says, copy file descriptor 3 onto standard input for this command).

Read user input inside a loop

Read from the controlling terminal device:

read input </dev/tty

more info: http://compgroups.net/comp.unix.shell/Fixing-stdin-inside-a-redirected-loop



Related Topics



Leave a reply



Submit