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
- for loop - a common alternative, and perhaps useless use of cat, instead loops over the hostnames via for host in `cat hostlist`; …. Beware extraneous IFS related data in the file, though this should not be a concern for hostnames.
- while loops will not process the last line, if the file does not end with a newline. This is a risk if the hostname list origniates on a foreign system, or via any editor that does not enforce the traditional unix requirement that files end with a trailing newline.
$ echo -n $IFS | od -bc
0000000 040 011 012 000
\t \n \0
0000004