Can I use variables inside {} expansion without `eval`?

Brace expansion happens very early during expansion (first thing, in fact), before variable expansion. To perform brace expansion on the result of a variable expansion, you need to use eval.

You can achieve the same effect without eval if you make extensions a wildcard pattern instead of a brace pattern. Set the extglob option to activate ksh-like patterns.

shopt -s extglob
extensions='@(foo|bar)'
ls 1.$extensions

Here is a way to expand variables inside braces without eval:

end=3
declare -a 'range=({'"1..$end"'})'

We now have a nice array of numbers:

for i in ${range[@]};do echo $i;done
1
2
3

Tested in bash 4.3.11 but should work in all modern versions.


Another technique you can use in bash and some other shells is to use an array:

$ extensions=(foo bar)
$ ls "${extensions[@]/#/1.}"

This is the ${parameter/pattern/string} (pattern substitution) form of parameter expansion.  (Unfortunately, this is not POSIX-compliant either.)  The [@] specifies that the substitution should be applied to each element of the array, and that the separation between the elements should be maintained.  # in a pattern string acts like a ^ in a regular expression (i.e., most s/old/new/ substitutions); it means that the substitution should occur only at the beginning of the parameter values.  So

$ ls "${extensions[@]/#/1.}"

is equivalent to

$ ls "${extensions[0]/#/1.}" "${extensions[1]/#/1.}"

(since this array has two elements, indexed as 0 and 1) and this expands to

$ ls "1.foo" "1.bar"

Quoting

The quotes are important if the “extensions” require quotes — i.e., if they (might possibly, ever) contain spaces or pathname expansion (glob/wildcard) characters.  For example, if

$ extensions=("foo bar" "*r")

then

$ ls ${extensions[@]/#/1.}

(without quotes) would expand to

$ ls 1.foo bar 1.*r

(in which 1.foo and bar are separate arguments), and this, in turn, expands to

$ ls 1.foo bar 1.anteater 1.bar 1.bear 1.cougar 1.deer 1.grasshopper …

(because 1.*r is an unquoted wildcard).  For more discussion of the importance of quoting, see Security implications of forgetting to quote a variable in bash/POSIX shells.

You can also match the end of a string by using % in the pattern:

$ fnames=(cat dog)
$ ls "${fnames[@]/%/.c}"

expands to

$ ls "cat.c" "dog.c"

But beware: % doesn’t work the same as $ in regular expressions.  You have to put it at the beginning of the pattern to constrain the match to occur at the end of the parameter value.  For example, if you have

$ fnames=(cat.c dog.c)

and you want to get cat.o and dog.o, don’t do "${fnames[@]/.c%/.o}" — it won’t work.  Do

$ ls "${fnames[@]/%.c/.o}"

But don’t do "${fnames[@]/.c/.o}" (leaving out the % altogether) either — if one of the “fnames” is dog.catcher.c, it will be converted to dog.oatcher.c (since the first .c gets replaced with .o).

Unfortunately, there is no easy way to add a prefix and a suffix, as you would be able to with

$ ls 1.{foo,bar}.c

See the bash documentation for more information on variables, arrays, and the transformations you can apply to them.