Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to use variables with brace expansion [duplicate]

I have four files:

1.txt  2.txt  3.txt  4.txt

in linux shell, I could use : ls {1..4}.txt to list all the four files but if I set two variables : var1=1 and var2=4, how to list the four files? that is:

var1=1
var2=4
ls {$var1..$var2}.txt  # error

what is the correct code?

like image 685
user2848932 Avatar asked Mar 15 '23 06:03

user2848932


2 Answers

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', ...
like image 186
mklement0 Avatar answered Apr 02 '23 10:04

mklement0


For that particular piece of syntax (a "sequence expression") you're out of luck, see Bash man page:

A sequence expression takes the form {x..y[..incr]}, where x and y are either integers or single characters, and incr, an optional increment, is an integer.

However, you could instead use the seq utility, which would have a similar effect -- and the approach would allow for the use of variables:

var1=1
var2=4
for i in `seq $var1 $var2`; do
    ls ${i}.txt
done

Or, if calling ls four times instead of once bothers you, and/or you want it all on one line, something like:

for i in `seq $var1 $var2`; do echo ${i}.txt; done | xargs ls

From seq(1) man page:

   seq [OPTION]... LAST
   seq [OPTION]... FIRST LAST
   seq [OPTION]... FIRST INCREMENT LAST
like image 31
Jameson Avatar answered Apr 02 '23 11:04

Jameson