2

I've finally run into the surprising stdin behavior when piping into a while read loop.

Consider the following:

find . | while read file; 
do
  echo "==[$file]==";
  cat;
done

In this instance, catis just a stand-in for any command that receives input from STDIN. It's surprising (to me at least) that cat's STDIN is actually coming from find, so it gobbles up the rest of the find output.

Suppose one wanted to interact directly from the tty with the command in cat's place. E.g. Suppose instead of cat you wanted to run a script which might ask questions you wanted to respond to interactively ("<file> exists: Overwrite? [y/n]").

Is there a way to force the inner command's STDIN to be the tty?

I've found a lot of similar questions, including this: Why redirect stdin inside a while read loop in bash?

But I couldn't understand the answer well enough to get it to work.

(edit: in light of clarifications to that other question, I'm now considering this a duplicate of that question.)

5
  • What part of the other answer, specifically, was hard to follow? (I actually just fixed up the code to add some comments less than an hour ago; was hoping that would be sufficient on its own). Commented Mar 15, 2017 at 20:17
  • ...seriously, though -- followup describing how exactly the other answer didn't address your question would be greatly appreciated. (One possibility is that we'll get a clearly distinct new question created here -- but the other one, of improving the original sufficient to make it more useful, also adds value to the site). Commented Mar 15, 2017 at 20:25
  • 1
    It's simply that your bash is so much stronger than mine. I read your answer before you added the code comments. It helped me understand that cat's stdin was gobbling everything up from the find, which I hadn't understood. I voted it up, which probably triggered you adding the comments. I'd done 2>&1 before and roughly understood it but this stuff is all foreign to me: exec 3</dev/tty || exec 3<&0; exec 3<&-. I tried adding that stuff to mine but it didn't work for me (because I left out the <&3). The comments help me understand it better. Commented Mar 15, 2017 at 20:41
  • I'd be content marking this question as a duplicate of the other one, but I'm not sure how. My S.O. is apparently as weak as my bash. :) Commented Mar 15, 2017 at 20:55
  • 1
    It is a useful duplicate, though -- looking at both, I think you've asked a question with fewer complicating factors, and thus allowed a similarly improved answer. Thank you for that. :) Commented Mar 15, 2017 at 20:56

2 Answers 2

5

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<&-
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the very thorough answer. I've now gotten solutions #1 and #2 working in my situation. As I commented on the original question, I didn't understand in the solution in the other thread until you added the code comments; it's clearer now. Thanks again.
1

This example could help:

{
while IFS= read -r -d '' file
do
    read -u3 -p "what to do with: [$file]?> " action
    printf "got [$action] for the [$file]\n\n"
done < <(find . -print0)
} 3<&0
  • for the whole script's the stdin is reditected to fd3
  • and internal while is redirected from the find
  • the read reads from the fd3 - e.g. from the terminal

5 Comments

Is there a reason that it's ( )s on the outside, rather than { }s (avoiding the performance overhead of a subshell, while still allowing scoped redirections?)
...actually, what's being added by that extra syntax that just a 3<&0 directly after the done wouldn't accomplish?
@CharlesDuffy the {} is better :) but, like 2ms overhead....
Sure, I agree, on any modern machine it's completely trivial when not in a tight loop (and you wouldn't want to spawn find in a tight loop anyhow).
@CharlesDuffy for me the extra {} mean - it is cleaner... - sure could be done shorter and nicer..

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.