In bash, `while read -r line; do ((count++)); done < <(find . -name '*.log')` leaves `count` holding the file count, unlike the same loop fed by a pipe. What is `< <(...)` doing to the loop's input, and why is the space between the two `<` characters mandatory?
answer
- two operators, not one
- the loop never becomes a pipeline stage
- a filename feeding standard input
- << would start a here-document
basics
~20 sThe inner <(find ...) expands to a /dev/fd path, and the separate outer < redirects the loop's standard input from it, so the loop itself runs in the current shell and its variable updates survive. Without the space, << starts a here-document.
solid answer
~50 sRead it right to left. `<(find ...)` is process substitution: it starts `find` as its own process and expands to a filename such as `/dev/fd/63`. The `<` in front of it is ordinary input redirection applied to the whole `while` compound command, so the loop reads its lines from that path. The point is what is *absent* — there is no pipeline, and bash only puts a command in a subshell when it is a pipeline stage, so the `while` body executes in the current shell and `count` is still set afterwards. The space is not style: `<<` is the here-document operator, so `done <<(find ...)` is tokenised as a here-doc and fails to parse. Keep `read -r` so backslashes in filenames survive, and remember that `find`'s own exit status is invisible to the loop.
code
bash · 11 linescount=0
while read -r line; do
count=$((count + 1))
done < <(printf 'one\ntwo\nthree\n')
echo "loop saw $count lines"
count=0
printf 'one\ntwo\nthree\n' | while read -r line; do
count=$((count + 1))
done
echo "pipeline left count at $count"go deeper
Learn the shape and use it: put the producer in < <( ) after done instead of piping into the loop, always with a space between the two < and always read -r.
Explain the two constructs separately — expansion to a /dev/fd filename, then ordinary input redirection of the whole compound command — and state that << is why the space is mandatory.
Show the failure modes you have actually hit: an ssh or ffmpeg in the body eating the loop's stdin, and a producer that dies mid-stream without anyone noticing. Offer 3< <( ) with read -u 3 as the cure for the first.
Frame the limit for the team: any loop that needs the producer's exit status, NUL-safe filenames, or /bin/sh portability has outgrown this idiom, and a script full of workarounds around it is a signal to move the job out of bash.
## Reading the line right to left `done < <(find . -name '*.log')` is two independent constructs that happen to sit next to each other. The inner one, `<(find . -name '*.log')`, is process substitution. Bash creates a pipe, forks a process that runs `find` with its stdout on the write end, and replaces the word with a filename naming the read end — typically `/dev/fd/63`. The outer one, the lone `<`, is plain input redirection, and it is attached to the entire `while ... done` compound command. So the loop's standard input is that path. `read` therefore consumes `find`'s output line by line, exactly as it would from a pipe — but through a filename rather than through a pipeline. That distinction is the point. A pipeline puts each of its stages in its own process; a redirected compound command does not. With `< <( )` the `while` body runs in the shell you are already in, so `count`, arrays, and any other state the body touches are still there when the loop ends: ```bash count=0 while read -r f; do count=$((count + 1)) done < <(find . -name '*.log') echo "$count" # the real number ``` ## Why the space is mandatory Bash tokenises redirection operators greedily. Written as `done <<(find ...)`, the lexer sees `<<` — the here-document operator — and then tries to read a delimiter word beginning with `(`. The result is a syntax error, not a subtly different behaviour, which at least makes this one loud. The space forces two separate tokens: the redirection `<`, and the expansion `<(`. The same rule applies at the front of a command, as in `wc -l < <(seq 5)`. ## What this still does not fix **The producer's exit status is gone.** After the loop, `$?` is the status of the last command in the body — nothing anywhere reports that `find` exited non-zero, or that it printed a permission error to stderr and stopped early. If failure matters, either check for a sentinel your producer writes as its last line, or use a construct whose status you can inspect, such as a real pipeline under `set -o pipefail`. **The loop body shares that stdin.** Any command in the body that reads standard input drains the same descriptor. The classic symptom is a loop over hosts that processes exactly one of them: ```bash while read -r host; do ssh "$host" uptime # eats the rest of the input done < <(cat hosts.txt) ``` There are two standard cures. Deny the inner command the input — `ssh -n`, or `cmd < /dev/null`. Or move the loop's input off descriptor 0 entirely: ```bash while read -r -u 3 host; do ssh "$host" uptime done 3< <(cat hosts.txt) ``` Here `3< <( )` opens the substitution on descriptor 3 and `read -u 3` reads from it, leaving stdin free for whatever the body wants to do with it. **Reading is still line-shaped, with all that implies.** `read -r` (never bare `read`) stops backslashes being treated as escapes, which matters the moment a path contains one. A final line with no trailing newline is stored in the variable but makes `read` return non-zero, so the loop exits without processing it — `while read -r line || [[ -n $line ]]` is the usual guard. And a filename containing a newline is genuinely unrepresentable this way; a NUL-delimited producer with `read -r -d ''` is the honest answer there. ## When you would not reach for it If the input is already a string in a variable, you do not need a second process at all — feed the loop from what you have. If the producer is small and its failure matters more than its streaming, capture it into a file or an array first, check the status, then loop. And in a script that must run under `/bin/sh`, `< <( )` is simply unavailable: it is a bashism, and the failure is a parse error at startup. The reason this pattern is worth memorising is that the pipe version is the one everyone writes first, it works perfectly in every test where the loop only prints, and it silently loses state the moment the loop starts counting or accumulating.
- In `while read -r host; do ssh "$host" uptime; done < <(cat hosts.txt)` only the first host is processed. Why?`ssh` inherits the loop's stdin — the same `/dev/fd` pipe — and drains the remaining lines while reading its own input. Fix it by denying `ssh` that input with `ssh -n` or `< /dev/null`, or by moving the loop's input off descriptor 0: `done 3< <(cat hosts.txt)` with `read -r -u 3`.
- How would you detect that the `find` inside `done < <(find ...)` failed?Not from `$?` — after the loop that is the status of the last command in the body. Options are to have the producer emit a sentinel line you check for, to write its status somewhere the parent can read, or to restructure as a real pipeline whose stages `set -o pipefail` can report on. Silence is the default here, so decide deliberately.
- The last line of the input has no trailing newline and never gets processed. What is happening?`read` returns non-zero when it hits EOF without a delimiter, even though it has already stored the partial line in the variable, so the `while` condition fails and the body is skipped. The standard guard is `while read -r line || [[ -n $line ]]`, which runs the body one final time for that unterminated line.
saying these in an interview costs you the question
- Thinks process substitution itself is what preserves the variables
- Writes done <<(cmd) without the space and expects it to work
- Assumes the loop can see that the producer failed
- Uses bare read instead of read -r for filenames
- Believes the whole output is buffered before the loop starts