I store in a file named ssh_hosts.txt a list of hostnames. In a shell script, I loop over the hostnames in ssh_hosts.txt and execute a command on the named host via SSH. The shell script is shown below.

The problem is that the script exits after processing the first host. But if I do something other than execute a command on the named host via ssh, the script will run to completion.

In the example below, I've commented out the call to ssh and replaced it with a simple echo of the current host name. This will run to completion.

I am executing this script from a bash shell running under the following Cygwin version on Windows 7:

$ uname -a
CYGWIN_NT-6.1 myHostname 1.7.16(0.262/5/3) 2012-07-20 22:55 i686 Cygwin

These SSH versions are involved:

$ ssh -V
OpenSSH_6.0p1, OpenSSL 1.0.1c 10 May 2012

$ ssh myUsername@remoteHost 'ssh -V'
myUsername@remoteHost password:
OpenSSH_6.7p1 Debian-5+deb8u3, OpenSSL 1.0.1t  3 May 2016

Here is the shell script:


if [[ $# -ne 1 ]]; then
   echo "Usage: $(basename $0) <user name>"
   exit 1


while IFS='' read -r ssh_host || [[ -n "$ssh_host" ]];
   # This line will execute for all hosts listed in ssh_hosts.txt.
   echo $ssh_host

   # This line will execute for *only the first* host in ssh_hosts.txt.
   # ssh $USER@$ssh_host 'echo $(whoami)@$(hostname)'
done < ssh_hosts.txt

How may I get this shell script to execute over all hosts in ssh_hosts.txt rather than just over the first host?


I think you are making it too complicated:

for ssh_host in $(cat ssh_hosts.txt)
   echo $ssh_host
   ssh $user@$ssh_host ....

Also, search github for gsh, which is a perl program that will do more if you do this often.

  • I would have loved to have selected all of the offered answers as "the" answer--they were all very good and I sure appreciate people's time in offering them--, but ultimately I picked this one because it most simply and directly solved my specific problem. Thank you to dave_thompson_085, virtex, dgoo2308, and eggo! – Dave Jun 16 '17 at 11:19

You have two programs in your loop both reading from stdin: read and ssh. When your loop runs, the read command will read the first line from ssh_hosts.txt, then ssh slurps up the rest. That's why you only see it connect to the first host.

I know of two solutions to this problem. One is to rewrite your loop so it doesn't redirect from stdin. A good way to do this is with the for loop that eggo has in his post.

The other solution is to pass the -n option to ssh:

ssh -n $USER@$ssh_host 'echo $(whoami)@$(hostname)'

This tells ssh not to use stdin, leaving the full contents of your ssh_hosts.txt to the read command. This solution requires your ssh sessions to not require any interaction with the user, including prompting for a password (so use ssh keys for authentication).

  • Ahhh stdin, thank you for explaining the underlying problem! – Dave Jun 16 '17 at 11:14

You need to fork it as the ssh will mess up your loop.

I got a script on hand for updating my authorised keys, I did this with 2 scripts:

  • run.sh: read your host list and fork the do_command.sh script
  • do_command.sh: make connection with control socket, execute commands and close

As an extra bonus:

  • all hosts are connected simultaneous
  • multiple commands are executed with one connection
  • a log file with the results

Main script: run.sh

rm -rf log
mkdir log
while read -u10 HOST ; do ( ./do_command.sh $HOST & ); done 10< ssh_hosts.txt
sleep 1
for f in log/*.pid; do
    [ -e "$f" ] && VMN=`ls log/*.pid`
    echo "waiting for process to finish"
while [ "$VMN" != "" ]

  sleep 3
  if (("$COUNT" != "$TIMEOUT"))
        for f in log/*.pid; do
            [ -e "$f" ] && VMN=`ls log/*.pid`
        if [ "$VMN" != "" ]
            echo "waiting already "$COUNT" seconds ..."
            #for VM in $VMN; do 
                #echo -e "\t pid:$myPID \t $VM "

        echo -e "\n!!! timeout, waiting for remaining processes's to shutdown"
        for VM in $VMN; do
            ITEM=$(sed -e 's/.pid//;s/log\///' <<<"$VM")
            echo -e "killing $myPID -> item: $ITEM \t log: $LOGX" 
            kill -9 $myPID 
            rm -rf $VM
            echo -e "\n ###### main Process ###### \n\n \t Killed due to Time out \nFAILED $ITEM" >>$LOGX
            echo "$ITEM Killed due to Time out" >>log/error.txt
        rm -fv log/*.sock
if [ -f log/error.txt ] 
    rm -fv log/*.sock
    echo -e "\n##### FAIL ##############\n"
    cat log/error.txt
    exit 1

Action script: do_command.sh

exec 5<&1 
exec 6<&2
echo "PROCESS $1"
echo $$ >"log/$1.pid"
chmod 777 "log/$1.pid"
exec 1> "log/$1.log" 2>&1
echo "PROCESS $1"
echo "Connecting to $TARGET .... "
ssh -f -N -M -o ControlPath=$SOCKET $TARGET
if [ $? -ne 0 ]; then
    echo "Could not connect $1, Cleanup "
    echo "Could not connect $1, exit " >>log/error.txt
    #ssh -S $SOCKET -O exit $TARGET
    rm -fv "log/$1.pid"
    rm -fv $SOCKET
    echo "FAILED $TARGET">&6
    echo "FAILED $TARGET"
    exit 1

# Do the thing you need to do
# This script copy a file to the remote and executes 2 commands
scp -o ControlPath=$SOCKET authorized_keys $TARGET:.ssh/new_authorized_keys

# Execute a command:
ssh -o ControlPath=$SOCKET $TARGET "mv -fv .ssh/authorized_keys .ssh/old_authorized_keys && cp -fv .ssh/new_authorized_keys .ssh/authorized_keys && chmod 600 .ssh/authorized_keys"

# Another remote command:   
ssh -o ControlPath=$SOCKET $TARGET "ls -la .ssh"

# And Close the socket 
ssh -S $SOCKET -O exit $TARGET

sleep 1
echo Cleanup
rm -fv "log/$1.pid"
rm -fv $SOCKET
exit 0
  • Very nice solution, and yes, the problem I'm trying to solve is that of creative an effective SSH key (both host and user keys) management solution. – Dave Jun 16 '17 at 11:15
  • @Dave don't forget to mark as answered – Danny Goossen Jun 22 '17 at 8:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.