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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

int main (int argc, char **argv)
{
	int a = atoi(argv[1]);		// Get an int from cmd-line arg

	if (a) {
		printf ("True!\n");
	} else {
		printf ("False!\n");
	}

	return (!a);			// Negated value returned as $? to shell
}
% 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:

1
if cmd; then echo True; else echo False; fi

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:

1
2
3
4
5
6
7
#!/bin/sh

if /usr/bin/true; then
	echo True
else 
	echo False
fi

Any number of command line arguments may be used as well, we can demo with the binary we just compiled:

1
2
3
4
5
6
7
#!/bin/sh

if ./return 1; then
        echo True
else
        echo False
fi

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:

1
2
3
4
5
6
7
8
9
#!/bin/sh

x=$1 # number from the first command line arg to the script

if /bin/test "$x" -gt 10; then
	echo "big"
else
	echo "small"
fi

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:

1
2
3
4
5
6
7
8
9
#!/bin/sh

x=$1 # number from the first command line arg to the script

if [ "$x" -gt 10 ]; then
	echo "big"
else
	echo "small"
fi

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:

1
2
3
4
5
6
7
8
9
#!/bin/sh

x=$1 # number from the first command line arg to the script

if /bin/[ "$x" -gt 10 ]; then
	echo "big"
else
	echo "small"
fi

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.

See also: