Using variables with the sequence-expression form ({<numFrom>..<numTo>}
) of brace expansion only works in ksh
and zsh
, but, unfortunately, not in bash
(and (mostly) strictly POSIX-features-only shells such as dash
do not support brace expansion at all, so brace expansion should be avoided with /bin/sh
altogether).
Given your symptoms, I assume you're using bash
, where you can only use literals in sequence expressions (e.g., {1..3}
); from the manual (emphasis mine):
Brace expansion is performed before any other expansions, and any characters special to other expansions are preserved in the result.
In other words: at the time a brace expression is evaluated, variable references have not been expanded (resolved) yet; interpreting literals such as $var1
and $var2
as numbers in the context of a sequence expression therefore fails, so the brace expression is considered invalid and as not expanded.
Note, however, that the variable references are expanded, namely at a later stage of overall expansion; in the case at hand the literal result is the single word '{1..4}'
- an unexpanded brace expression with variable values expanded.
While the list form of brace expansion (e.g., {foo,bar)
) is expanded the same way, later variable expansion is not an issue there, because no interpretation of the list elements is needed up front; e.g. {$var1,$var2}
correctly results in the 2 words 1
and 4
.
As for why variables cannot be used in sequence expressions: historically, the list form of brace expansion came first, and when the sequence-expression form was later introduced, the order of expansions was already fixed.
For a general overview of brace expansion, see this answer.
Workarounds
Note: The workarounds focus on numerical sequence expressions, as in the question; the eval
-based workaround also demonstrates use of variables with the less common character sequence expressions, which produce ranges of English letters (e.g., {a..c}
to produce a b c
).
A seq
-based workaround is possible, as demonstrated in Jameson's answer.
A small caveat is that seq
is not a POSIX utility, but most modern Unix-like platforms have it.
To refine it a little, using seq
's -f
option to supply a printf
-style format string, and demonstrating two-digit zero-padding:
seq -f '%02.f.txt' $var1 $var2 | xargs ls # '%02.f'==zero-pad to 2 digits, no decimal places
Note that to make it fully robust - in case the resulting words contain spaces or tabs - you'd need to employ embedded quoting:
seq -f '"%02.f a.txt"' $var1 $var2 | xargs ls
ls
then sees 01 a.txt
, 02 a.txt
, ... with the argument boundaries correctly preserved.
If you want to robustly collect the resulting words in a Bash array first, e.g., ${words[@]}
:
IFS=$'\n' read -d '' -ra words < <(seq -f '%02.f.txt' $var1 $var2)
ls "${words[@]}"
The following are pure Bash workarounds:
A limited workaround using Bash features only is to use eval
:
var1=1 var2=4
# Safety check
(( 10#$var1 + 10#$var2 || 1 )) 2>/dev/null || { echo "Need decimal integers." >&2; exit 1; }
ls $(eval printf '%s\ ' "{$var1..$var2}.txt") # -> ls 1.txt 2.txt 3.txt 4.txt
You can apply a similar technique to a character sequence expression;
var1=a var2=c
# Safety check
[[ $var1 == [a-zA-Z] && $var2 == [a-zA-Z] ]] || { echo "Need single letters."; exit 1; }
ls $(eval printf '%s\ ' "{$var1..$var2}.txt") # -> ls a.txt b.txt c.txt
Note:
- A check is performed up front to ensure that
$var1
and $var2
contain decimal integers or single English letters, which then makes it safe to use eval
. Generally, using eval
with unchecked input is a security risk and use of eval
is therefore best avoided.
- Given that output from
eval
must be passed unquoted to ls
here, so that the shell splits the output into individual arguments through words-splitting, this only works if the resulting filenames contain no embedded spaces or other shell metacharacters.
A more robust, but more cumbersome pure Bash workaround to use an array to create the equivalent words:
var1=1 var2=4
# Emulate brace sequence expression using an array.
args=()
for (( i = var1; i <= var2; i++ )); do
args+=( "$i.txt" )
done
ls "${args[@]}"
- This approach bears no security risk and also works with resulting filenames with embedded shell metacharacters, such as spaces.
- Custom increments can be implemented by replacing
i++
with, e.g., i+=2
to step in increments of 2.
- Implementing zero-padding would require use of
printf
; e.g., as follows:
args+=( "$(printf '%02d.txt' "$i")" ) # -> '01.txt', '02.txt', ...
.txt
) in the expansion. – Raddy