Memra

Because every variable is a string

◈ 10 cards

Do integer arithmetic in a Bourne script with expr and with POSIX arithmetic expansion, and explain why the shell needs a detour at all and what each detour costs.

The shell has exactly one data type

In the Bourne shell every variable holds a string. count=5 does not store the number five; it stores the one-character string 5. Nothing about the assignment declares a type, and nothing later infers one.

That single fact explains the first thing every shell programmer gets wrong:

count=5
count=$count+1
echo $count

This prints 5+1. Not six, not an error — the shell substituted the string 5, glued +1 onto the end, and stored the three-character result. A loop written that way never terminates, and it never complains either.

Arithmetic therefore needs a detour, and there are two of them. The textbook takes one; you should write the other; the exam may show either.

The detour the textbook takes: expr

expr is not part of the shell. It is a separate program in /usr/bin that reads an expression from its argument list, evaluates it, and writes the answer to standard output. Command substitution catches that output and puts it back in the variable:

count=5
count=`expr $count + 1`
echo $count

Now count is 6. Three details in that one line are examinable.

The operands and the operator must be separate arguments. expr 17 + 5 prints 22; expr 17+5 prints 17+5, because the shell handed expr one argument and expr had nothing to evaluate. The spaces are syntax, not formatting.

Operators that are shell metacharacters must be escaped, because the shell reads the line long before expr sees it. Multiplication is written \* — a bare * would be replaced by the names of the files in your current directory. The comparisons are \> and \< (a bare > would redirect the output into a file called 5), and the logical operators are \| and \&.

expr is a process. Every single evaluation costs a fork, a path search, an exec and a program start-up. A loop that counts to a thousand starts a thousand processes to perform a thousand additions. That is the cost model the chapter exists to make you feel, and it is the usual answer to "why is my shell script slow".

All of it is integer arithmetic. expr 17 / 5 prints 3, not 3.4: / is the quotient with the remainder thrown away, and % hands you back the remainder that was thrown away, 2.

The detour to write today: $(( ))

POSIX moved arithmetic into the shell language itself. Arithmetic expansion is written $(( expression )): the shell evaluates the expression internally and substitutes the result, exactly the way it substitutes a variable.

count=5
count=$((count + 1))
echo $count

No external program, no fork, no command substitution, and no escaping — inside the double parentheses the shell is parsing arithmetic, not command words, so * means multiply and > means greater-than. Whitespace is optional, and a bare variable name is read as its value, so count and $count mean the same thing in there. Comparisons evaluate to 1 for true and 0 for false.

Worked example — summing the arguments

A script that adds up every number it is given:

#!/bin/sh
total=0
for n in "$@"
do
    total=$((total + n))
done
echo "sum = $total"

Run as ./sum.sh 3 9 30 it prints sum = 42. Follow it once: total starts as the string 0; on each pass the shell evaluates total + n as integers, and stores the decimal string of the answer back in total. The strings and the integers meet exactly at the $(( )) boundary and nowhere else.

The same loop in the book’s idiom is `total=expr n , and it starts one expr` process per argument. For three arguments nobody notices. For a script summing thirty thousand lines of a log file, that is thirty thousand processes, and it is the difference between a second and a minute.

What to write, and what to be able to read

Write $(( )). It is POSIX, it is in every shell you will meet on Linux, WSL2, macOS or BSD, and it is faster. But expr is what the textbook uses, what older scripts you inherit will contain, and what an exam question is likely to print at you — so you must be able to read an expr line, say what it costs, and translate it in either direction.

Operationexpr formEscape?$(( )) formaddexpr $i + 1no$((i + 1))subtractexpr $i - 1no$((i - 1))multiplyexpr $i \* 3yes - * globs$((i * 3))divideexpr $i / 3no - quotient only$((i / 3))greater thanexpr $i \> 5yes - > redirects$((i > 5))Each expr line forks a process. No $(( )) line does.
The backslashes in the middle column are aimed at the shell, not at `expr` — which is exactly why the right-hand column needs none of them.
NORMAL ~/memra/learn/comp-325/arithmetic-with-expr-and-arithmetic-expansion utf-8 LF