14

I am writing a bash script to use rsync and update files on about 20 different servers.

I have the rsync part figured out. What I'm having trouble with is going through a list of variables.

My script thus far look like this:

#!/bin/bash
SERVER1="192.xxx.xxx.2"
SERVER2="192.xxx.xxx.3"
SERVER3="192.xxx.xxx.4"
SERVER4="192.xxx.xxx.5"
SERVER5="192.xxx.xxx.6"
SERVER6="192.xxx.xxx.7"

for ((i=1; i<7; i++))
do
    echo [Server IP Address]
done

Where [Server IP Address] should be the value of the associated variable. So when i = 1 I should echo the value of $SERVER1.

I've tried several iterations of this including

echo "$SERVER$i"    # printed the value of i
echo "SERVER$i"     # printer "SERVER" plus the value of i ex: SERVER 1 where i = 1
echo $("SERVER$i")  # produced an error SERVER1: command not found where i = 1
echo $$SERVER$i     # printed a four digit number followed by "SERVER" plus the value of i
echo \$$SERVER$i    # printed "$" plus the value of i

It has been a long time since I scripted so I know I am missing something. Plus I'm sure I'm mixing in what I could do using C#, which I've used for the past 11 years.

Is what I'm trying to do even possible? Or should I be putting these values in an array and looping through the array? I need to this same thing for production IP addresses as well as location names.

This is all in an effort to not have to repeat a block of code I will be using to sync the files on the remote server.

  • Thanks for all the responses and comments. I will more than likely take the array approach. – Paul Stoner Aug 1 '18 at 15:41
32

Use an array.

#! /bin/bash
servers=( 192.xxx.xxx.2 192.xxx.xxx.3
          192.xxx.xxx.4 192.xxx.xxx.5
          192.xxx.xxx.6 192.xxx.xxx.7
)

for server in "${servers[@]}" ; do
    echo "$server"
done
  • 6
    +1; and note that you can put arbitrary whitespace before/after/between the array elements, so you can (if desired) put one element per line as in the OP. – ruakh Jul 31 '18 at 21:51
  • @ruakh: thanks, updated to show what can be done. – choroba Jul 31 '18 at 22:06
27

As the other answers point out, an array is the most convenient way to do this. However, for completeness, the exact thing you are asking for is indirect expansion. Rewritten as follows, your sample will also work using this method:

#!/bin/bash
SERVER1="192.xxx.xxx.2"
SERVER2="192.xxx.xxx.3"
SERVER3="192.xxx.xxx.4"
SERVER4="192.xxx.xxx.5"
SERVER5="192.xxx.xxx.6"
SERVER6="192.xxx.xxx.7"

for ((i=1; i<7; i++))
do
    servervar="SERVER$i"
    echo "${!servervar}"
done

If you're OK with just putting the list of IP addresses in the the for loop, then you might also consider simply using some brace expansions to iterate over whatever you need:

#!/bin/bash

for server in \
192.xxx.xxx.{2..7} \
192.yyy.yyy.{42..50} \
192.zzz.zzz.254
do
    echo "$server"
done

But if you need to reuse the (possibly brace-expanded) list, then using the list to initialize an array would be the way to go:

#!/bin/bash

servers=(
192.xxx.xxx.{2..7} 
192.yyy.yyy.{42..50}
192.zzz.zzz.254 )

for server in "${servers[@]}"
do
    echo "$server"
done
14

While I would probably go with one of the array answers myself, I’d like to point out that it is possible to loop over names directly. You can do

for name in "${!SERVER*}"; do
    echo "${!name}"
done

or, in 4.3 and up, you can use a nameref:

declare -n name
for name in "${!SERVER*}"; do
    echo "$name"
done

Hat tip ilkkachu for the < 4.3 solution.

  • 2
    Indirect expansion (${!var}) works with older versions of Bash too: foo1=aa; foo2=bbb; foox=cccc; for p in "${!foo@}"; do echo "$p = ${!p}"; done – ilkkachu Aug 1 '18 at 9:14
  • @ilkkachu thanks for the assist. I've updated my answer. – kojiro Aug 1 '18 at 13:40
6

Everyone who said you should use an array is correct, but -- as an academic exercise -- if you were determined to do it with separate variables ending in a number (SERVER1, SERVER2, etc.), this is how you'd do it:

for ((i=1; i<7; i++))
do
    eval echo \"\$SERVER$i\"
done
  • eval is hard to use safely. Yes this can work, but bash indirect expansion is a better way in general. – Peter Cordes Aug 1 '18 at 20:26
  • Habit. When I started writing shell scripts, even in bash, there was no indirect expansion. "eval" is perfectly safe if you understand how to escape properly. Still, I agree with you that indirect expansion is better...but the old ways still work. I wouldn't do either in this case anyway. I'd use an array! – Beam Davis Aug 1 '18 at 22:27
  • In this case, notice that you didn't escape the double quotes, so after eval is done, you have echo $SERVER1, not echo "$SERVER1". Yes possible to use safely, but very easy to use unsafely or not how you intended! – Peter Cordes Aug 1 '18 at 22:29
  • Since a server name would contain no spaces, it wouldn't have mattered in this case anyway...but you are right, the double-quotes should be escaped. Fixed in answer! – Beam Davis Aug 1 '18 at 22:47
  • Right, removing the double-quotes entirely (to avoid the misleading impression that they were protecting anything) would be the other option. But escaping them too is the more generic way, so +1. – Peter Cordes Aug 2 '18 at 0:21

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.