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 'user1@server1.domain1.com -p 12345' 'user2@server2.domain2.com -p 54321'
 do printf "e[%sm%se[00mn" 32 $i
 ssh $i crontab -l
done

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 server1.domain1.com -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 user1@server1.domain1.com 12345
  blue  user2@server2.domain2.com 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
ssh_args=(
  (-p 12345 user1@server1.domain1.com)
  (-p 54321 user2@server2.domain2.com)
)
for i in "${!ssh_args[@]}"}; do
  printf 'e[1;32m%se[mn' "${ssh_args[i][2]}"
  ssh "${ssh_args[i][@]}" 'crontab -l'
done

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
ssh_args=(
  -p:12345:user1@server1.domain1.com
  -p:54321:user2@server2.domain2.com
)
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'
done

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
ssh_args=(
  -p:12345:user1@server1.domain1.com
  -p:54321:user2@server2.domain2.com
)
IFS=:
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'
done

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'
user1@server1.domain1.com 12345
user2@server2.domain2.com 54321
EOF

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" user1@server1.domain1.com 12345 
  "$bg_blue$fg_white"   user2@server2.domain2.com 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
done
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://user1@server1.domain1.com:12345' 'ssh://user2@server2.domain2.com:54321'
 do printf ...
 ssh "$i" ...
done

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 server1.domain1.com  # 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 server1.domain1.com  # The actual hostname or IP it will use to connect
User user2
Port 54321

Then ssh server1.domain1.com 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 domain1.com had its sshd listening on port 1234, you could match all of them with Host *.domain1.com, 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:

#!/bin/sh

# The list of port numbers
set -- 123245 54321

# Loop over the list of user@host strings
for remote in 
    user1@server1.domain1.com 
    user2@server2.domain2.com
do
    # 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
    shift
done
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]

https://code.google.com/archive/p/pdsh/wikis/UsingPDSH.wiki

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

Full example below ..

~/.ssh/config

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.