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'
ab​cd
$ 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:

  1. First Edition UNIX dc manual page https://man.cat-v.org/unix-1st/1/dc
  2. Bc specification: Rationale https://pubs.opengroup.org/onlinepubs/9699919799/utilities/bc.html#tag_20_09_18
  3. Expr specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/expr.html
  4. GNU multiple precision arithmetic library https://en.wikipedia.org/wiki/GNU_Multiple_Precision_Arithmetic_Library
  5. Test specification (2024) https://pubs.opengroup.org/onlinepubs/9799919799/utilities/test.html
  6. Test specification (2017) https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html
  7. PWB1 expr manual page https://www.tuhs.org/cgi-bin/utree.pl?file=PWB1/usr/man/man1/expr.1
  8. PWB/UNIX https://en.wikipedia.org/wiki/PWB/UNIX
  9. Seventh Edition UNIX expr manual page https://man.cat-v.org/unix_7th/1/expr
  10. Seventh Edition UNIX test manual page https://man.cat-v.org/unix_7th/1/test
  11. Eighth Edition UNIX ksh manual page https://man.cat-v.org/unix_8th/1/ksh
  12. Tenth Edition UNIX sh manual page https://man.cat-v.org/unix_10th/1/sh
  13. X/Open CAE Specification: Commands and Utilities Issue 4, Version 2 https://pubs.opengroup.org/onlinepubs/009656399/toc.pdf
  14. 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


Comments

Subscribe to the comments RSS feed.

Leave Comment

Note to Verbose Commenters
If you can't fit everything you want to say in the comment below then you really should record a response show instead.

Note to Spammers
All comments are moderated. All links are checked by humans. We strip out all html. Feel free to record a show about yourself, or your industry, or any other topic we may find interesting. We also check shows for spam :).

Provide feedback
Your Name/Handle:
Title:
Comment:
Anti Spam Question: What does the letter P in HPR stand for?
Are you a spammer?
Who is the host of this show?
What does HPR mean to you?