Shell Loop Interaction with SSH

A flawed method to run commands on multiple systems entails a shell while loop over hostnames, and a Secure Shell (SSH) connection to each system. However, the default standard input handling of ssh drains the remaining hosts from the while loop:

#!/bin/sh

# run hostname command on systems listed below; will only work for
# the first host
while read hostname; do
ssh $hostname hostname
done <<EOF
example.com
example.org
EOF

The hostname command will only be run on the first host connected to, as ssh will pass the remaining hostnames in the while loop to hostname as standard input. hostname ignores this input silently, and the while loop then exits, as no hosts remain to connect to.

Any command that reads from standard input will consume the rest of the items to loop over, as illustrated below using the cat command.

#!/bin/sh

while read line; do
echo "line: $line"
cat
done <<EOF
foo
bar
zot
EOF

The cat should print the remaining items of the loop to standard output, as by default it reads from standard input.

$ sh test
line: foo
bar
zot

To avoid this problem, alter where the problematic command reads standard input from. If no standard input need be passed to the command, read standard input from the special /dev/null device:

#!/bin/sh

while read hostname; do
ssh $hostname hostname < /dev/null
done <<EOF
example.com
example.org
EOF

Another option: use the -n option to ssh, which prevents ssh from reading from standard input:

ssh -n $hostname hostname

However, sometimes standard input must be passed to the remote system, for example when listing, editing, then updating crontab(5) data. The first ssh instance disables passing standard input, while the second does not, as the updated crontab(5) data is passed via standard input over ssh:

while read hostname; do
ssh -n $hostname crontab -l | \
grep -v rdate | \
ssh $hostname crontab -
done

On a somewhat related note, background processes on Unix should reopen standard input to /dev/null.

Alternatives & Caveats