Brace expansion with variable? [duplicate]
Asked Answered
S

7

36
#!/bin/sh
for i in {1..5}
do
   echo "Welcome"
done

Would work, displays Welcome 5 times.

#!/bin/sh
howmany=`grep -c $1 /root/file`
for i in {1..$howmany}
do
   echo "Welcome"
done

Doesn't work! howmany would equal 5 as that is what the output of grep -c would display. $1 is parameter 1 which is specific when running the script.

Any ideas?

Swaim answered 17/10, 2013 at 16:54 Comment(8)
add an echo $howmany for debugging to check it IS 5 'doesn't work' ==> could mean anything, send the full output of a run.Mathieu
You can use the seq command. For i in $(seq 1 $howmany)Janiculum
@user2701753, I tried with: x=5; for i in {1..$x}; do echo "Hi"; done and also doesn't work. It only prints Hi once.Argybargy
I done some of the suggested comments: i.imgur.com/xCza7Fv.png You can see the output above in the picture.Swaim
Bash is not ksh: exp={0..9}; echo $exp; 0 1 2 3 4 5 6 7 8 9Elastomer
@tjm3772 unfortunately not, that's where I got the seq idea from but that doesn't work with alphabetic characters. I couldn't find a solution to my problem at that answer, unless I'm missing it.Hofmann
@GillesQuénot not entirely sure what you're saying but if you're telling me to use brace expansion, you can't put variables in the expansion in bash. I would need to do {$range} or at the very least {$rangeFrom..$rangeTo} but bash doesn't support this.Hofmann
Check my answer. My example is ksh. I know the limitations of bash.Elastomer
O
15

create a sequence to control your loop

for i in $(seq 1 $howmany); do
echo "Welcome";
done
Osteoclast answered 17/10, 2013 at 17:0 Comment(1)
seq is not a POSIX-standardized tool. It's not guaranteed to be installed at all on a POSIX system, much less to behave in any specific way.Kubetz
G
34

Workarounds for not being able to use variables in a sequence brace expression:

  • If the intent is merely to iterate over numbers in a range - as in the OP's case - the best choice is not to use brace expansion, but instead use bash's C-style loop - see user000001's answer.

    • If the specific numbers aren't important and you simply need to execute a loop body a specified number of times, Cole Tierney's answer is an option.
  • If use of brace expansion is desired nonetheless:

    • If you do NOT need the numbers in the list to have a prefix or postfix, use the seq utility with an unquoted command substitution (small caveat: seq is NOT a POSIX utility, but it is widely available); e.g.

      • echo $(seq 3) -> 1 2 3; start number 1 implied
        • echo $(seq -f '%02.f' 3) -> 01 02 03 - zero-padded
      • echo $(seq 2 4) -> 2 3 4; explicit start and end numbers
      • echo $(seq 1 2 5) -> 1 3 5; custom increment (the 2 in the middle)
    • If you DO need the numbers in the list to have a prefix or postfix, you have several choices:

      • Use the seq utility with its -f option for providing a printf-style format string (as used above for zero-padding), or pure Bash workarounds based on eval (extra care needed!) or building an array in a loop, all of which are detailed in this answer.
      • You could also consider implementing the functionality generically, such as by writing a custom shell function or a custom script with utilities such as awk or perl.

Example of safe use of eval with variables driving a sequence brace expression:

The variables are validated beforehand, to make sure they contain decimal integers.

from=1 to=3  # sample values

# Ensure that $from and $to are decimal numbers and abort, if they are not.
(( 10#$from + 10#$to || 1 )) 2>/dev/null || { echo "Need decimal integers" >&2; exit 1; }

eval echo "A{$from..$to}"  # -> 'A1 A2 A3'

General overview of brace expansion

The main purpose of brace expansion is to expand to a list of tokens with each token having an optional prefix and/or postfix; brace expansions must be unquoted and come in 2 flavors:

  • a fixed series (list) of comma-separated strings - variables supported
    • specifies and expands to a fixed number of tokens (2 or more); e.g.:
    • echo A{b,c,d} -> Ab Ac Ad, i.e., 3 tokens, as implied by the number of args.
    • echo {/,$HOME/}Library e.g., -> /Library /User/jdoe/Library
    • Variable references - and even globs - are supported, but note that they get expanded after brace expansion, in its result, in the course of normal evaluation.
  • a sequence expression (range) with .., typically numerical - variables NOT supported

    • expands to a variable number of tokens, driven by literal start and end points (for historical reasons, use of variables is NOT supported - see the comments on user000001's answer):
      • [rare] strings: only single English letters allowed; e.g. {a..c}
      • numbers: decimal integers only; e.g., {1..10}, {10..1}, {-1..2}
        • example with prefix and postfix: A{1..3}# -> A1# A2# A3#
        • broken example with variables: {$from..$to} # !! FAILS - $from and $to are interpreted as literals and therefore not recognized as either a single letter or a decimal integer - no brace expansion is performed (see below).
          • by contrast, using variables does work in zsh and ksh.
      • bash 4+ adds two features:
        • optional increment step value:
          • echo A{1..5..2} -> A1 A3 A5 - numbers incremented by 2
        • ability to zero-pad:
          • echo A{001..003} -> A001 A002 A003
  • An invalid brace expression is not expanded (treated like a regular unquoted string, with { and } treated as literals):

    • echo {} -> '{}' - invalid as a brace expr.: at least 2 ,-separated tokens needed
      • This allows the use of unquoted {} with find, for instance.
    • echo {1..$to} -> '{1..<value-of-$to>}' - invalid as a brace expr. in bash: variables not supported; however, valid in ksh and zsh.
    • (fish, by contrast, expands any {...} sequence; similarly, zsh has option BRACE_CCL (OFF by default) for expanding individual characters inside {..}, which effectively causes expansion of any nonempty {...} sequence.)
Gracioso answered 12/3, 2015 at 22:55 Comment(0)
D
25

The brace expansion is evaluated before the variables are expanded. You need a c-style for loop instead:

for ((i=1;i<=howmany;i++))
do
   echo "Welcome"
done
Dolorisdolorita answered 17/10, 2013 at 17:0 Comment(5)
Why is that? Wouldn't it make more sense to first expand the variables, then the bracket expression?Argybargy
@Argybargy Perhaps it would make sense. But this is how they designed the language.Dolorisdolorita
@Argybargy It might, but bash simply doesn't. Originally, brace expansion was designed for things like a{b,c}d to be expanded to abd acd; the {1..10} syntax was a later addition, but the order of expansion was already fixed. I suspect it wasn't considered worth the trouble to change that to allow for parameters inside the braces. For what it's worth, zsh does allow parameter expansions inside brace expansions.Platinocyanide
@chepner: Good to know; I was puzzled at first that it does work with variables in the string-token list form: v1=a v2=b; echo {$v1,$v2} -> 'a b'. Bash first turns that into token list "$v1 $v2" (brace expansion), and then expands the variable references (parameter expansion), correct? Since this expansion order must be retained for backward compatibility, using variables in the numeric-range form cannot work, because, without knowing what numbers the variable references represent, bash cannot create the token list, because it doesn't know what and how many tokens to create.Gracioso
@chepner: Also good to know that you can use variable references in zsh; ditto in ksh.Gracioso
O
15

create a sequence to control your loop

for i in $(seq 1 $howmany); do
echo "Welcome";
done
Osteoclast answered 17/10, 2013 at 17:0 Comment(1)
seq is not a POSIX-standardized tool. It's not guaranteed to be installed at all on a POSIX system, much less to behave in any specific way.Kubetz
M
11

The problem is that the "brace expansion" is performed before the "variable expansion"

for i in $(seq 1 $howmany) 

works as @damienfrancois said, or, if you would like:

for i in $(eval echo "{$start..10}") 

probably does, but don't use it for everyone's sanity.

Mathieu answered 17/10, 2013 at 17:4 Comment(1)
You need "s not 's in the eval. for i in $(eval echo "{$start..10}")Publia
E
9

You could also use a while loop:

while ((howmany--)); do
   echo "Welcome"
done
Emilie answered 17/10, 2013 at 18:13 Comment(0)
S
1

We could also use eval in this case:

howmany=`grep -c $1 /root/file`
for i in $(eval echo {1..$howmany}); do
    echo "Welcome"
done
Sporophore answered 9/1, 2017 at 18:46 Comment(0)
B
1

With ksh, can be run in a subshell in bash:

#!/bin/bash

x=0-9

read -ra digits < <(
    ksh -c '
        range={$1}
        echo ${range/-/..}
    ' ksh $x
)
echo "${digits[@]}"

Will work with any brace expansion too.

With a filtered eval (use at your own risks, the first solution is preferred if ksh can be installed, and the filter have to be modified for other expansion than digits)

#!/bin/bash

x=1-3
case $x in
    '' | *[!0123456789-]*)
        printf '%s\n' "$0: $x: invalid entry" >&2; exit 1;;
esac
exp={$x}
eval "echo ${exp/-/..}"

If you try to make some code injection, like

x='$(reboot)'

you will have

script.sh: $(reboot): invalid entry

Output

0 1 2 3 4 5 6 7 8 9
Bred answered 16/5, 2023 at 15:50 Comment(8)
A shame if there isn't a way to do this without running a subshell but this is a great alternative, thank you!Hofmann
Another alternative like other linked answer is to use eval, but I don't need it hereElastomer
would you be able to explain how to accomplish this using eval? This ksh solution obviously has the downside that it will not work if ksh is not installedHofmann
eval is a serious can of worms -- unless you're very careful to escape everything else in the data being evald (or, as the answer here does, have nothing else at all in the variable you're evaling) you can easily get security vulnerabilities that way.Kubetz
@CharlesDuffy and Joe Herbert: check my answer. Looks safe, no?Elastomer
@GillesQuénot eval works perfectly and definitely solves the problem. I think Charles still has a point about the security vulnerabilities though, as in order to make the solution work with letters you need to allow letters to be part of the input, which does reduce the security factor. Either way, for a local solution where I'm the only person who will run it, this works great, thanks!Hofmann
You're welcome. Change the test for alphabetic characters if neededElastomer
Disclaimer applies not only for you, but for following persons coming in this thread.Elastomer

© 2022 - 2024 — McMap. All rights reserved.