52

I want to use $var in a shell brace expansion with a range, in bash. Simply putting {$var1..$var2} doesn't work, so I went "lateral"...

The following works, but it's a bit kludgey.

# remove the split files
echo rm foo.{$ext0..$extN} rm-segments > rm-segments
source rm-segments

Is there a more "normal" way?

ilkkachu
  • 138,973
Peter.O
  • 32,916

5 Answers5

49

You may want to try :

eval rm foo.{$ext0..$extN}

Not sure whether this is the best answer, but it certainly is one.

asoundmove
  • 2,495
  • 1
    Durrh! :( I couldn't see the obvious... Thanks :) – Peter.O Feb 21 '11 at 03:54
  • 5
    In bash, {} expansion happens before $ expansion, so I don't think there's any other way to do this other than using eval or some other trick to cause two passes through the expression. – mattdm Feb 21 '11 at 04:14
  • 9
    I just "got" it!... There is no brace expansion with {$one..$three}, because it is not a valid form of brace-expansion, which in this case expects integers... It only becomes a valid form after the $var expansion, which eval then passes through the same process to generate a normal brace sequence expansion... 1 2 3 QED ;) ... Summary: the simple presence of a brace pair does't trigger brace expansion... only a valid form triggers it. – Peter.O Feb 21 '11 at 06:49
  • @fred: Welcome :-) – asoundmove Feb 21 '11 at 21:48
  • 1
    When using a variable like a={0..9}; echo $a; there is no brace extension. Using eval , it works. So I think @mattdm's explanation is good. – dr0i May 22 '17 at 08:32
  • 1
    You can also use bash -c "..." instead of eval to ensure that brace-expansion actually works shell-agnostic. e.g. Dash does not support it. – Morty Jan 22 '21 at 09:35
29

As you already realized, {1..3} expands to 1 2 3 but {foo..bar} or {$foo..$bar} don't trigger brace expansion, and the latter is subsequently expanded to replace $foo and $bar by their values.

A fallback on GNU (e.g. non-embedded Linux) is the seq command.

for x in `seq $ext0 $extN`; do rm foo.$x; done

Another possibility. if foo. contains no shell special character, is

rm `seq $ext0 $extN | sed 's/^/foo./'`

The simplest solution is to use zsh, where rm foo.{$ext0..$extN} does what you want.

  • Thanks for the options.. It's good to see them all grouped .. I'll stick with bash and eval... eval is simple enough now that I know what's going on behind the scenes; so it's no longer a problem. No need to change a good horse because of the rider :)... – Peter.O Feb 22 '11 at 04:36
  • A small suggestion: seq -w $ext0 $extN may be closer to what you want. Without -w, the inputs 001 003 will produce 1 2 3, whereas with it, you'll get 001 002 003. I generally want the latter. – HorsePunchKid Apr 11 '20 at 14:56
3
    function f_over_range {
        for i in $(eval echo {$1..$2}); do
            f $i
        done
    }

    function f {
        echo $1
    }

    f_over_range 0 5
    f_over_range 00 05

Notes:

  • Using eval exposes command injection security risk
  • Linux will print "00\n01\n02..etc", but OSX will print "1\n2\n...etc"
  • Using seq or C-style for loops won't match brace expansion's handling of leading zeros
  • I think this is the best answer to the question which brought me here: "Why can't a variable be in between the range-making curly braces?" I think this might have been the OP's question as well, given the quote, "I had proposed that the problem was to 'use a variable inside mkdir {1 .. $variable}'". I always get tripped up by how (actually, mostly by when) the shell does the brace expansion. I've just got to keep telling myself, "eval, echo, range-with-variables, all inside a dollar paren ($(...))". +1 – bballdave025 Sep 22 '22 at 19:09
3

While the other answers discuss using eval and seq, in bash, you can use a traditional C style for loop in an arithmetic context. The variables ext0 and extN are expanded inside the ((..)) causing the loop to run for the range defined.

for (( idx = ext0; idx <= extN; idx++ )); do
    [[ -f "$foo.$idx" ]] || { printf "file %s does not exist" "$foo.$idx" >&2 ; continue ; }
    rm "$foo.$idx"
done

If you are looking for an optimal way and avoid multiple rm commands, you can use a temporary placeholder to store the filename results and call rm in one-shot.

 results=()
 for (( idx = ext0; idx <= extN; idx++ )); do
     [[ -f "$foo.$idx" ]] || { printf "file %s does not exist" "$foo.$idx" >&2 ; continue ; }
     results+=( "$foo.$idx" )
 done

and now call the rm command on the expanded array

 rm -- "${results[@]}"
Inian
  • 12,807
0

I have the following command that finds the total number of files in my root directory:

command 01:

 ls -dq *dog* | wc -l

I also have a command to create multiple folders using a numeric sequence:

command 02:

mkdir -p {0..3}

I know in the root directory there are 3 files that have the string "DOG" in your name.

But I would like to use the 01 command in other root folders and that counted how many files have "DOG" in your name, store the result in variable result and used this variable in place of "3" as seen in command 02.

Below all my possible attempts without having been able to create subfolders 1, 2 and 3:

result=$ls -dq *dog* | wc -l) mkdir {0..$result}

result=$(ls -dq dog | wc -l) | mkdir {0..$result}

ls -dq dog | wc -l | xargs I{} mkdir {0..{}}

ls -dq dog | wc -l | while read file; do mkdir -p {1.."$file"}; done

But for any attempt above I only have two results: or is created a folder called {0..3} or nothing happens besides receiving a printing of the result of

 ls -dq *dog* | wc -l

Solved so bellow:

Looking at this answer it was possible to build a model capable of getting the desired result:

eval "mkdir -p {1..`ls -dq *dog* | wc -l`}"

While strictly is not the most appropriate answer because I had proposed that the problem was to "use a variable inside mkdir {1 .. $variable}"