30
#!/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?

codeforester
  • 39,467
  • 16
  • 112
  • 140
  • add an echo $howmany for debugging to check it IS 5 'doesn't work' ==> could mean anything, send the full output of a run. – tolanj Oct 17 '13 at 16:58
  • You can use the seq command. For i in $(seq 1 $howmany) – damienfrancois Oct 17 '13 at 16:59
  • @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. – Shahbaz Oct 17 '13 at 16:59
  • I done some of the suggested comments: http://i.imgur.com/xCza7Fv.png You can see the output above in the picture. –  Oct 17 '13 at 17:03
  • Bash is not ksh: `exp={0..9}; echo $exp; 0 1 2 3 4 5 6 7 8 9` – Gilles Quénot May 16 '23 at 15:42
  • @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. – Joe Herbert May 16 '23 at 15:44
  • @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. – Joe Herbert May 16 '23 at 15:48
  • Check my answer. My example is `ksh`. I know the limitations of `bash`. – Gilles Quénot May 16 '23 at 15:56

7 Answers7

30

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.)
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
23

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
user000001
  • 32,226
  • 12
  • 81
  • 108
  • Why is that? Wouldn't it make more sense to first expand the variables, then the bracket expression? – Shahbaz Oct 17 '13 at 17:00
  • 1
    @Shahbaz Perhaps it would make sense. But this is how they designed the language. – user000001 Oct 17 '13 at 17:01
  • 9
    @Shahbaz 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. – chepner Oct 17 '13 at 17:02
  • 2
    @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. – mklement0 Mar 12 '15 at 19:37
  • @chepner: Also good to know that you _can_ use variable references in `zsh`; ditto in `ksh`. – mklement0 Mar 12 '15 at 19:38
12

create a sequence to control your loop

for i in $(seq 1 $howmany); do
echo "Welcome";
done
DomainsFeatured
  • 1,426
  • 1
  • 21
  • 39
LMC
  • 10,453
  • 2
  • 27
  • 52
  • 6
    `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. – Charles Duffy Nov 12 '15 at 21:39
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.

tolanj
  • 3,651
  • 16
  • 30
8

You could also use a while loop:

while ((howmany--)); do
   echo "Welcome"
done
Cole Tierney
  • 9,571
  • 1
  • 27
  • 35
2

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
Gilles Quénot
  • 173,512
  • 41
  • 224
  • 223
  • A shame if there isn't a way to do this without running a subshell but this is a great alternative, thank you! – Joe Herbert May 16 '23 at 15:56
  • Another alternative like other linked answer is to use `eval`, but I don't need it here – Gilles Quénot May 16 '23 at 15:58
  • 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 installed – Joe Herbert May 16 '23 at 16:01
  • `eval` is a serious can of worms -- unless you're very careful to escape everything else in the data being `eval`d (or, as the answer here does, _have nothing else at all_ in the variable you're `eval`ing) you can easily get security vulnerabilities that way. – Charles Duffy May 16 '23 at 16:11
  • @CharlesDuffy and Joe Herbert: check my answer. Looks safe, no? – Gilles Quénot May 16 '23 at 16:13
  • @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! – Joe Herbert May 16 '23 at 16:18
  • You're welcome. Change the test for alphabetic characters if needed – Gilles Quénot May 16 '23 at 16:20
  • Disclaimer applies not only for you, but for following persons coming in this thread. – Gilles Quénot May 16 '23 at 16:25
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
codeforester
  • 39,467
  • 16
  • 112
  • 140