How can I generate new variable names on the fly i

2019-01-05 13:03发布

问题:

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?

回答1:

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."



回答2:

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:

  1. Multi-stage dereferencing of an interpolated variable, via the eval builtin or indirect variable expansion.
  2. 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:

  1. You don't need to specify a complex loop test.
  2. You access individual array elements via the $SAMPLE[$i] syntax.
  3. 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.



回答3:

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


回答4:

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



回答5:

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


标签: