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. 9.

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.

Comment #1 posted on 2026-08-18 22:22:56 by xmanmonk

Another great show

I typically use bc or the kde calculator, but there was some stuff here I either forgot or didn't know! Thanks for the info!

Comment #2 posted on 2026-08-19 14:31:02 by Whiskeyjack

HPR4707 Unix curio - dc

Having just read some of the background of dc, I would have thought that it would have been worth a "Unix Curio" episode all on its own.

It is obscure in that most people probably didn't know that it exists, it uses RPN notation, which few people use, it didn't become part of the POSIX standard, and it has an interesting relationship to the more commonly used bc.

I might be worth making this a subject of a future Unix Curio.

Comment #3 posted on 2026-08-20 04:47:32 by Vance

dc

Thank you both! Whiskeyjack, you do have a good point. Thirty years ago when I was first learning about UNIX, 'dc' was often included in introductory materials, which is why I said it isn't obscure. However, I probably didn't fully appreciate how the situation has changed since then.

To be blunt, I'm still not a fan of RPN or 'dc' and I don't feel very motivated to dig any deeper into it. But I suppose one never knows; maybe one day it might catch my interest. Actually, regarding the HP calculator I described, its case was as fascinating to me as the device itself - it was the first place I had ever seen Velcro hook-and-loop, which in the 1970s seemed magical!

Comment #4 posted on 2026-08-21 16:00:48 by Whiskeyjack

HPR4707 Unix curio - Stack Machines

I prefer using normal infix notation over RPN, but one of the easiest ways of making an interpreter is to create what is called a "stack machine" which does everything on a stack. Data values get pushed onto a stack, and operators work on the values in the stack. Forth is probably the best known stack oriented programming language.

RPN translates very easily to a stack machine, and dc clearly works with a stack because it gives the error "dc: stack empty" if you make an error such as not providing enough arguments.

I don't know the history of either HP calculators or dc, but I suspect that both may have used RPN more for the reason that it was easiest to implement it that way than because anyone thought it was better from a human perspective.

In episode 4 of my series on industrial controls (HPR4738) I talk about the stack based boolean logic interpreter in the Siemens S5 series PLCs. The concept is fundamental to most industrial controls, although it is often hidden from view.

The Postscript language used in PDF files is also a stack based programming language, so the concept is fairly widely used.

I find the subject of stack based interpreters to be fascinating even though they are not my preferred way of writing programs.

Comment #5 posted on 2026-08-27 10:42:43 by Trollercoaster

Suggestion for Linux Curio

I've been enjoying your episodes of Linux Curio - and they're on my favlist.

But you are way too serious. Maybe an episode on the fun gems of the linux terminal could be cool. I'm pretty sure we all know cowsay already, but I once remember I saw a choo choo train too.
Maybe there are other perfectly useless but fun linux commands out there?

Comment #6 posted on 2026-08-27 16:16:15 by Whiskeyjack

Obscure X Window programs

Don't forget the obscure X Window programs as well. People did a lot of silly things with the X Window system that seem to have faded away and been forgotten. I'm not sure it is even possible to do some of these things with Wayland.

Here's a few:
XEyes
XNeko
XRoach
XSnow
GLXGears

I notice that XEyes is installed on my PC running Ubuntu 24.04, but I have no recollection of installing it, so perhaps it comes by default. I had no idea it was there until I just tried it now.

It doesn't seem to work quite correctly with Wayland, as it is confined to a single window.

Some of the others can still be installed, but I haven't tried that.

Where these things came from and why they were created could be an interesting story if someone were to research it.

Comment #7 posted on 2026-08-27 18:13:51 by candycanearter07

re: Suggestion for Linux Curio

the train one is called "sl" (steam locomotive) and is specifically designed to prank you for typing ls too fast

the go-to silly program that theres a million articles about is "hollywood", and "genact" is another silly one that infinitely outputs garbage that looks productive, but at that point thats straying from the built in programs so it probably wouldnt be covered in THIS series. maybe you could make a secondary series about interesting non-standard packages that may or may not be useful

Comment #8 posted on 2026-08-28 10:07:31 by Archer72

re: Suggestion for Linux Curio

ASCII Star Wars is another.

There is a command to telnet to towel.blinkenlights.nl

Here Star Wars plays in a terminal.

Comment #9 posted on 2026-08-28 16:39:28 by Vance

Thanks for the ideas

First off, I just want to give fair warning that the future cadence is likely to be closer to one a month rather than one every two weeks. Someone else has already submitted hpr4717 so there will be some extra time between episodes. That will be true for other future episodes as I have mostly exhausted subjects I am familiar with, so these will need more research than past ones.

I appreciate the value in adding some more levity, but it's difficult as my nature tends to be more serious. The next episode (still being written) is a bit more frivolous, but not really humorous so far - will think about how I might re-write it to bring that in.

Good point about looking in different places - it recently occurred to me that I should be more open to utilities outside POSIX. I have identified a few that came from BSD and gained widespread use despite never being standardized, which might be interesting.

I'm surprised to hear that "xeyes" is even present on a system using Wayland. Of course the design of Wayland means "xeyes" cannot track the mouse pointer when it's outside the "xeyes" window, kind of defeating the point. Like with BSD, I'll start looking more into X-related programs to see if there are any that would be a good fit.

Very much appreciate the input, glad you are enjoying these!

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?