A bash script runs `while read -r host; do ssh "$host" uptime; done < hosts.txt` but only ever connects to the first host in the file. What is consuming the rest of the input, and how do you fix the loop?
answer
- the body shares the loop's input
- who else is reading that file?
- ssh forwards stdin to the remote
- one iteration, no error, exit 0
- give the command /dev/null instead
basics
~20 sThe loop body inherits the loop's standard input, and ssh reads standard input to forward it to the remote command — so the first ssh drains hosts.txt. Run ssh with -n, or redirect its input from /dev/null.
solid answer
~50 sThe redirection on `done` attaches `hosts.txt` to the whole loop, and every command inside the body inherits that same standard input. `ssh` reads its standard input and forwards it to the remote command, so on the first iteration it happily consumes the entire remainder of the file. `read` then finds end of file and the loop stops after one host. The fix is to stop the body from touching that stream: `ssh -n "$host" uptime`, which makes ssh read from /dev/null, or the explicit `ssh "$host" uptime < /dev/null`. The same trap catches any interactive-ish tool in a loop body — `mysql`, `ffmpeg` (which has `-nostdin` for it) — so the habit is worth having. If you cannot control the command, the other route is to read on a different file descriptor so the body's stdin is never the data file.
code
bash · 13 linesprintf 'one\ntwo\nthree\n' > /tmp/items.txt
echo '--- body eats stdin ---'
while read -r item; do
echo "processing $item"
cat > /dev/null # stands in for ssh: reads all of stdin
done < /tmp/items.txt
echo '--- body given /dev/null ---'
while read -r item; do
echo "processing $item"
cat > /dev/null < /dev/null
done < /tmp/items.txtgo deeper
Know the habit rather than the theory: when you run a command inside a while read loop, give it < /dev/null (or ssh -n) so it cannot eat the list you are reading.
Explain that the redirect on done becomes the standard input of every command in the body, and that ssh reads stdin to forward it to the remote command, which is why the file is drained on the first pass.
Diagnose it from the symptom — a loop that ends early with no error and a variable iteration count — separate it from the subshell variable-loss bug, and pick between -n, a per-command redirect, a separate descriptor, or replacing the loop with xargs.
Push the pattern out of hand-rolled loops entirely: fan-out over hosts belongs in a tool with real concurrency, timeouts and per-host status, and a shell loop over ssh should be a conscious choice for small, bounded work rather than the default.
## The symptom ```bash while read -r host; do ssh "$host" uptime done < hosts.txt ``` With ten hosts listed you get one result, no error, and exit status 0. Nothing looks wrong: `read` is correct, the redirect is the recommended shape, and the loop clearly ran at least once. Interviewers like this one because every obvious explanation — a subshell, a broken `read`, a failing `ssh` — is the wrong one. ## What is really happening `done < hosts.txt` attaches the file to the *entire compound command*. Standard input is inherited, so every command the body runs starts with `hosts.txt` as its own standard input, positioned wherever the previous reads left it. `ssh` is designed to be usable as a pipe: `echo hi | ssh host cat` must work, so ssh reads its standard input and forwards it to the remote command. In the loop, ssh has no idea that the stream it is reading is your list of hosts. It reads it all, ships it to `uptime` on the remote side, which ignores it, and returns. Control comes back to `read`, which now finds end of file, so the loop condition fails and the loop ends after a single iteration. The tell in production is that you *sometimes* get two or three iterations: how much ssh manages to slurp before the remote command exits is a timing matter, so the bug can look intermittent. ## Fix 1 — take the command's stdin away ```bash while read -r host; do ssh -n "$host" uptime done < hosts.txt ``` `ssh -n` redirects ssh's standard input from `/dev/null`. It is the documented option for exactly this situation and is the fix to reach for when ssh is the culprit. The general form, which works for any command, is an explicit redirect on the offending command: ```bash while read -r host; do ssh "$host" uptime < /dev/null done < hosts.txt ``` Do not put `< /dev/null` on the loop itself — that would starve `read` and the loop would not run at all. ## Fix 2 — read on a descriptor the body does not use If the body must keep its own standard input free, read the list on a numbered descriptor instead of on stdin, and tell `read` to use it. Bash lets you attach a file to a spare descriptor on the `done` and have `read` pull from that one, leaving the body's standard input untouched by the loop's data. The descriptor mechanics are a topic in their own right; what matters for the loop is the principle — the data stream and the body's stdin do not have to be the same stream. ## Fix 3 — do not loop at all Often the honest answer is that the loop is the wrong tool. `xargs` reads the list itself, so there is no shared stdin to fight over, and it gives you parallelism for free: ```bash xargs -a hosts.txt -I{} -P4 ssh -n {} uptime ``` ssh still needs `-n` there, because xargs gives its children the shell's stdin, but the loop-drain problem is gone. ## The general rule Any command in a loop body that reads standard input will eat the loop's input, and most of the offenders are commands you would not think of as "reading input": - `ssh` — forwards stdin to the remote command; use `-n`. - `mysql`, `psql` — read a script from stdin when not given a file. - `ffmpeg` — reads stdin for keyboard controls; has `-nostdin`. - Anything that might prompt for confirmation, which will consume a line as the answer. When a `while read` loop mysteriously processes fewer items than the file contains, this is the first thing to check — before you suspect the file, `read`, or your shell.
- Why can you not fix this by putting `< /dev/null` on the `done` line?That would replace the loop's own input, so `read` would see end of file immediately and the body would never run. The redirect has to go on the offending command inside the body, or you use ssh's `-n` option — you are taking stdin away from ssh, not from the loop.
- Which other commands cause this in a loop body?Anything that reads standard input when it is not obvious: `mysql` and `psql` treat stdin as a script when no file is given, `ffmpeg` reads it for keyboard controls and offers `-nostdin`, and any command that prompts for confirmation will consume a line as the answer. Redirecting from /dev/null in the body is the blanket defence.
- How would you tell this apart from the subshell bug where a loop's variables vanish?Different symptom. Here the loop stops early and the body runs once; there the body runs for every line and only the variables are lost afterwards. Loop stops early points at something in the body eating stdin; correct iteration count with empty variables afterwards points at the loop being a pipeline stage.
saying these in an interview costs you the question
- Claims read only ever reads a single line
- Blames a subshell even though there is no pipe
- Says ssh -T stops ssh reading standard input
- Adds sleep or retry logic instead of fixing stdin
- Rewrites it as for h in $(cat hosts.txt)