Shell Arithmetic and Test Operators
- Description: Integer arithmetic forms (
$((...)),((...)),let,expr), the three test commands (test,[,[[), and numeric / string / file operators - My Notion Note ID: K2A-E-4
- Created: 2020-06-03
- Updated: 2026-05-18
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Arithmetic Forms
- 2. Arithmetic Operators
- 3. Floats and
bc/awk - 4.
test,[, and[[— Three Tests - 5. Numeric Comparison
- 6. String Comparison
- 7. File Tests
- 8. Combining Tests
- 9. Regex with
[[ =~ ]] - 10. References
1. Arithmetic Forms
Bash has four ways to do integer math; pick by context:
n=$(( 2 + 3 )) # substitution: yields the value, assign or echo
echo $(( a * b ))
(( count++ )) # evaluation: runs as a command, exit 0 if non-zero
(( a > b )) && echo big
let "x = 4 * 5" # legacy "let" command
let x++
result=$(expr 2 + 3) # external program — slow, requires spaces, `\*`
| Form | Use for | Notes |
|---|---|---|
$(( ... )) |
Get the value | Preferred for assignment, echo, indexing |
(( ... )) |
Conditional / mutation | No $; exit status 0 if non-zero result. Good for loops, if (( ... )) |
let "..." |
Same as (( )) |
Older form; same exit-on-zero gotcha with set -e |
expr |
POSIX sh portability | Slow (forks), needs spaces, must escape * as \* |
- Inside
(( ))and$(( )): no$needed for variables, no quoting, C-like syntax (++,--, ternary?:, comma operator). set -etrap:(( x = 0 ))returns exit 1 (because result is 0) → aborts under strict mode. Patch:(( x = 0 )) || trueor assign viax=$(( 0 )).
2. Arithmetic Operators
Bash supports the full C operator set on 64-bit signed integers (no floats):
| Op | Meaning |
|---|---|
+ - * / % |
Add / sub / mul / int-div / mod |
** |
Exponent (bash extension) |
++ -- |
Pre/post increment & decrement |
+= -= *= /= %= |
Compound assign |
<<= >>= &= ^= |= |
Compound bitwise |
== != |
Equality |
< <= > >= |
Order |
&& || ! |
Logical |
& | ^ ~ |
Bitwise AND/OR/XOR/NOT |
<< >> |
Shift |
? : |
Ternary |
, |
Sequence |
(( a = 10, b = 3 ))
echo $((a / b)) # 3 — truncating integer division
echo $((a % b)) # 1
echo $((1 << 10)) # 1024
echo $(( a > b ? a : b )) # 10 — ternary
- Bases:
0xhex,0octal,b#narbitrary base 2..64.echo $((0xff)) # 255 echo $((010)) # 8 — careful, leading 0 is OCTAL echo $((2#1010)) # 10 — explicit base 2 - Pitfall: leading-zero strings parse as octal.
(( n = 09 ))→ "value too great for base" error. Strip with${n#0}or use explicit base10#$n.
3. Floats and bc / awk
Bash has no native float. Fork an external tool:
# bc — arbitrary precision; needs `scale` for division precision
echo "scale=4; 22/7" | bc # 3.1428
# awk — fast for one-off math
awk 'BEGIN { print 22 / 7 }' # 3.14286
# python — heavy but readable
python3 -c 'print(22/7)' # 3.142857142857143
printf '%.3f\n' "$value"formats but doesn't compute.- For numerical work, drop into Python/awk/bc rather than fighting shell.
4. test, [, and [[ — Three Tests
test -f /etc/passwd && echo exists
[ -f /etc/passwd ] && echo exists
[[ -f /etc/passwd ]] && echo exists
| Form | Provided by | POSIX? | Word-splits unquoted $var? |
Pattern / regex? |
|---|---|---|---|---|
test EXPR |
builtin / /usr/bin/test |
yes | yes | no |
[ EXPR ] |
builtin / /usr/bin/[ |
yes | yes | no |
[[ EXPR ]] |
bash keyword | no | no | yes (globs + =~ regex) |
[ ... ]is a command, not syntax. The brackets are arguments; spaces inside are required:[$a == $b]is wrong,[ "$a" = "$b" ]is right.[[ ... ]]is a bash keyword — parses specially:- No word splitting →
[[ -f $file ]]is safe even if$fileis empty. - Supports
&&,||,!inside. - RHS of
==/!=is a glob pattern:[[ $name == prod-* ]]. - Supports
=~for regex (§9).
- No word splitting →
- Rule of thumb: in bash scripts, use
[[ ... ]]. Reach for[ ... ]only when targeting POSIX sh.
5. Numeric Comparison
Both [ ] and [[ ]] use the SAME operators here (different from the equality ops below):
| Op | Meaning |
|---|---|
-eq |
equal |
-ne |
not equal |
-lt |
less than |
-le |
less-or-equal |
-gt |
greater than |
-ge |
greater-or-equal |
[[ $age -ge 18 ]] && echo adult
[ "$count" -lt 10 ] && echo "few"
-eqdoes numeric comparison:[[ 01 -eq 1 ]]is true.[[ 01 == 1 ]]is false (string).- Operands must look like integers; "abc" raises an error in
[ ], a milder one in[[ ]]. - Inside
(( ... )), use the C-style operators instead:(( a > b )),(( count == 0 )).
6. String Comparison
Op (in [[ ]]) |
Meaning |
|---|---|
= or == |
string equal (RHS is glob in [[ ]]) |
!= |
string not equal |
< |
lexicographic less than (only inside [[ ]]) |
> |
lexicographic greater than (only inside [[ ]]) |
-z STR |
string is empty (zero length) |
-n STR |
string is non-empty |
[[ -z $name ]] && echo "empty"
[[ -n $name ]] && echo "set"
[[ $env == prod* ]] && echo "prod-like" # glob match!
[[ $a < $b ]] && echo "a sorts first"
- In
[ ](POSIX),<and>are NOT operators — they redirect. Use[[ ]]for ordering, orexpr/sortfor portable code. - In
[ ], prefer=(POSIX) over==(bash-only inside[ ]). - The RHS of
==inside[[ ]]is a glob, not a literal. Quote it to force literal match:[[ $version == 1.* ]] # glob — matches 1.0, 1.2, ... [[ $version == "1.*" ]] # literal — only the string "1.*"
7. File Tests
Used as [[ -X path ]]. Most common:
| Test | True if path... |
|---|---|
-e |
exists (any kind) |
-f |
exists and is a regular file |
-d |
exists and is a directory |
-L (or -h) |
is a symbolic link |
-r |
is readable by this user |
-w |
is writable by this user |
-x |
is executable / dir traversable |
-s |
exists and is non-empty (size > 0) |
-p |
is a named pipe (FIFO) |
-S |
is a socket |
-b |
is a block device |
-c |
is a character device |
-O |
is owned by the current user |
-G |
belongs to the current user's group |
f1 -nt f2 |
f1 is newer than f2 (mtime) |
f1 -ot f2 |
f1 is older than f2 |
f1 -ef f2 |
same inode (hardlink or same path) |
[[ -d $dir ]] || mkdir -p "$dir"
[[ -s $log ]] && tail "$log"
[[ build/output.bin -nt src/main.c ]] || rebuild
-efollows symlinks. To check the link itself, use-L.-fdistinguishes "regular file" from "directory or device". Use it, not-e, when you mean "file".- Pitfall: unquoted
[[ -f $path ]]is safe inside[[ ]]even if$pathhas spaces; inside[ -f $path ]it isn't — quote it.
8. Combining Tests
# Inside [[ ]] — preferred
[[ -f $f && -r $f ]] && cat "$f"
[[ $x -gt 0 && $x -lt 100 ]] || die "out of range"
# Outside, with shell operators — works everywhere
[ -f "$f" ] && [ -r "$f" ] && cat "$f"
# Inside [ ] with -a / -o — POSIX but FRAGILE
[ -f "$f" -a -r "$f" ] && cat "$f"
- Prefer
&&and||between separate tests over-a/-oinside[ ]. The latter has unpredictable parsing with empty arguments and is deprecated by POSIX itself. - Inside
[[ ]],&&and||are full short-circuit operators, parentheses work for grouping:[[ ( $a == x || $a == y ) && $b == z ]].
9. Regex with [[ =~ ]]
Bash extension; ERE syntax (same as egrep / grep -E).
ip="192.168.1.10"
if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
echo "looks like an IP"
fi
# Capture groups in BASH_REMATCH[]
re='^([^@]+)@(.+)$'
if [[ "[email protected]" =~ $re ]]; then
echo "user=${BASH_REMATCH[1]}" # alice
echo "host=${BASH_REMATCH[2]}" # example.com
fi
- Pitfall: don't quote the regex literal —
[[ $x =~ "[0-9]+" ]]matches the literal string[0-9]+, not "one-or-more digits". Either inline the regex unquoted, or store in a variable:re='[0-9]+'; [[ $x =~ $re ]]. - Captures live in the indexed array
$BASH_REMATCH.BASH_REMATCH[0]is the full match;[1],[2], ... are groups. - Locale matters:
[A-Z]may include accented letters under non-C locales. Force withLC_ALL=Cfor byte-level regex.