Unix shell: loop on a server list and execute interactive commands remotely with ssh

When managing several Unix servers it may be useful to execute some commands remotely looping on them. This can be done in several ways for the simplest case, but you may encounter some problems:

  • If the remote command or the ssh command requires input, the loop created with “cat list.txt | while read SERVER ; do ...” will be interrupted.
  • It may be not possible to execute interactive commands with manually entered input for each server.

The following sample is useful when you need to execute commands on a list of servers and you want to manually enter input data on each of them, but automating the connect-disconnect commands.

# Open file descriptor 3 (fd #3) for reading with list.txt
exec 3< list.txt

# Read from fd #3 until end of file
while read SERVER <&3 ; do
    echo Executing on $SERVER ...
    ssh -t $SERVER script_with_input.sh
    echo
done

# Close input for fd #3
exec 3>&-

The concept behind the script is to open a different file descriptor from the standard input(0) / output(1) / error(2) ones by using exec 3< list.txt. To read data from opened file I use the following syntax: read SERVER <&3. This command reads one value from each line of the file and puts it into "SERVER" variable. It can be expanded for reading multiple values (space delimited on each line of list.txt) by using read VAR1 VAR2 VAR3 <&3. The file descriptor must be closed by using exec 3>&-.

The last detail that gives "full power" to this script is the "-t" option in the ssh command: it asks ssh command to allocate pseudo-tty to allow input on the remote server (see ssh man page)

If you want to read only some lines from list.txt or you want to open the file descriptor against a pipe (i.e. input taken from other commands output), in ksh shell, you have to change the open-file syntax by appending &0 to the exec command. This will tell to open input from the pipe output. For example this will read from list.txt only servers with "ab" in the name:

cat list.txt | grep "ab" | exec 3<&0

This will not work in bash shell because the exec 3<&0 command is executed in a subshell. See Vidar’s Blog : Why Bash is like that: Subshells for an explanation.

In bash shell you can do it with a temporary file ($$ will use the PID of the current process to guarantee the file to be unique):

TMP_FILE=_tmp.$$
cat list.txt | grep "ab" > $TMP_FILE

# Open file descriptor 3 (fd #3) with TMP_FILE
exec 3< $TMP_FILE

Remember to remove the temporary file when finished to read from it.

To debug the current file descriptors you can use the command in your scripts:

lsof -a -p $$ -d0,1,2,3

Note that -d0,1,2,3 is the sample list of descriptors to print out.

Here you'll find a good site with samples on the various types of Bash I/O Redirection.

Another article that explains how redirection works in bash is Bash One-Liners Explained, Part III: All about redirections.

Notes:

Script tested on Linux 2.6.18-274.el5 / RedHat 5.7, HP-UX B.11.11, with bash, ksh and nsh shells.

The script_with_input.sh is the remote interactive command that must be present on the remote server.