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.)

Community
  • 1
  • 1
adfaklsdjf
  • 250
  • 1
  • 11
  • 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). – Charles Duffy Mar 15 '17 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). – Charles Duffy Mar 15 '17 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 – adfaklsdjf Mar 15 '17 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. :) – adfaklsdjf Mar 15 '17 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. :) – Charles Duffy Mar 15 '17 at 20:56

2 Answers2

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<&-
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • 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. – adfaklsdjf Mar 15 '17 at 20:44
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
clt60
  • 62,119
  • 17
  • 107
  • 194
  • 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?) – Charles Duffy Mar 15 '17 at 20:27
  • ...actually, what's being added by that extra syntax that just a `3<&0` directly after the `done` wouldn't accomplish? – Charles Duffy Mar 15 '17 at 20:28
  • @CharlesDuffy the `{}` is better :) but, like 2ms overhead.... – clt60 Mar 15 '17 at 20:29
  • 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). – Charles Duffy Mar 15 '17 at 20:30
  • @CharlesDuffy for me the extra `{}` mean - it is cleaner... - sure could be done shorter and nicer.. – clt60 Mar 15 '17 at 20:30