24

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.

1
  • Thanks for all the responses and comments. I will more than likely take the array approach. Commented Aug 1, 2018 at 15:41

4 Answers 4

50

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
3
  • 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
    Commented Jul 31, 2018 at 21:51
  • @ruakh: thanks, updated to show what can be done.
    – choroba
    Commented Jul 31, 2018 at 22:06
  • I wish I could upvote you twice! thanks a lot!
    – Slim Aloui
    Commented May 17, 2022 at 15:59
32

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
1
  • +1. While arrays are relevant to many types of problems, they don't actually answer the question. I came here looking for an answer which arrays weren't suitable for, and you provided.
    – cryptarch
    Commented Jul 22, 2020 at 20:12
15

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.

3
  • 4
    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
    Commented Aug 1, 2018 at 9:14
  • @ilkkachu thanks for the assist. I've updated my answer.
    – kojiro
    Commented Aug 1, 2018 at 13:40
  • For me, this rather prints the variables name rather than the value? Commented Aug 12, 2021 at 6:49
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
5
  • eval is hard to use safely. Yes this can work, but bash indirect expansion is a better way in general. Commented Aug 1, 2018 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
    Commented Aug 1, 2018 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! Commented Aug 1, 2018 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
    Commented Aug 1, 2018 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. Commented Aug 2, 2018 at 0:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .