How can I create an arithmetic loop in a POSIX shell script?

10,096

Solution 1

I have found useful information in Shellcheck.net wiki, I quote:

  1. Bash¹:

     for ((init; test; next)); do foo; done
    
  2. POSIX:

     : "$((init))"
     while [ "$((test))" -ne 0 ]; do foo; : "$((next))"; done
    

though beware that i++ is not POSIX so would have to be translated, for instance to i += 1 or i = i + 1.

: is a null command that always has a successful exit code. "$((expression))" is an arithmetic expansion that is being passed as an argument to :. You can assign to variables or do arithmetic/comparisons in the arithmetic expansion.


So the above script in the question can be POSIX-wise re-written using those rules like this:

#!/bin/sh
: "$((i=1))"
while [ "$((i != 10))" -ne 0 ]
do
    echo "$i"
    : "$((i = i + 1))"
done

Though here, you can make it more legible with:

#!/bin/sh
i=1
while [ "$i" -ne 10 ]
do
    echo "$i"
    i=$((i + 1))
done

as in init, we're assigning a constant value, so we don't need to evaluate an arithmetic expression. The i != 10 in test can easily be translated to a [ expression, and for next, using a shell variable assignment as opposed to a variable assignment inside an arithmetic expression, lets us get rid of : and the need for quoting.


Beside i++ -> i = i + 1, there are more translations of ksh/bash-specific constructs that are not POSIX that you might have to do:

  • i=1, j=2. The , arithmetic operator is not really POSIX (and conflicts with the decimal separator in some locales with ksh93). You could replace it with another operator like + as in : "$(((i=1) + (j=2)))" but using i=1 j=2 would be a lot more legible.

  • a[0]=1: no arrays in POSIX shells

  • i = 2**20: no power operator in POSIX shell syntax. << is supported though so for powers of two, one can use i = 1 << 20. For other powers, one can resort to bc: i=$(echo "3 ^ 20" | bc)

  • i = RANDOM % 3: not POSIX. The closest in the POSIX toolchest is i=$(awk 'BEGIN{srand(); print int(rand() * 3)}').


¹ technically, that syntax is from the ksh93 shell and is also available in zsh in addition to bash

Solution 2

thanks for above in-depth background knowledge on the difference. A drop in replacement that worked for me when using shellcheck.net was as below.

BASH

for i in {1..100}; do  
  ...  
done  

POSIX

i=1; while [ $i -le 100 ]; do  
  ...  
  i=$(( i + 1 ))  
done

some people noted that seq is also an option using seq 1 10 . Creating a loop, however this is dependant that os has seq.

Solution 3

This method is even more general, since you can plug arbitrary $ variable in it:

#!/bin/sh

for i in $(seq 1 10); do
    echo $i
done

Just make sure, you have seq utility installed, but since it is in CoreUtils, you do.

Share:
10,096

Related videos on Youtube

Vlastimil Burián
Author by

Vlastimil Burián

I am passionate about Linux systems in general and POSIX shell scripting in particular.

Updated on September 18, 2022

Comments

  • Vlastimil Burián
    Vlastimil Burián almost 2 years

    I know how to create an arithmetic for loop in bash.

    How can one do an equivalent loop in a POSIX shell script?

    As there are various ways of achieving the same goal, feel free to add your own answer and elaborate a little on how it works.

    An example of one such bash loop follows:

    #!/bin/bash
    for (( i=1; i != 10; i++ ))
    do
        echo "$i"
    done
    
  • mirageglobe
    mirageglobe about 6 years
    note that i have placed i=0 in the same line as while. although readability is not great, it is a one liner drop-in. This ensures that variable i is not tainted by anywhere else defined in the script.
  • dosentmatter
    dosentmatter over 3 years
    The second code block should be i=1 in order to be equivalent to the first code block.
  • Stéphane Chazelas
    Stéphane Chazelas over 3 years
    Note that though not standard that {1..100} syntax is not specific to bash. It was actually copied from zsh.
  • Stéphane Chazelas
    Stéphane Chazelas over 2 years
    Note that it cannot be used in contexts where $IFS may not contain the newline character or may contain decimal digits.
  • Admin
    Admin about 2 years
    Just a note about the test condition: It is generally better not to check just against a single value (10 in the example). Use i < 10 instead of i != 10. Why? It is much more robust and logically correct. The condition will not stop working when you change the incrementation step, change the i value outside of the next expression, when you use float instead of int (in other languages than shell) etc. --- BTW I am wondering if there are reasons to prefer one of these two variants of a numerical variable assignment: : $((x = a + b)) or x=$((a + b))