| |

Bash Scripting 08 ๐Ÿง Arithmetic Operations โ€” Performing math with let, expr, and bc

โฑ๏ธ Estimated read: 11 min | |

Bash is a shell, not a programming language with a native numeric type. Every value a variable holds is, by default, a string. Arithmetic is performed by commands and expansions that parse those strings as numbers, compute a result, and return it as a string again. This distinction matters because the choice of arithmetic tool determines whether the script supports only integers, whether it can handle floating-point precision, and whether it spawns an external process for every calculation.

The previous chapters covered conditional logic and pattern matching. This chapter covers the four arithmetic mechanisms that Bash scripts actually use: the $(( )) expansion, the (( )) compound command, the let builtin, and the external expr and bc commands. Each has a different scope. $(( )) and (( )) are the modern, fast, integer-only tools. let is a legacy builtin with the same integer scope. expr is an external command that predates the shell’s built-in arithmetic. bc is an arbitrary-precision calculator that handles floating-point numbers, which Bash cannot represent natively.

Key point: Bash arithmetic is integer-only. $(( )), (( )), let, and expr all truncate fractional results. For floating-point calculations, the expression must be piped to bc, and the scale variable must be set to specify the number of decimal places. The external expr command requires spaces around every operator and escaping of shell metacharacters like *, making it slower and more error-prone than the built-in expansions.


Why multiple arithmetic mechanisms exist

The historical problem.

The Bourne shell, which Bash superseded, had no built-in arithmetic. Calculations were performed by spawning the external expr command, which parsed its arguments as an expression and printed the result to stdout. Every addition in a Bourne shell script was a fork() and an exec(). The let builtin was introduced by the Korn shell as an internal replacement, and Bash adopted it for compatibility. The $(( )) expansion was standardized by POSIX.2 in the early 1990s and is now the preferred form because it is fast, readable, and does not require escaping .

The integer problem.

All of the shell’s built-in arithmetic evaluates expressions as C-style long integers. Division truncates toward zero, and there is no representation for a fractional value . A script that needs to calculate 10 / 3 and receive 3.333 cannot use $(( )); it receives 3. The bc command exists to fill this gap. It is a separate program that implements arbitrary-precision decimal arithmetic, and it is invoked by piping an expression into it and capturing the output .

The portability problem.

A script intended to run under /bin/sh cannot assume that $(( )) or (( )) are available, because they are not required by the original Bourne shell specification. In that context, expr is the portable option, despite its verbosity and slowness . In a Bash script with a #!/usr/bin/env bash shebang, $(( )) is the correct default, and expr is unnecessary. The choice of tool is therefore determined by the target shell, not by preference.


a. The $(( )) expansion and (( )) compound command

The $(( )) expansion evaluates an arithmetic expression and substitutes the result. The (( )) compound command evaluates the same expressions but does not substitute the result; it is used for its exit status and for side effects like assignment.

#!/usr/bin/env bash
# Arithmetic expansion: substitutes the result
a=5
b=3
sum=$(( a + b ))              # sum is now 8
product=$(( a * b ))          # product is now 15
quotient=$(( a / b ))         # quotient is 1 (integer division, truncated)
remainder=$(( a % b ))        # remainder is 2

echo "Sum: $sum, Product: $product, Quotient: $quotient, Remainder: $remainder"

The $(( )) form allows variable names without the $ prefix inside the expression, which makes complex expressions more readable. $(( a + b )) and $(( $a + $b )) are equivalent, but the first form is cleaner .

The (( )) compound command is used when the result is not needed as a substitution, or when the exit status is the point. It returns exit status 0 if the expression evaluates to a non-zero value, and 1 if the expression evaluates to zero. This is the inverse of the command convention, which makes it useful directly in if statements .

# (( )) for assignment and increment
count=0
(( count += 1 ))              # Increments count; no output
(( count++ ))                 # Post-increment
echo "$count"                 # Output: 2

# (( )) as a condition: returns 0 (true) if result is non-zero
if (( count > 0 )); then
    echo "Count is positive"  # Runs because count is 2
fi

The (( )) command is not subject to word splitting or pathname expansion on its contents, which makes it safer than let for expressions containing spaces or special characters .


b. The let builtin

The let builtin evaluates each argument as an arithmetic expression. It is a simple command, not a compound command, which means its arguments undergo the normal shell expansionsโ€”word splitting and pathname expansionโ€”before let sees them. This is the source of its main disadvantage: expressions with spaces or special characters must be quoted.

#!/usr/bin/env bash
# let requires quoting for expressions with spaces
let "x = 5 + 3"
echo "$x"                     # Output: 8

# Multiple expressions can be evaluated in one call
let "y = x * 2" "z = y - 1"
echo "$y $z"                  # Output: 16 15

# Increment and compound assignment
let "x += 10"
echo "$x"                     # Output: 18

The let builtin is a legacy feature retained for compatibility with older scripts. The Bash Hackers Wiki explicitly recommends (( )) over let because (( )) does not require quoting and is not subject to word splitting . A script written today should use (( )) for assignment and increment, and $(( )) for substitution. The let builtin appears in older tutorials and legacy code, which is why it must be recognized, but it is not the recommended tool for new scripts.


c. The expr command and bc for floating-point

expr is an external command from GNU Coreutils. It evaluates an expression and prints the result to stdout. Its syntax is strict: every token must be a separate argument, spaces are required around operators, and shell metacharacters like * and > must be escaped or quoted .

#!/usr/bin/env bash
# expr: spaces around operators, escape *
sum=$(expr 5 + 3)
product=$(expr 5 \* 3)
comparison=$(expr 5 \> 3)

echo "Sum: $sum, Product: $product, Comparison: $comparison"

The escaping requirement makes expr cumbersome. expr 5 * 3 fails because the shell expands * to the list of files in the current directory before expr sees it. The backslash prevents that expansion, but the backslash must be remembered on every multiplication. For these reasons, expr is used in scripts targeting POSIX sh, and avoided in Bash scripts where $(( )) is available .

bc is the tool for floating-point arithmetic. It reads expressions from stdin, evaluates them with arbitrary precision, and prints the result. The scale variable controls the number of decimal places in division and other operations that produce fractional results .

#!/usr/bin/env bash
# bc for floating-point division
result=$(echo "scale=2; 10 / 3" | bc)
echo "$result"                # Output: 3.33

# bc for a more complex expression
total=$(echo "scale=4; 3.14159 * 2.5" | bc)
echo "$total"                 # Output: 7.8539

The scale=2 directive sets the precision to two decimal places. Without it, bc defaults to zero decimal places for division, producing 3 for 10 / 3. The expression is passed through echo and piped to bc, and the output is captured with command substitution. This pattern is the standard way to perform floating-point math in a Bash script .


Complete Example Session

#!/usr/bin/env bash
# ============================================
# PART 1: INTEGER ARITHMETIC WITH $(( ))
# ============================================
a=17
b=5

sum=$(( a + b ))              # 22
difference=$(( a - b ))       # 12
product=$(( a * b ))          # 85
quotient=$(( a / b ))         # 3 (truncated)
remainder=$(( a % b ))        # 2

echo "Sum: $sum"
echo "Difference: $difference"
echo "Product: $product"
echo "Quotient: $quotient"
echo "Remainder: $remainder"

# ============================================
# PART 2: THE (( )) COMPOUND COMMAND
# ============================================
count=0
(( count += 5 ))
(( count++ ))
echo "Count: $count"          # 6

# ============================================
# PART 3: (( )) AS A CONDITION
# ============================================
if (( count > 0 )); then
    echo "Count is positive"  # Runs
fi

if (( count == 0 )); then
    echo "Count is zero"      # Does not run
fi

# ============================================
# PART 4: THE let BUILTIN (LEGACY)
# ============================================
let "legacy = a * 2"
echo "Legacy: $legacy"        # 34

# ============================================
# PART 5: THE expr COMMAND (EXTERNAL)
# ============================================
expr_sum=$(expr 10 + 20)
expr_product=$(expr 10 \* 20)
echo "expr sum: $expr_sum, product: $expr_product"

# ============================================
# PART 6: FLOATING-POINT WITH bc
# ============================================
float_div=$(echo "scale=4; 10 / 3" | bc)
echo "10 / 3 = $float_div"    # 3.3333

float_calc=$(echo "scale=2; 3.14159 * 2" | bc)
echo "Pi * 2 = $float_calc"   # 6.28

# ============================================
# PART 7: COMBINING INTEGER AND FLOAT
# ============================================
# Use $(( )) for integers, bc for anything with a decimal point
total_items=100
items_per_box=7
boxes=$(( total_items / items_per_box ))
leftover=$(( total_items % items_per_box ))
echo "$total_items items = $boxes boxes and $leftover left over"

price=19.99
quantity=3
total=$(echo "scale=2; $price * $quantity" | bc)
echo "Total cost: \$$total"   # 59.97

The seven parts demonstrate the division of labor: $(( )) and (( )) for all integer calculations, let for legacy compatibility, expr for POSIX portability, and bc for anything that requires decimal precision. The script does not use expr for the integer calculations because $(( )) is faster and cleaner; it includes expr only to demonstrate the syntax that appears in older code.


Quick Reference

Arithmetic Tools

ToolTypePrecisionSubstitutes ResultSpeed
$(( ))ExpansionIntegerYesFast (built-in)
(( ))Compound commandIntegerNo (exit status)Fast (built-in)
letBuiltin commandIntegerNo (exit status)Fast (built-in)
exprExternal commandIntegerYes (stdout)Slow (fork/exec)
bcExternal commandArbitraryYes (stdout)Slow (fork/exec)

Common Operators

OperatorMeaningExample
+ - * /Addition, subtraction, multiplication, division$(( a + b ))
%Remainder$(( a % b ))
**Exponentiation$(( a ** 2 ))
++ --Increment, decrement(( count++ ))
+= -= *=Compound assignment(( count += 5 ))
< > == !=Comparison (in (( )))(( a > b ))

bc Precision

DirectiveEffect
scale=NSet decimal places for division
scale=2; 10/3Result: 3.33
scale=4; 10/3Result: 3.3333

Best Practices

โœ… Do This:

# Use $(( )) for integer substitution
sum=$(( a + b ))                                              # โœ…

# Use (( )) for assignment and conditions
(( count += 1 ))                                              # โœ…

# Use bc for floating-point math
result=$(echo "scale=2; $a / $b" | bc)                        # โœ…

# Quote expressions passed to let (legacy)
let "x = a + b"                                               # โœ…

# Escape * in expr (POSIX)
expr 5 \* 3                                                   # โœ…

# Use (( )) in if conditions for numeric comparison
if (( count > 0 )); then ... fi                               # โœ…

โŒ Don’t Do This:

# Expect floating-point from $(( ))
result=$(( 10 / 3 ))    # Result is 3, not 3.33            # โŒ

# Forget to escape * in expr
expr 5 * 3              # Shell expands * to filenames      # โŒ

# Use let without quoting spaces
let x = 5 + 3           # Tries to run "let x", then "="    # โŒ

# Use $(( )) for string concatenation
$(( "5" + "3" ))        # Works only because strings are numeric # โŒ

# Omit scale in bc division
echo "10 / 3" | bc      # Output: 3 (integer division)     # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Result is truncatedUsed integer arithmetic for divisionUse bc with scale
expr: syntax errorMissing spaces around operatorexpr 5 + 3, not expr 5+3
Filenames expanded instead of multiplication* not escaped in exprexpr 5 \* 3
let command not foundExpression with spaces unquotedlet "x = 5 + 3"
bc result has no decimalsscale not setecho "scale=2; ..." | bc
Octal interpretationLeading zero in number$(( 08 )) is an error; use 10#08

Real-World Examples

1. Integer addition

sum=$(( 5 + 3 ))

2. Integer division with remainder

q=$(( 17 / 5 ))
r=$(( 17 % 5 ))

3. Increment in a loop

for (( i=0; i<10; i++ )); do echo "$i"; done

4. Compound assignment

(( total += price * quantity ))

5. Numeric condition

if (( count > 10 )); then echo "Large"; fi

6. Floating-point division

result=$(echo "scale=3; 22 / 7" | bc)

7. Floating-point multiplication

total=$(echo "scale=2; 19.99 * 3" | bc)

8. expr for POSIX compatibility

sum=$(expr "$a" + "$b")

9. let for legacy scripts

let "counter = counter + 1"

10. Force base-10 for numbers with leading zeros

value=$(( 10#08 ))    # Interprets 08 as decimal 8, not octal error

Visual

Integer vs Floating-Point

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  BASH IS INTEGER-ONLY; bc HANDLES FLOATS                     โ”‚
โ”‚                                                              โ”‚
โ”‚  $(( 10 / 3 ))                                               โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ Result: 3                                              โ”‚  โ”‚
โ”‚  โ”‚ Fractional part discarded                              โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                              โ”‚
โ”‚  echo "scale=2; 10 / 3" | bc                                 โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ scale=2 โ†’ 2 decimal places                             โ”‚  โ”‚
โ”‚  โ”‚ Result: 3.33                                           โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                              โ”‚
โ”‚  Rule: $(( )) for integers, bc for decimals.                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Tool Selection

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WHICH ARITHMETIC TOOL?                                      โ”‚
โ”‚                                                              โ”‚
โ”‚  Need a result substitution?                                 โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ YES โ†’ Integers?                                    โ”‚
โ”‚       โ”‚         โ”‚                                            โ”‚
โ”‚       โ”‚         โ”œโ”€โ”€ YES โ†’ $(( ))    (fast, clean)            โ”‚
โ”‚       โ”‚         โ”‚                                            โ”‚
โ”‚       โ”‚         โ””โ”€โ”€ NO  โ†’ bc        (pipe, scale)            โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ””โ”€โ”€ NO โ†’ Need assignment/increment?                    โ”‚
โ”‚                 โ”‚                                            โ”‚
โ”‚                 โ”œโ”€โ”€ YES โ†’ (( ))     (built-in, safe)         โ”‚
โ”‚                 โ”‚                                            โ”‚
โ”‚                 โ””โ”€โ”€ NO  โ†’ Condition only?                    โ”‚
โ”‚                           โ”‚                                  โ”‚
โ”‚                           โ””โ”€โ”€ (( )) in if                    โ”‚
โ”‚                                                              โ”‚
โ”‚  POSIX sh only? โ†’ expr (external, escape *)                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The let vs (( )) Difference

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  let IS A SIMPLE COMMAND; (( )) IS A COMPOUND COMMAND        โ”‚
โ”‚                                                              โ”‚
โ”‚  let "x = 5 + 3"                                             โ”‚
โ”‚  โ”œโ”€โ”€ Arguments undergo word splitting and globbing           โ”‚
โ”‚  โ”œโ”€โ”€ Must quote expressions with spaces                      โ”‚
โ”‚  โ””โ”€โ”€ Legacy; retained for compatibility                      โ”‚
โ”‚                                                              โ”‚
โ”‚  (( x = 5 + 3 ))                                             โ”‚
โ”‚  โ”œโ”€โ”€ Contents are not subject to word splitting              โ”‚
โ”‚  โ”œโ”€โ”€ No quoting needed                                       โ”‚
โ”‚  โ””โ”€โ”€ Recommended for new scripts                             โ”‚
โ”‚                                                              โ”‚
โ”‚  Both evaluate as integers.                                  โ”‚
โ”‚  (( )) is safer and preferred.                               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Integer substitution$(( expression ))
Integer assignment(( expression )) or let "expression"
Legacy builtinlet (quote spaces; prefer (( )))
POSIX externalexpr (spaces around operators; escape *)
Floating-pointecho "scale=N; expr" | bc
Default bc precision0 decimal places
Division behavior$(( )) truncates; bc uses scale
Exponentiation$(( a ** b ))
Octal trapLeading zero interpreted as octal; use 10#
Recommended default$(( )) for integers, bc for floats

Key takeaways:

  • Bash arithmetic is integer-only. $(( )), (( )), let, and expr all evaluate expressions as C-style integers and truncate fractional results. bc is required for any calculation that needs decimal precision .
  • $(( )) is the modern standard for substitution. It is fast, does not require quoting for spaces, and was standardized by POSIX.2. (( )) is its compound-command counterpart for assignment and conditions .
  • let is legacy. It works, but its arguments undergo word splitting, so expressions with spaces must be quoted. The Bash Hackers Wiki recommends (( )) over let for new scripts .
  • expr is external and strict. It requires spaces around operators and escaping of *. It is slower than the built-ins and is used primarily for POSIX portability .
  • bc handles floating-point. The scale variable sets decimal places. Without it, division returns an integer. The expression is piped to bc and the result is captured with command substitution .
  • Leading zeros are octal. $(( 08 )) is a syntax error because 08 is not a valid octal number. Use 10#08 to force decimal interpretation .

Remember: Bash has no native floating-point type. The shell’s built-in arithmetic is integer-only, and the choice of tool is determined by three questions: Does the result need to be substituted, does the calculation require decimals, and does the script need to run under POSIX sh. For integers with substitution, use $(( )). For integer assignment, use (( )). For decimals, pipe to bc with an explicit scale. For POSIX, use expr and escape the operators. The external tools are slower, but they are the only way to exceed the shell’s integer boundary.


Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!