Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 8 · Variables, Command Substitution and Arithmetic

Arithmetic in Bash

The shell can count — pass and fail totals, loop indices, percentages that do not need a decimal point. What it cannot do is fractions, and the two places where it quietly gives a wrong answer instead of an error are worth meeting deliberately rather than at two in the morning.

Intermediate18 min readLinuxPart IFundamentalsVariablesBash

Part I · Module 8 · Chapter 8.5 · Arithmetic in Bash

Chapter 8.1 established that every value is text, and that echo $n+1 gives 5+1. This chapter is about asking for the other thing.

1. Asking for Arithmetic

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ pass=18
$ fail=4

$ total=$(( pass + fail ))
$ echo "$total"
22

$ echo "pass rate: $(( pass * 100 / total ))%"
pass rate: 81%

$(( expression )) evaluates the expression and becomes the result. The same shape as $( ) from Chapter 8.4, with a second pair of parentheses and an entirely different job.

Two things are different inside the double parentheses from everywhere else in the shell:

Variables do not need a dollar sign.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=3
$ b=4
$ echo "$(( a + b ))  $(( $a + $b ))"
7  7

Both work. The bare form is conventional and easier to read, because inside an arithmetic context a bare word can only be a variable.

Spaces do not matter. $((a+b)), $(( a + b )) and $(( a+b )) are identical. This is a small relief after Chapter 8.1's rule that a space around = changes everything.

The operators

OperatorDoes
+ - * /The usual four
%Remainder
**Exponentiation
++ --Increment and decrement
+= -= *= /=Compound assignment
== != < <= > >=Comparison — produces 1 or 0
&& || !Logical
? :Conditional
& | ^ ~ << >>Bitwise
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "$(( 2 ** 10 ))"
1024
$ echo "$(( 17 % 5 ))"
2
$ echo "$(( 1 << 8 ))"
256
$ echo "$(( 5 > 3 ))"
1

Comparison yielding 1 and 0 matters later: it is how arithmetic conditions work in Module 16.

2. Integers Only

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "7/2   = $(( 7 / 2 ))"
7/2   = 3
$ echo "99/100= $(( 99 / 100 ))"
99/100= 0
$ echo "-7/2  = $(( -7 / 2 ))"
-7/2  = -3

There is no floating point. Division discards the fractional part rather than rounding — 7 / 2 is 3, not 3.5 and not 4. And it truncates toward zero, so -7 / 2 is -3, not -4.

Try to use a decimal point at all and it is a hard error:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo $(( 3.5 + 1 ))
bash: 3.5 + 1 : syntax error: invalid arithmetic operator (error token is ".5 + 1 ")

3. The Leading-Zero Trap

This one is genuinely nasty, because the input that triggers it is completely ordinary.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "010 -> $(( 010 ))"
010 -> 8

A number with a leading zero is read as octal. 010 is eight.

And in base eight there is no digit 8 or 9, so:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "08 -> $(( 08 ))"
bash: 08: value too great for base (error token is "08")

Why this matters: zero-padded values are everywhere. Month and day numbers from date +%m and date +%d. Run numbers formatted as run_007. Hours and minutes. Any of them with a leading zero behaves differently from the number it looks like, and the failure arrives on the eighth and ninth of the month — which is a bug report nobody can reproduce in October.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ month=$(date +%m)       # "10" in October, "08" in August
$ echo "$(( month + 1 ))"
bash: 08: value too great for base (error token is "08")

The fix is to say which base you mean:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ m=08
$ echo "$(( 10#$m ))"
8

10# forces base ten, whatever the leading zeros say. Use it on anything that arrives zero-padded from date, from a file name, or from a person.

4. Arithmetic as a Statement

$(( )) produces a value. (( )) — without the dollar sign — is a command that evaluates an expression for its side effects:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ i=5
$ (( i++ ))
$ echo "$i"
6

$ (( i += 10 ))
$ echo "$i"
16

Nothing is printed and nothing is substituted. It exists to change variables, and it is the natural way to write a counter.

Its exit status is surprising

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ i=1
$ (( i++ ))
$ echo "status: $?"
status: 0

$ i=0
$ (( i++ ))
$ echo "status: $?"
status: 1

(( )) reports success when the result is non-zero and failure when the result is zero. That is deliberate — it makes (( a > b )) usable directly as a condition, since the comparison yields 1 for true. But it means a perfectly successful increment reports failure whenever the value it produced was zero. Chapter 16.3 returns to this from the other direction, and contrasts it with [ 0 ] — which is true, because [ is asking a completely different question.

i++ is the post-increment, so it produces the value before incrementing. Starting from i=0, that value is 0, so the status is 1 — even though i is now correctly 1.

let and declare -i

Two older ways to do the same thing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ let "x = 3 + 4"
$ echo "$x"
7

$ declare -i n
$ n="3+4"
$ echo "$n"
7

let is the predecessor of (( )) and needs careful quoting. declare -i marks a variable as integer so that assignments to it are evaluated arithmetically — which is clever and surprising to anyone reading the assignment later. Both are worth recognising; neither is worth preferring over $(( )) and (( )).

5. When You Actually Need Fractions

Integer arithmetic covers most of what a script does — counting tests, computing a percentage to the nearest whole number, indexing a loop. When you genuinely need a decimal, the shell is not the tool, and there are three reasonable answers.

awk, for a formatted number:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ pass=18
$ total=22
$ awk -v p="$pass" -v t="$total" 'BEGIN { printf "%.1f%%\n", 100*p/t }'
81.8%

This is the one to reach for. awk is on every Unix system, -v passes values in safely without quoting problems, and printf gives you exact control over the number of decimal places. (Module 13 covers awk properly.)

bc, for a calculation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "scale=2; 7/2" | bc
3.50

bc is an arbitrary-precision calculator. scale sets the number of decimal places and defaults to zero, so echo "7/2" | bc gives 3 and the missing scale is the usual first surprise. bc -l loads the maths library and sets a scale of 20.

Scaled integers, when you cannot depend on either:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ pass=18
$ total=22
$ tenths=$(( pass * 1000 / total ))
$ echo "pass rate: $(( tenths / 10 )).$(( tenths % 10 ))%"
pass rate: 81.8%

Work in tenths or hundredths and place the decimal point yourself. Ugly, entirely portable, and occasionally the right call in a script that must run on a minimal system.

6. $RANDOM

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "$RANDOM $RANDOM"
16387 27615

A special variable giving a different integer from 0 to 32767 each time it is read. Useful for a quick seed or a unique-ish temporary name:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
seed=$(( RANDOM % 1000 ))
tmp="/tmp/work_$$_$RANDOM"

($$ is the shell's PID from Chapter 7.1, which makes the combination unlikely to collide.)

Two limits worth stating: it is not cryptographically secure, and RANDOM % n is slightly biased toward smaller values unless n divides 32768. For choosing a simulation seed neither matters. For anything involving security, use /dev/urandom.

7. A Realistic Summary

Counting results from a regression and reporting them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
set -u

pass=0
fail=0

for log in sim/logs/*.log; do
  if grep -q "TEST PASSED" "$log"; then
    pass=$(( pass + 1 ))
  else
    fail=$(( fail + 1 ))
  fi
done

total=$(( pass + fail ))

if (( total == 0 )); then
  echo "no logs found"
  exit 1
fi

echo "total  : ${total}"
echo "passed : ${pass}"
echo "failed : ${fail}"
echo "rate   : $(( pass * 100 / total ))%"
awk -v p="$pass" -v t="$total" 'BEGIN { printf "exact  : %.2f%%\n", 100*p/t }'

Four decisions in there worth naming:

  • pass=$(( pass + 1 )), not (( pass++ )) — an assignment whose exit status is always zero, so it is safe under any set -e on any Bash.
  • pass * 100 / total — multiply first. The other order gives 0% for anything short of perfection.
  • (( total == 0 )) guards the division. Dividing by zero is a fatal arithmetic error, and an empty log directory is not an unusual situation.
  • awk for the exact figure, alongside the integer one. Two lines, both correct, neither pretending the shell can do something it cannot.

The for loop and if belong to Chapter 17.3 and Chapter 16.1; everything arithmetic is this chapter.

Practice

1 — The basics.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a=17; b=5
for op in '+' '-' '*' '/' '%' '**'; do
  echo "$a $op $b = $(( a $op b ))"
done

2 — Watch truncation change the answer.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pass=99; total=100
echo "divide first  : $(( pass / total * 100 ))%"
echo "multiply first: $(( pass * 100 / total ))%"

3 — Fall into the octal trap and climb out.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
m=08
echo $(( m + 1 ))        # fails
echo $(( 10#$m + 1 ))    # works
echo $(( 010 ))          # eight, silently

4 — Measure the exit status.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
i=1; (( i++ )); echo "from 1: status $?"
i=0; (( i++ )); echo "from 0: status $?"

5 — Test the set -e behaviour on your Bash. It varies, so measure rather than assume:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bash --version | head -1
printf '#!/bin/bash\nset -e\ni=0\n(( i++ ))\necho reached\n' > a.sh
printf '#!/bin/bash\nset -e\ni=0\nlet i++\necho reached\n'   > b.sh
chmod +x a.sh b.sh
./a.sh; echo "  (( )) exit: $?"
./b.sh; echo "  let   exit: $?"

6 — Three ways to a decimal.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
p=18; t=22
awk -v p=$p -v t=$t 'BEGIN { printf "%.2f%%\n", 100*p/t }'
echo "scale=2; 100*$p/$t" | bc
x=$(( p * 1000 / t )); echo "$(( x / 10 )).$(( x % 10 ))%"

7 — Divide by zero on purpose. Run echo $(( 1 / 0 )) and read the message, so you recognise it later.

8 — Clean a padded number. Confirm $(( )) strips whitespace:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
c="   42   "
echo "[$c] -> [$(( c ))]"

Check Your Understanding

1. $(( 9 / 10 * 100 )) gives 0. What is the rule, and what is the fix?

2. $(( 09 + 1 )) fails but $(( 07 + 1 )) gives 8. Explain both.

3. i=0; (( i++ )) leaves i at 1 and reports exit status 1. Is the increment broken?

4. Why is count=$(( count + 1 )) a safer way to increment than (( count++ ))?

5. You need a pass rate to one decimal place. Give two ways, and say which you would put in a script that must run anywhere.

6. A script reads day=$(date +%d) and then does arithmetic on it. On which days of the month does it fail, and why?

8. Remember

  • $(( expression )) evaluates arithmetic and becomes the result. Inside it, variables need no dollar sign and spaces do not matter.
  • Integers only. There is no floating point, and a decimal point is a syntax error.
  • Division truncates toward zero, so 7/2 is 3 and -7/2 is -3.
  • Multiply before you divide. pass / total * 100 gives 0; pass * 100 / total gives the answer.
  • A leading zero means octal. 010 is 8, and 08 is an error — which bites on zero-padded dates and run numbers.
  • 10# forces base ten. Use it on anything padded that arrived from outside.
  • (( )) without a dollar sign is a command that evaluates for side effects.
  • (( )) returns success when the result is non-zero — so a correct (( i++ )) from zero reports failure.
  • Prefer i=$(( i + 1 )). An assignment's status is always zero, on every Bash.
  • set -e behaviour around arithmetic varies between versions. Measure it on the Bash you have rather than trusting a rule of thumb.
  • For decimals use awk, or bc with an explicit scale, or scaled integers when nothing can be depended on.
  • Needing fractions is a signal that the calculation wants a different tool.
  • $RANDOM gives 0 to 32767. Fine for a seed, not for anything security-related.
  • $(( value )) strips surrounding whitespace, which is handy for numbers captured from a command.

The shell can now hold text, take it apart, run commands for their output and count. The last thing it needs is a way to get a value from outside — from a person at a prompt, or from one line of a file at a time.

Where this fits

Part of the Linux & Bash curriculum.