hpr4707 :: UNIX Curio #12 - expr
Another way to do math (and more)
Hosted by Vance on Tuesday, 2026-08-18 is flagged as Clean and is released under a CC-BY-SA license.
unix curio, unix, expr.
(Be the first).
Listen in ogg,
opus,
or mp3 format. Play now:
Duration: 00:24:27
Download the transcription and
subtitles.
general.
This series is dedicated to exploring little-known—and occasionally useful—trinkets lurking in the dusty corners of UNIX-like operating systems.
Arithmetic is something that one would normally expect computers to be able to do. With UNIX, one could of course always write a program to perform a calculation, but for people like me who are bad at programming, it would be nice to have a tool that makes things a bit easier.
First Edition UNIX in 1971 included such a tool, called
dc
, which stands for "desk calculator"
1
. This utility performed integer arithmetic (though later versions can handle real numbers) using reverse Polish notation. To divide 11 by 4 with this method, instead of entering "11 ÷ 4 =", you would key in "11 ENTER 4 ENTER ÷". The very first calculator I ever used, one made by Hewlett-Packard that my father brought home from work a few times, employed reverse Polish notation but I have never gotten used to it. All the calculators I have made significant use of and bought for myself used the more common infix notation "11 ÷ 4"—I also prefer computer utilities following that pattern, so I almost never use
dc
.
For reasons explained in the
rationale for the
bc
utility
2
,
dc
has not been standardized in POSIX despite its long tenure. However, while it doesn't seem to get a lot of attention, I still would not consider
dc
to be a UNIX Curio.
Today, it is possible to do integer arithmetic in a standard POSIX shell without any outside utilities. This is called "arithmetic expansion" and is described in references or manual pages for many shells. It wasn't always this way, however. Before a shell was available that supported arithmetic expansion, you needed to call another utility for your calculations, and
expr
was one of those
3
. That program is the UNIX Curio for this episode.
Some people might pronounce this name, but I find it awkward to say, so I just spell out
expr
the same as I would do with
dc
. Its name is an abbreviation of "expression", and it takes arguments representing an expression. An expression is formed by combining integers or strings with zero or more operator symbols. There is quite a variety of operators—some are mathematical, some perform comparisons, one matches a regular expression, and others are used for grouping or logical tests.
Since we started this episode talking about arithmetic, let's tackle those first. The "+", "-", "*", and "/" symbols are for performing addition, subtraction, multiplication, and division respectively, as is common in many programming and scripting languages. The "%" produces the remainder of integer division. So,
expr 11 / 4
would output
2
; it only does integer calculations. The command
expr 11 % 4
would output
3
—in this case, the remainder left after "4" is removed from "11" twice. Take note that
expr
expects the integers or strings and operators it is given to all be separate arguments. Running
expr 11/4
would just output the string
11/4
because the slash is not treated as being an operator. You also need to be careful with characters that have meaning to the shell—
expr 11 * 4
would probably result in an error because the shell will expand the asterisk to a list of files in the current directory. You would have to use
expr 11 \* 4
or
expr 11 "*" 4
instead to multiply those numbers.
It is possible for each integer to be preceded by a hyphen (with no spaces in between), meaning the number is negative. However, you need to be careful here, too. The command
expr $a + 1
could potentially fail if the value of
$a
is
-1
—some implementations might treat the
-1
as being an option to
expr
. (While POSIX does not specify any options, an implementation of the utility could add them as an extension.) The safer method is to make sure the variable doesn't appear first:
expr 1 + $a
would work, as would
expr \( $a \) + 1
. Parentheses can be used for grouping; in this example, they just prevent the value of
$a
from being the first argument. Another way to prevent a value from being treated as an option is to put the standard two hyphens after
expr
, signaling the end of options (for example,
expr -- $a + 1
).
The result of evaluating the expression is printed to standard output. The exit status of
expr
is also set based on the result—if the expression is evaluated successfully and the result is
not
zero or the null string, the exit status will be 0. The exit status is 1 if evaluation is successful and results in either zero or the null string. When the expression is invalid, the exit status will be 2 and if some other error occurs, it will be greater than 2. A script using
expr
can therefore potentially take some action depending on its exit status, the value it outputs, or both.
The next set of operators recognized by
expr
consists of comparison operators. These include "=", "!=", ">", ">=", "<", and "<=", and they work just how you would expect with integers. If one or both of the arguments are strings, however, instead of doing a numerical comparison, the arguments are compared using the collation sequence in the current locale. When the comparison is true,
expr
outputs "1" and returns an exit status of 0; if false, it outputs "0" and returns a status of 1. It is important to be careful in the arguments you use;
10 = 10.0
would evaluate to false because the period forces a string comparison rather than a numerical one. By contrast,
10 = 010
would evaluate to true; unlike some other utilities,
expr
does not consider a leading zero to mean an octal number. All numbers are treated as decimal and standard
expr
has no method for working with other bases.
There are also logical operators, though they might not act exactly the way you would expect if you are used to other programming languages. When a
&
symbol appears between two expressions, it outputs the result of the first one if both expressions evaluate to something that is not 0 or a null string. Otherwise, it outputs 0. If a
|
symbol appears between two expressions, it outputs the result of the first one provided it evaluates to something that is not 0 or a null string. Otherwise, it outputs the evaluation of the second expression if that is not a null string. If neither of these are true, it outputs 0. As with some other operators, these are significant to the shell so they need to be quoted.
I mentioned parentheses before; these can be used for grouping. The last operator I will cover is
:
and it is quite different from the others. Instead of performing a mathematical or logical function, it instead performs a regular expression match. The expression before the colon is treated as the string to match against, and the expression after the colon is a basic regular expression. (Be careful, features of extended regular expressions are
not
available.) There is one special characteristic to the match—it must occur starting at the beginning of the string, as if
^
appeared at the beginning of the regular expression. The colon operator normally returns the number of characters matched by the regular expression. So, for example,
expr "x$a" : ".*" - 1
would return the length of the string in
$a
. (The "x" is used in case
$a
happens to be a null string. Also,
${#a}
is a more efficient way to return the length of
$a
within modern shells.) However, if the regular expression contains any
subexpressions
, indicated by
\(
subexpression
\)
, this operator instead returns what is matched by the first subexpression, or a null string if that does not match. So in some cases,
expr
could be used in place of another tool like
grep
,
sed
, or
awk
.
There is one final case: where an integer or string appears without any operators at all. In that situation, it is simply output as a string with no calculations, comparisons, or matching performed. It's as if you used
echo
with a single argument.
So why would you want to use
expr
? For mathematical calculations, arithmetic expansion will certainly be faster because it happens within the shell. While
expr
does give you an exit status, allowing it to be used within shell constructs like
if
and
while
, it is likely that combining arithmetic expansion with the
test
utility (which is built in to many shells) would use fewer resources. However, arithmetic expansion has limits: POSIX only requires it to operate on signed long integers; what this means in practice depends on the platform you are running on. In contrast, the standard for
expr
does not say
anything
about what range of integers must be supported.
GNU's version uses arbitrary precision
4
, allowing it to represent very huge integers like
bc
does. The implementations of
expr
included with FreeBSD 15.0, NetBSD 10.1, and OpenIndiana 2025.10 all use a 64-bit signed representation (at least on the virtual machine I tested). While on FreeBSD and NetBSD an error results when the limit it can faithfully represent is exceeded, the OpenIndiana version just overflows and returns an incorrect value. So one cannot depend on
expr
providing any extra capabilities than shell arithmetic expansion unless the exact implementation is known.
If
expr
can't be relied on for any improvements in arithmetic relative to other tools, what about comparisons? For numerical comparisons,
test
offers the same set of options that
expr
does. When it comes to strings,
the POSIX standard only added
<
and
>
operators to the
test
utility in 2024
5
—
earlier versions just offered
=
and
!=
6
, so
expr
guaranteed more functionality up until just recently. There
is
a semantic difference between how
=
and
!=
in the two utilities behave with strings. In
test
, those operators mean "is (or is not) identical", while in
expr
they mean "collates (or does not collate) the same in the current locale". If this is important to you, there is no way to get the other behavior under standard POSIX with either of these utilities, so you have to choose the right one. I have to say that it is unclear to me how "collates the same" is applied in practice; some examples I tried did not reflect what I expected.
My reading of the
Unicode
standards
would imply that the presence of a ZERO WIDTH SPACE character (Unicode code point U+200B, represented in octal escape sequences as \342\200\213) does not affect how a string collates. However,
expr
seems
not
to ignore it. The below was obtained on Debian 12 using the en_US.UTF-8 (US English) locale, but I got the same results on FreeBSD 15.0. Reminder: in the output of
expr
, 1 means true and 0 means false.
$ printf 'abcd\n' abcd $ printf 'ab\342\200\213cd\n' abcd $ expr "abcd" = "$(printf 'ab\342\200\213cd')" 0 $ expr "abcd" \> "$(printf 'ab\342\200\213cd')" 1
Another section
of the same Unicode reference outright states that the three code points U+212B, U+00C5, and A followed by U+030A are equivalent. I tried the below on Debian 12, using the da_DK.UTF-8 (Danish) locale to be sure this character was not excluded. These were not treated as being the same by
expr
. All three had a different appearance in the terminal program I was using (Konsole).
$ printf '\342\204\253\n' Å $ printf '\303\205\n' Å $ printf 'A\314\212\n' Å $ expr "$(printf '\342\204\253')" = "$(printf '\303\205')" 0 $ expr "$(printf '\342\204\253')" \< "$(printf '\303\205')" 1 $ expr "$(printf '\303\205')" = "$(printf 'A\314\212')" 0 $ expr "$(printf '\303\205')" \> "$(printf 'A\314\212')" 1 $ expr "$(printf '\342\204\253')" = "$(printf 'A\314\212')" 0 $ expr "$(printf '\342\204\253')" \> "$(printf 'A\314\212')" 1
Another difference is that
test
has two separate sets of comparison operators—one set does numerical comparisons and the other does string comparisons. Normally in a script, you would want to be as explicit as possible about the type of comparison you want. However, there might be circumstances where you want the type of comparison to depend on what the two values are. That is how
expr
works, so in that situation you might want to use it instead. It
would
be possible to use a
case
construct in the shell to choose, based on the values, whether to use the arithmetic or string comparison operators with
test
, but
expr
does that for you automatically (see the Appendix for an example of duplicating its behavior).
For matching using the
:
operator,
expr
offers a syntax that is slightly less flexible than
grep
. On some systems,
expr
can be a smaller binary, but at most I've seen it be about 80 kilobytes smaller, so that's unlikely to be a real savings in practice. To get the length of a regular expression match, one could use the
match()
function of
awk
instead. The ability of
expr
to return a subexpression match is neat, but
sed
can also accomplish that.
The
expr
utility first appeared
7
in a version of UNIX from Bell Laboratories called the
Programmer's Workbench
8
1.0, released in 1977. From there,
it made its way into 1979's Seventh Edition UNIX
9
, which is where
the
test
utility made its first appearance
10
. The overlap between the capabilities of the two utilities seems a little strange to me, but I haven't dug into details of the history—perhaps they were developed separately and just happened to meet in Seventh Edition.
It took substantially longer for arithmetic to be a feature built directly into the shell, at least on versions of UNIX from Bell Laboratories/AT&T. The
Korn shell (ksh), included with Eighth Edition UNIX
11
in 1985, offered it with the
let
keyword or
(( ... ))
syntax, but these have not been adopted by POSIX. I couldn't track down exactly when the standard shell gained the syntax
$(( ... ))
for arithmetic expansion used today—it
was not present in 1989's Tenth Edition UNIX
12
, but
did
appear in The Open Group's CAE Specification from 1994
13
(a standard that existed alongside POSIX), so presumably it showed up somewhere in between. Separately, BSD's
C shell (csh) included arithmetic expressions from its first appearance in 2BSD
14
in 1979, very much in line with that shell adopting syntax from the C programming language, so the concept was not new.
My overall conclusion is that almost everything that
expr
does can be performed with another, better-known utility. The exception would be the way the
=
and
!=
operators behave, which to my knowledge isn't duplicated by another standard program. In some cases, using a different utility would require a bit more complex work, so if your script didn't already call that utility, you might be better off with
expr
instead. I am unlikely to start making much use of it, but still enjoy the fact that I learned a bit more about it.
This episode focuses on integer arithmetic because that is what
expr
is capable of doing. More details of arithmetic expansion within the shell are covered in
Hacker Public Radio episode 1951
. If you are instead looking for tools that can do real/floating-point arithmetic, consider
bc
or
awk
. Whiskeyjack briefly discusses these in his recent episode (
Hacker Public Radio 4678
) about high resolution timing. I don't consider either of them to be UNIX Curios, so I don't expect to talk about them in the future.
References:
- First Edition UNIX dc manual page https://man.cat-v.org/unix-1st/1/dc
- Bc specification: Rationale https://pubs.opengroup.org/onlinepubs/9699919799/utilities/bc.html#tag_20_09_18
- Expr specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/expr.html
- GNU multiple precision arithmetic library https://en.wikipedia.org/wiki/GNU_Multiple_Precision_Arithmetic_Library
- Test specification (2024) https://pubs.opengroup.org/onlinepubs/9799919799/utilities/test.html
- Test specification (2017) https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html
- PWB1 expr manual page https://www.tuhs.org/cgi-bin/utree.pl?file=PWB1/usr/man/man1/expr.1
- PWB/UNIX https://en.wikipedia.org/wiki/PWB/UNIX
- Seventh Edition UNIX expr manual page https://man.cat-v.org/unix_7th/1/expr
- Seventh Edition UNIX test manual page https://man.cat-v.org/unix_7th/1/test
- Eighth Edition UNIX ksh manual page https://man.cat-v.org/unix_8th/1/ksh
- Tenth Edition UNIX sh manual page https://man.cat-v.org/unix_10th/1/sh
- X/Open CAE Specification: Commands and Utilities Issue 4, Version 2 https://pubs.opengroup.org/onlinepubs/009656399/toc.pdf
- An introduction to the C shell (2BSD) https://www.tuhs.org/cgi-bin/utree.pl?file=2BSD/doc/csh
Appendix
This shell script shows how one could perform either an arithmetic or string "less than or equal to" comparison using
test
based on the values of the two operands
$a
and
$b
. It makes a string comparison if either operand is null or contains characters other than decimal digits. Otherwise, it does a numerical comparison. It behaves the same as
expr -- "$a" \<= "$b"
, except that
expr
also writes "1" or "0" to standard output depending on whether the comparison is true or false
and
the fact that
=
in
test
means "is identical" rather than "collates the same". I assume you don't care about those, and also that the version of
test
you have supports the
<
and
>
operators.
case "x$a" in
x*[!0-9]*|x)
test "$a" \< "$b" || test "x$a" = "x$b"
;;
*)
case "x$b" in
x*[!0-9]*|x)
test "$a" \< "$b" || test "x$a" = "x$b"
;;
*)
test "$a" -le "$b"
esac
esac
If you really do want a "1" or "0" sent to standard output, you could use the following, though things are getting rather complicated at this point. Despite the complexity, it still doesn't handle situations where an error occurs.
case "x$a" in
x*[!0-9]*|x)
test "$a" \< "$b" && echo 1 || { test "x$a" = "x$b" && echo 1 ; } || \
{ echo 0 ; false ; }
;;
*)
case "x$b" in
x*[!0-9]*|x)
test "$a" \< "$b" && echo 1 || { test "x$a" = "x$b" && echo 1 ; } || \
{ echo 0 ; false ; }
;;
*)
test "$a" -le "$b" && echo 1 || { echo 0 ; false ; }
esac
esac