After you run a command in UNIX shell (bourne, bash, etc), $? has
the exit code (return value) of the prior run command:
$? Expands to the exit status of the most recent pipeline.
Shell scripting is a bit weird in that the return value of a program
is evaluated as true if it’s 0 and false if it’s non-zero.
This is useful as by convention unix commands return 0 on success
or any other integer code to map to specific error conditions.
This conditional logic, however, is flipped, opposite of conditional
logic in C or pretty much any other programming language.
Compiling and running the below C program demonstrates this distinct behavior:
| |
% clang -O0 -g -o return return.c
% ./return 1
True!
% echo $?
0
% ./return 0
False!
% echo $?
1
You can also test the same with the /usr/bin/true binary:
$ /usr/bin/true
$ echo $?
0
$ /usr/bin/false
$ echo $?
1
Using this mechanism you can construct an if statement in your shell of the following form:
| |
Playing this out fully, we can replace cmd with actual commands /usr/bin/true and /usr/bin/false
$ if /usr/bin/true; then echo True; else echo False; fi
True
$ if /usr/bin/false; then echo True; else echo False; fi
False
In a shell script, rather than an interactive shell, this is commonly written out as such:
| |
Any number of command line arguments may be used as well, we can demo with the binary we just compiled:
| |
UNIX and unix-like systems come with a condition evaluation utility,
typically installed at /bin/test
From the TEST(1) man page:
The test utility evaluates the expression and, if it evaluates to true, returns a zero (true) exit status; otherwise it returns 1 (false). If there is no expression, test also returns 1 (false).
Reading on in the man page are all sorts of example conditionals:
n1 -eq n2 True if the integers n1 and n2 are algebraically equal. n1 -ne n2 True if the integers n1 and n2 are not algebraically equal. n1 -gt n2 True if the integer n1 is algebraically greater than the integer n2.
You can continue to work this into the same if syntax:
| |
Running the above shell script with some arbitrary numbers:
$ ./test.sh 5
small
$ ./test.sh 9
small
$ ./test.sh 50
big
In the wild it’s far more common, however, to see a shell script conditional statement written like this:
| |
But it turns out, this is the exact same form we already saw, in fact, you can rewrite as follows and it’ll still work:
| |
It turns out, the if [ ]; then syntax is a lie!
A cute, clever lie… but a lie nonetheless!
You can call the /bin/[ binary just as we did with /bin/true earlier and it will set $? in exactly the same way:
$ /bin/[ 5 -gt 10 ]
$ echo $?
1
$ /bin/[ 11 -gt 10 ]
$ echo $?
0
In fact, /bin/[ is actually a hardlink to /bin/test; they are one and the same binary:
$ stat /bin/[
9844274905266950648 459383 -r-xr-xr-x 2 root wheel 18446744073709551615 11736 "Dec 1 19:50:35 2025" "Dec 1 19:50:35 2025" "Dec 1 19:50:37 2025" "Dec 1 19:50:35 2025" 11776 17 0x800 /bin/[
$ stat /bin/test
9844274905266950648 459383 -r-xr-xr-x 2 root wheel 18446744073709551615 11736 "Dec 1 19:50:35 2025" "Dec 1 19:50:35 2025" "Dec 1 19:50:37 2025" "Dec 1 19:50:35 2025" 11776 17 0x800 /bin/test
…but what of the closing ] what’s its deal?
$ lldb /bin/[
(lldb) target create "/bin/["
Current executable set to '/bin/[' (x86_64).
(lldb) b error
Breakpoint 1: where = [`error + 98 at test.c:41:2, address = 0x0000000000002302
(lldb) run
Process 8345 launched: '/bin/[' (x86_64)
1 location added to breakpoint 1
Process 8345 stopped
* thread #1, name = '[', stop reason = breakpoint 1.1
frame #0: 0x0000260bed11f302 [`error(msg="missing ']'") at test.c:41:2
38 error(const char *msg, ...)
39 {
40 va_list ap;
-> 41 va_start(ap, msg);
42 verrx(2, msg, ap);
43 /*NOTREACHED*/
44 va_end(ap);
(lldb) bt
* thread #1, name = '[', stop reason = breakpoint 1.1
* frame #0: 0x0000260bed11f302 [`error(msg="missing ']'") at test.c:41:2
frame #1: 0x0000260bed11f282 [`main(argc=<unavailable>, argv=0x000026140d45dbb8) at test.c:201:4
frame #2: 0x000026140e47337f libc.so.7`__libc_start1(argc=1, argv=0x000026140d45dbb8, env=0x000026140d45dbc8, cleanup=<unavailable>, mainX=([`main at test.c:191)) at libc_start1.c:180:7
frame #3: 0x0000260bed11f0c1 [`_start at crt1_s.S:80
(lldb) f 1
frame #1: 0x0000260bed11f282 [`main(argc=<unavailable>, argv=0x000026140d45dbb8) at test.c:201:4
198 p++;
199 if (strcmp(p, "[") == 0) {
200 if (strcmp(argv[--argc], "]") != 0)
-> 201 error("missing ']'");
202 argv[argc] = NULL;
203 }
204
(lldb)
Turns out, it’s merely a command line argument that /bin/test
enforces in its
code
when it is called as /bin/[!
Note: In modern systems, test, [, true, false and others are typically
implemented as shell built-ins
but inspecting and using the external binaries was useful to reveal
the underlying concepts clearly.