12

I want to make a for loop in bash with 0.02 as increments I tried

for ((i=4.00;i<5.42;i+=0.02))
do
commands
done

but it didn't work.

Rui F Ribeiro
  • 55,929
  • 26
  • 146
  • 227
Mehrshad
  • 121
  • 1
  • 1
  • 3
  • 10
    Bash doesn't do floating point math. – jordanm Sep 01 '15 at 14:42
  • 1
    increment can be made by `bc`, but stopping on 4.52 might be tricky. use @roaima suggestion, have auxiliary var with step of 2, and use `i=$(echo $tmp_var / 100 | bc)` – Archemar Sep 01 '15 at 14:43
  • 2
    http://stackoverflow.com/a/12722107 – Pandya Sep 01 '15 at 15:01
  • 5
    You normally [don't want to use floats as a loop index](http://stackoverflow.com/q/12878655/4885801). You're accumulating error on each iteration. – isanae Sep 01 '15 at 18:48

4 Answers4

20

Avoid loops in shells.

If you want to do arithmetic, use awk or bc:

awk '
  BEGIN{
    for (i = 4.00; i < 5.42; i+ = 0.02)
      print i
  }'

Or

bc << EOF
for (i = 4.00; i < 5.42; i += 0.02)  i
EOF

Note that awk (contrary to bc) works with your processors double floating point number representation (likely IEEE 754 type). As a result, since those numbers are binary approximations of those decimal numbers, you may have some surprises:

$ gawk 'BEGIN{for (i=0; i<=0.3; i+=0.1) print i}'
0
0.1
0.2

If you add a OFMT="%.17g" you can see the reason for the missing 0.3:

$ gawk 'BEGIN{OFMT="%.17g"; for (i=0; i<=0.5; i+=0.1) print i}'
0
0.10000000000000001
0.20000000000000001
0.30000000000000004
0.40000000000000002
0.5

bc does arbitrary precision so doesn't have this kind of problem.

Note that by default (unless you modify the output format with OFMT or use printf with explicit format specifications), awk uses %.6g for displaying floating point numbers, so would switch to 1e6 and above for floating point numbers above 1,000,000 and truncate the fractional part for high numbers (100000.02 would be displayed as 100000).

If you do really need to use a shell loop, because for instance you want to run specific commands for each iteration of that loop, either use a shell with floating point arithmetic support like zsh, yash or ksh93 or generate the list of values with one command as above (or seq if available) and loop over its output.

Like:

unset -v IFS # configure split+glob for default word splitting
for i in $(seq 4 0.02 5.42); do
  something with "$i"
done

Or:

seq 4 0.02 5.42 | while IFS= read i; do
  something with "$i"
done

unless you push the limits of your processor floating point numbers, seq handles errors incurred by floating point approximations more gracefully than the awk version above would.

If you don't have seq (a GNU command), you can make a more reliable one as a function like:

seq() { # args: first increment last
  bc << EOF
    for (i = $1; i <= $3; i += $2) i
EOF
}

That would work better for things like seq 100000000001 0.000000001 100000000001.000000005. Note however that having numbers with arbitrarily high precision won't help much if we're going to pass them to commands which don't support them.

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
  • I appreciate use of awk! +1 – Pandya Sep 01 '15 at 15:05
  • Why do you need to `unset IFS` in the first example? – user1717828 Sep 01 '15 at 23:18
  • @user1717828, ideally, with that split+glob invocation, we want to split on newline characters. We can do that with `IFS=$'\n'` but that doesn't work in all shells. Or `IFS=''` but that's not very legible. Or we can split on words instead (space, tab, newline) like you get with the default value of $IFS or if you unset IFS and also works here. – Stéphane Chazelas Sep 02 '15 at 07:15
  • @user1717828: we don't need to mess with `IFS`, because we know that `seq`'s output doesn't have spaces that we need to avoid splitting on. It's mostly there to make sure you realize that this example depends on `IFS`, which might matter for a different list-generating command. – Peter Cordes Sep 02 '15 at 08:27
  • 1
    @PeterCordes, it's there so we don't need to make any assumption on what IFS was set to beforehand. – Stéphane Chazelas Sep 02 '15 at 08:47
19

Reading the bash man page gives the following information:

for (( expr1 ; expr2 ; expr3 )) ; do list ; done

First, the arithmetic expression expr1 is evaluated according to the rules described below under ARITHMETIC EVALUATION. [...]

and then we get this section

ARITHMETIC EVALUATION

The shell allows arithmetic expressions to be evaluated, under certain circumstances (see the let and declare builtin commands and Arithmetic Expansion). Evaluation is done in fixed-width integers with no check for overflow [...]

So it can be clearly seen that you cannot use a for loop with non-integer values.

One solution may be simply to multiply all your loop components by 100, allowing for this where you later use them, like this:

for ((k=400;k<542;k+=2))
do
    i=$(bc <<<"scale=2; $k / 100" )    # when k=402 you get i=4.02, etc.
    ...
done

If you starting and ending values are the same number of significant figures (three in this example) you can avoid the call out to bc for each iteration of the loop and instead use string processing to generate the decimal value,

    i="${k%??}.${k#?}"                 # POSIX; when k=402 you get i=4.02, etc.
    i="${k:0:1}.${k:1}"                # bash; when k=402 you get i=4.02, etc.
roaima
  • 107,089
  • 14
  • 139
  • 261
  • I think this is the best solution with the `k=400;k<542;k+=2` it avoids also potential floating point arithmetic troubles. – Huygens Sep 01 '15 at 17:39
  • 1
    Note that for each iteration in the loop, you create a pipe (to read the output of `bc`), fork a process, create a temporary file (for the here-string), execute `bc` in it (which implies loading an executable and shared libraries and initialising them), wait for it and clean-up. Running `bc` once to do the loop would be a lot more efficient. – Stéphane Chazelas Sep 02 '15 at 09:14
  • @StéphaneChazelas yes, agreed. But if this is the bottleneck then we're probably writing the code in the wrong language anyway. OOI which is less inefficient (!)? `i=$(bc <<< "scale...")` or `i=$(echo "scale..." | bc)` – roaima Sep 02 '15 at 09:54
  • 1
    From my quick test, the pipe version is faster in zsh (where `<<<` comes from), `bash` and `ksh`. Note that switching to another shell than `bash` will give you a better performance boost than using the other syntax anyway. – Stéphane Chazelas Sep 02 '15 at 10:00
  • (and most of the shells that support `<<<` (zsh, mksh, ksh93, yash) also support floating point arithmetics (`zsh`, `ksh93`, `yash`)). – Stéphane Chazelas Sep 02 '15 at 11:52
  • @StéphaneChazelas per-iteration callout to `bc` finally removed (for certain circumstances) – roaima Feb 17 '21 at 13:33
3

Use "seq" - print a sequence of numbers

seq FIRST INCREMENT LAST

for i in $(seq 4.00 0.02 5.42)
do 
  echo $i 
done
borzole
  • 31
  • 2
1

As others have suggested, you can use bc:

i="4.00"

while [[ "$(bc <<< "$i < 5.42")" == "1" ]]; do
    # do something with i
    i="$(bc <<< "$i + 0.02")"
done
Andy Dalton
  • 13,654
  • 1
  • 25
  • 45