How to Use Variables with Brace Expansion

Brace expansion with variable?

create a sequence to control your loop

for i in $(seq 1 $howmany); do
echo "Welcome";
done

How can I do brace expansion on variables?

See man bash:

The order of expansions is: brace expansion, tilde expansion, parameter, variable and arithmetic expansion and command substitution (done in a left-to-right fashion), word splitting, and pathname expansion.

As you see, variable expansion happens later than brace expansion.

Fortunately, you don't need it at all: let the user specify the braced paths, let the shell expand them. You can then just

mv "$@"

If you need to separate the arguments, use an array and parameter expansion:

sources=("${@:1:$#-1}")
target=${@: -1}
mv "${sources[@]}" "$target"

how to use variables with brace expansion

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', ...

How to store curly brackets in a Bash variable

The expression

ls {*.xml,*.txt}

results in Brace expansion and shell passes the expansion (if any) to ls as arguments. Setting shopt -s nullglob makes this expression evaluate to nothing when there are no matching files.

Double quoting the string suppresses the expansion and shell stores the literal contents in your variable name (not sure if that is what you wanted). When you invoke ls with $name as the argument, shell does the variable expansion but no brace expansion is done.

As @Cyrus has mentioned, eval ls $name will force brace expansion and you get the same result as that of ls {\*.xml,\*.txt}.

Order of brace expansion and parameter expansion

from: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html

To avoid conflicts with parameter expansion, the string ‘${’ is not
considered eligible for brace expansion, and inhibits brace expansion
until the closing ‘}’.

That said, for echo $x{1..2} , first the brace expansion takes place, and then the parameter expansion, so we have echo $x1 $x2. For echo ${x{1..2}} the brace expansion doesn't happen, because we are after the ${ and haven't reached the closing } of the parameter expansion.

Regarding the bash manual part you have quoted, left-to-right order still exists for the expansions (with respect to allowed nested ones). Things get clearer if you format the list instead of using , and ;:

  1. brace expansion
  2. In a left-to-right fashion:

    tilde expansion, parameter and variable expansion, arithmetic expansion, and command substitution
  3. word splitting
  4. filename expansion.

Bash variable expansion and brace expansion

You can try

A="a,b"
eval echo {$A}.txt

you get,

a.txt b.txt


Related Topics



Leave a reply



Submit