For loop through servers with custom ports (for i in "user1@server1 -p 12345" "user2@server2 -p 54321" …; do)

I’m trying to run a command on a series of servers via ssh. I have recently changed ssh ports to avoid Internet scanners, but it broke my script. Does anyone know "an easy way" to specify different ports per user@server entry in a for loop?

for i in ' -p 12345' ' -p 54321'
 do printf "e[%sm%se[00mn" 32 $i
 ssh $i crontab -l

The second line is just to print the server name before the output (for readability).

Instead of happiness it gave me: ssh: Could not resolve hostname -p 12345: Name or service not known

It worked before when all ports were the default 22 and didn’t need to be specified.

Asked By: Daniel Krajnik


For a for loop that can loop over more than one variable, switch to zsh. You’re already using zsh syntax by not quoting your variables:

# zsh
for colour host port (
  green 12345
  blue 54321
) {
  print -rP "%F{$colour}%B$host:$port%b%f"
  ssh -p $port $host 'crontab -l'

To loop over lists of arguments, you could use a shell with multidimensional arrays such as ksh93:

# ksh93
  (-p 12345
  (-p 54321
for i in "${!ssh_args[@]}"}; do
  printf 'e[1;32m%se[mn' "${ssh_args[i][2]}"
  ssh "${ssh_args[i][@]}" 'crontab -l'

Or have the elements of the array containing strings with the arguments concatenated using a character that you know doesn’t occur in the strings and split them. zsh does have a ${(s[x])var} splitting character.

ksh and bash also do but it’s only via the clunky split+glob one that is invoked implicitly when you forget to quote an expansion there. You could use it as:

# ksh93 / bash / yash / mksh / zsh --emulate ksh
IFS=:; set -o noglob # tune your split+glob operator by specifying
                     # the separator and disabling globbing
for joined_args in "${ssh_args[@]}"; do
  args=( $joined_args ) # invoke split+glob
  printf 'e[1;32m%se[mn' "${args[2]}"
  ssh "${args[@]}" 'crontab -l'

That’s basically what you did in your approach except you used space instead of : to separate the elements, but didn’t set $IFS and didn’t disable glob.

The fact that your $i wasn’t split could be explained either by $IFS being set to the empty string, or the code being run by zsh instead of bash, where neither splitting nor globbing is done implicitly upon parameter expansion.

In zsh, if you want IFS-splitting, use $=var instead of $var, and if you want globbing: $~var. bash’s $var is like zsh’s $=~var. So in zsh, you’d need:

# zsh
for joined_args in $ssh_args; do
  args=( $=joined_args ) # split on $IFS or better:
  args=( ${(s[:])joined_args} ) # split explicitly on : without
                               # having to touch $IFS
  print -rP "%F{green}%B$host[-1]%b%f"
  ssh $args 'crontab -l'

A portable approach that would work in sh would be to write it:

# sh / bash / dash / ksh / yash / zsh...
while IFS=' ' read<&3 host port; do
    printf '33[1;32m%se[mn' "$host:$port"
    ssh -p "$port" "$host" 'crontab -l'
  } 3<&-
done 3<< 'EOF' 12345 54321

Omitting the -r option to read allows the values to contain the separator (here space) by prefixing them with a backslash like in:

john doe@server1 1234

But contrary to the other approaches, it doesn’t allow those values to contain newline characters (likely not a problem for user, host, service names or port numbers).

Another portable one to loop over lists all of which have the same $n number of elements and that doesn’t have that limitation is to store all the elements in the positional parameters and loop over them in a while [ "$#" -ge "$n" ]; do something with "$1" "$2"...; shift "$n"; done loop. So here:

# sh / bash / dash / ksh / yash / zsh...
eval "$(
  printf "fg_%s='33[3%sm' " 
    black 0 red     1 green 2 yellow 3 
    blue  4 magenta 5 cyan  6 white  7
  printf "bg_%s='33[4%sm' " 
    black 0 red     1 green 2 yellow 3 
    blue  4 magenta 5 cyan  6 white  7
  printf "attr_%s='33[%sm' " 
    reset '' bold     1 dim     2 italics 3 underline 4 
    blink 5  standout 7 reverse 7 secure  8
set -- 
  "$fg_green$attr_bold" 12345 
  "$bg_blue$fg_white" 54321

while [ "$#" -ge 3 ]; do
  colour=$1 host=$2 port=$3
  printf '%sn' "$colour$host:$port$attr_reset"
  ssh -p "$port" "$host" 'crontab -l'
  shift 3
Answered By: Stéphane Chazelas

Since version 7.7, OpenSSH’s ssh (along with its scp and sftp) will accept the destination in the form of a URI. You can specify the port number as part of the URI if you like, for example "ssh://someuser@somehost:42" to connect to port 42. A URI has the advantage of being a single string, as far as the shell is concerned. So you can do this:

for i in 'ssh://' 'ssh://'
 do printf ...
 ssh "$i" ...

The general form of an SSH URI is "ssh://user@host:port". The "user" and "port" parts are both optional and can be left out (along with the "@" or ":" punctuation) if you don’t need to specify values for those.

Answered By: Kenster

The right way would be to store that on config, rather than specifying everything on every connection.

On ~/.ssh/config you could have:

Host  # When connecting to this
User user1  # Use this user
Port 12345  # with this port

Host serv2  # We can also use a different, internal, name
Hostname  # The actual hostname or IP it will use to connect
User user2
Port 54321

Then ssh and ssh serv2 would do the Right Thing™ No need to remember the usernames or port you used for each machine. Benefiting both scripts and humans.

And should you later change the ports, there’s a single place to modify.

It is also possible to use wildcards, so if all machines on had its sshd listening on port 1234, you could match all of them with Host *, without having to list them individually.

Answered By: Ángel

A general solution for looping through two lists, using a pair of elements (one from either list) in each iteration of the loop, in a POSIX shell:


# The list of port numbers
set -- 123245 54321

# Loop over the list of user@host strings
for remote in
    # Use "$remote" together with the port number in "$1", then shift

    printf 'Connecting to %s, port %sn' "$remote" "$1"

    ssh -p "$1" "$remote" crontab -l
Answered By: Kusalananda

Use pdsh for multiple hosts.. It is widely used in data centers with large number of clustered hosts..

From pdsh documentation ..

Run with user ‘foo’ on hosts h0,h1,h2, and user ‘bar’ on hosts h3,h5:

pdsh -w foo@h[0-2],bar@h[3,5]

This tool along side with ssh config file as Ángel mentioned.

Full example below ..


Host server1
Hostname server1
User user1  
Port 12345  

Host server2
Hostname server2
User user2
Port 54321

Now simple run

pdsh -w server1,server2 crontab -l 
Answered By: Abdullah
Categories: Answers Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.