I'm trying to generate dynamic var names in a shell script to process a set of files with distinct names in a loop as follows:
#!/bin/bash
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ ))
do
echo SAMPLE{$i}
done
I would expect the output:
1-first.with.custom.name
2-second.with.custom.name
but i got:
SAMPLE{1}
SAMPLE{2}
Is it possible generate var names in the fly?
You need to utilize Variable Indirection:
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ ))
do
var="SAMPLE$i"
echo ${!var}
done
From the Bash man page, under 'Parameter Expansion':
"If the first character of parameter is an exclamation point (!), a
level of variable indirection is introduced. Bash uses the value of
the variable formed from the rest of parameter as the name of the
variable; this variable is then expanded and that value is used in the
rest of the substitution, rather than the value of parameter itself.
This is known as indirect expansion."
The Problem
You're using the value of i as if it were an array index. It isn't, because SAMPLE1 and SAMPLE2 are separate variables, not an array.
In addition, when calling echo SAMPLE{$i}
you are only appending the value of i to the word "SAMPLE." The only variable you are dereferencing in this statement is $i, which is why you got the results you did.
Ways to Address the Problem
There are two main ways to address this:
- Multi-stage dereferencing of an interpolated variable, via the eval builtin or indirect variable expansion.
- Iterating over an array, or using i as an index into an array.
Dereferencing with eval
The easiest thing to do in this situation is to use eval:
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ )); do
eval echo \$SAMPLE${i}
done
This will append the value of i to the end of the variable, and then reprocess the resulting line, expanding the interpolated variable name (e.g. SAMPLE1 or SAMPLE2).
Dereferencing with Indirect Variables
The accepted answer for this question is:
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ ))
do
var="SAMPLE$i"
echo ${!var}
done
This is technically a three-step process. First, it assigns an interpolated variable name to var, then dereferences the variable name stored in var, and finally expands the result. It looks a little cleaner, and some people are more comfortable with this syntax than with eval, but the result is largely the same.
Iterating Over an Array
You can simplify both the loop and the expansion by iterating over an array instead of using variable interpolation. For example:
SAMPLE=('1-first.with.custom.name' '2-second.with.custom.name')
for i in "${SAMPLE[@]}"; do
echo "$i"
done
This has added benefits over the other methods. Specifically:
- You don't need to specify a complex loop test.
- You access individual array elements via the $SAMPLE[$i] syntax.
- You can get the total number of elements with the ${#SAMPLE} variable expansion.
Practical Equivalency for Original Example
All three methods will work for the example given in the original question, but the array solution provides the most overall flexibility. Choose whichever one works best for the data you have on hand.
You can use eval
as shown below:
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ ))
do
eval echo \$SAMPLE$i
done
Not as far as I know, They way @johnshen64 said. Also, you could solve your problem using an array like so:
SAMPLE[1]='1-first.with.custom.name'
SAMPLE[2]='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ )) do
echo ${SAMPLE[$i]}
done
Note that you don't need to use numbers as indexes SAMPLE[hello]
will work just as well
Not a standalone answer, just an addition to Miquel's answer which I couldn't fit well in a comment.
You can populate the array using a loop, the += operator, and a here document as well:
SAMPLE=()
while read; do SAMPLE+=("$REPLY"); done <<EOF
1-first.with.custom.name
2-second.with.custom.name
EOF
In bash 4.0, it's as simple as
readarray SAMPLE <<EOF
1-first.with.custom.name
2-second.with.custom.name
EOF