28

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
terdon
  • 234,489
  • 66
  • 447
  • 667
Vlastimil Burián
  • 27,586
  • 56
  • 179
  • 309

3 Answers3

26

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

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
Vlastimil Burián
  • 27,586
  • 56
  • 179
  • 309
  • 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))` – pabouk - Ukraine stay strong Apr 27 '22 at 12:27
5

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.

mirageglobe
  • 151
  • 1
  • 4
  • 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. – mirageglobe May 19 '18 at 22:49
  • 1
    The second code block should be `i=1` in order to be equivalent to the first code block. – dosentmatter Oct 01 '20 at 05:58
  • Note that though not standard that `{1..100}` syntax is not specific to `bash`. It was actually copied from zsh. – Stéphane Chazelas Oct 01 '20 at 08:19
2

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.

milanHrabos
  • 371
  • 1
  • 4
  • 12