Shell builtins · bash builtin
test
Evaluate a condition about files, strings or integers and return 0 for true or 1 for false.
test EXPRESSION or [ EXPRESSION ]
test checks a condition and reports the answer only through its exit status: 0 when true,
1 when false, 2 when the expression itself is malformed. It prints nothing. [ is the same
command under another name, with one extra rule: its last argument must be ]. That is why
[ needs spaces around it; it is a command name, not punctuation, and if [ -f x ] is just
if running a command called [.
There are three families of tests. File tests take one operand: -e exists, -f regular
file, -d directory, -s not empty, -r/-w/-x permissions, -L symlink, and
-nt/-ot compare modification times. String tests use = and !=, plus -z (empty) and
-n (not empty). Integer comparisons use letters, -eq -ne -lt -le -gt -ge, because < and
> would be redirections.
In bash you will usually prefer [[ ... ]], a keyword rather than a command: variables inside
it are not word-split or globbed, &&, ||, < and > work as you would expect, and it
adds pattern matching with == and regexes with =~. Use [ in #!/bin/sh scripts, where
[[ does not exist, and always quote the variables you pass to it. For arithmetic, (( a >
b )) reads better than -gt. The full operator table is in Test operators.
Examples
Check that a file exists
if [ -f notes.txt ]; then echo "notes.txt is a regular file"; fi
test -d data && echo "data is a directory"
[ -e nothing.txt ] || echo "nothing.txt does not exist"
notes.txt is a regular file data is a directory nothing.txt does not exist
test X and [ X ] are the same command. Since only the exit status matters, they combine
naturally with if, && and ||.
See the exit status directly
[ 5 -gt 3 ]; echo "5 -gt 3: $?"
[ 5 -lt 3 ]; echo "5 -lt 3: $?"
[ 5 -gt ]; echo "malformed: $?"
5 -gt 3: 0 5 -lt 3: 1 /bin/bash: line 3: [: 5: unary operator expected malformed: 2
True is 0 and false is 1. A malformed expression prints an error and returns 2, which an
if treats the same as false, so syntax mistakes can hide as "condition not met".
String comparison versus numeric comparison
[ 10 = 10.0 ] && echo "= equal" || echo "= different"
[ 010 -eq 10 ] && echo "-eq equal" || echo "-eq different"
[[ 9 > 10 ]] && echo "[[ > ]] says 9 is greater (text order)"
(( 9 > 10 )) || echo "(( > )) says 9 is smaller"
= different -eq equal [[ > ]] says 9 is greater (text order) (( > )) says 9 is smaller
= compares characters, so 10 and 10.0 differ. -eq compares integers, so 010 and
10 are equal. Inside [[ ]], > sorts as text and 9 comes after 1; use -gt or
(( )) for numbers.
Test for empty and non-empty strings
name=""
[ -z "$name" ] && echo "name is empty"
name="alice"
[ -n "$name" ] && echo "name is set to $name"
[ "$name" != "root" ] && echo "not root"
name is empty name is set to alice not root
-z is true for an empty string and -n for a non-empty one. Note the quotes around the
variable in every test; the next example shows why.
An unquoted empty variable breaks [
name=""
if [ $name = "admin" ]; then echo admin; else echo "not admin"; fi
if [ -n $name ]; then echo "-n says non-empty"; fi
if [ -n "$name" ]; then echo "never"; else echo "quoted -n says empty"; fi
/bin/bash: line 2: [: =: unary operator expected not admin -n says non-empty quoted -n says empty
Unquoted, the empty variable disappears: the first test becomes [ = admin ], an error
(and the else branch runs anyway). The second becomes [ -n ], a one-argument test that
is true because -n is a non-empty string. Quoting fixes both.
[[ ]] does not split variables and supports patterns
file="file with spaces.txt"
[ -f $file ] || echo "[ with unquoted var: failed"
[[ -f $file ]] && echo "[[ with unquoted var: works"
[[ $file == *.txt ]] && echo "pattern match: ends in .txt"
[[ $file =~ ^file\ (.*)\.txt$ ]] && echo "regex group: ${BASH_REMATCH[1]}"
/bin/bash: line 2: [: too many arguments [ with unquoted var: failed [[ with unquoted var: works pattern match: ends in .txt regex group: with spaces
With [, the unquoted name splits into three words and the test errors out. [[ is
parsed by the shell itself, so the variable stays one word. On the right of == an
unquoted value is a glob pattern, and =~ matches an extended regex whose groups land in
BASH_REMATCH.
Distinguish files, directories and links
for p in notes.txt data link-to-notes scripts/greet.sh missing; do
printf '%-17s' "$p"
[ -e "$p" ] && printf ' exists' || printf ' missing'
[ -f "$p" ] && printf ' file'
[ -d "$p" ] && printf ' dir'
[ -L "$p" ] && printf ' symlink'
[ -x "$p" ] && printf ' executable'
echo
done
notes.txt exists file data exists dir executable link-to-notes exists file symlink scripts/greet.sh exists file executable missing missing
-f and -d follow symlinks, so link-to-notes counts as a file as well as a symlink.
Directories are -x because for them the bit means "may enter". Only -L (or -h) looks
at the link itself.
Check that a file is not empty with -s
: > empty.log
for f in empty.log logs/error.log; do
if [ -s "$f" ]; then echo "$f has data"; else echo "$f is empty"; fi
done
empty.log is empty logs/error.log has data
-s is true when the file exists and its size is greater than zero. : > file creates
or truncates a file to zero bytes.
Rebuild only when the source is newer with -nt
touch -d '1 year ago' app.bin
[ src/main.c -nt app.bin ] && echo "main.c changed, rebuild"
[ app.bin -ot src/main.c ] && echo "app.bin is older than main.c"
[ src/main.c -nt never-built.bin ] && echo "never-built.bin is missing, build it"
main.c changed, rebuild app.bin is older than main.c never-built.bin is missing, build it
-nt and -ot compare modification times, the same check make does. -nt is also
true when the left file exists and the right one does not, which covers "build if missing
or stale" in one test.
Combine conditions
n=7
[ "$n" -ge 1 ] && [ "$n" -le 10 ] && echo "$n is between 1 and 10"
[[ $n -ge 1 && $n -le 10 ]] && echo "same test inside [["
[ ! -d "$n" ] && echo "and there is no directory called $n"
7 is between 1 and 10 same test inside [[ and there is no directory called 7
Chain separate [ commands with the shell's && and ||, or put && and || inside
[[ ]]. ! negates a test. The -a and -o operators inside [ are obsolete and
ambiguous; avoid them.
Integer operators reject non-numbers
count="12 items"
[ "$count" -gt 10 ] && echo "more than ten"
echo "status: $?"
/bin/bash: line 2: [: 12 items: integer expression expected status: 2
-gt needs integers, so [ prints an error and returns 2. Validate input first, for
example with [[ $count =~ ^[0-9]+$ ]].
[[ does not exist in sh
x=abc
[[ $x == a* ]] && echo match
[ "$x" = abc ] && echo "[ works in sh"
/bin/sh: 2: [[: not found [ works in sh
dash, /bin/sh on Debian and Ubuntu, has no [[ keyword and reports it as an unknown
command (status 127), so the && never fires. Scripts with #!/bin/sh must use [.
Common pitfalls
Leaving out the spaces:
["$a"="$b"]is one word, so the shell looks for a command with that name.[, every operand and]must be separate words.Unquoted variables in
[: an empty value vanishes and a value with spaces splits, giving errors or wrong answers. Quote them, or use[[ ]]in bash.Using
>or<in[for numbers:[ 10 > 9 ]creates a file named9and is always true. Use-gt, or(( ))in bash.=is the portable string equality operator.==works in bash's[and[[but not in dash's[.[ -n $var ]with an empty, unquoted$varis always true;[ -z $var ]happens to work. Quote and both behave.
GNU vs BSD and macOS
The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).
testexists as a builtin in every shell and as/usr/bin/testand/usr/bin/[. The file tests and integer operators shown here are POSIX and behave the same on BSD and macOS;[[,==in[,=~and-vare bash (and ksh/zsh) features.
Options worth knowing
A selection, described in our own words. For the full list, see test(1), bash(1), SHELL BUILTIN COMMANDS.
| Option | What it does |
|---|---|
-e FILE | True if FILE exists (of any type). |
-f FILE | True if FILE exists and is a regular file. |
-d FILE | True if FILE exists and is a directory. |
-L FILE | True if FILE is a symbolic link (does not follow it). |
-h FILE | Same as -L. |
-s FILE | True if FILE exists and is larger than zero bytes. |
-r FILE | True if FILE exists and is readable by you. |
-w FILE | True if FILE exists and is writable by you. |
-x FILE | True if FILE exists and is executable (searchable, for a directory). |
-p FILE | True if FILE is a named pipe (FIFO). |
-S FILE | True if FILE is a socket. |
-b FILE | True if FILE is a block device. |
-c FILE | True if FILE is a character device. |
-t FD | True if file descriptor FD is open on a terminal. |
-z STRING | True if STRING is empty. |
-n STRING | True if STRING is not empty. |
-v NAME | True if shell variable NAME is set (bash builtin only). |
= | Binary: true if the two strings are identical. |
!= | Binary: true if the two strings differ. |
-eq | Binary: true if the integers are equal. |
-ne | Binary: true if the integers are not equal. |
-lt | Binary: true if the first integer is less than the second. |
-le | Binary: true if the first integer is less than or equal to the second. |
-gt | Binary: true if the first integer is greater than the second. |
-ge | Binary: true if the first integer is greater than or equal to the second. |
-nt | Binary: true if the first file is newer than the second, or only the first exists. |
-ot | Binary: true if the first file is older than the second, or only the second exists. |
-ef | Binary: true if both names refer to the same file (same device and inode). |
! | Negate the expression that follows. |
Related
trueDo nothing and exit with status 0 (success).falseDo nothing and exit with status 1 (failure).statShow a file's metadata (size, permissions, owner, inode, timestamps), or print chosen fields in your own format.exprEvaluate an integer, comparison or string expression given as separate arguments and print the result.findWalk a directory tree and print, or act on, every file that matches a set of tests.- Test operatorsEvery file, string and number test for [ ] and [[ ]], pattern and regex matching, and the traps like `[ $a > $b ]` creating a file.
- ConditionalsHow if, elif, &&, || and ! decide using exit statuses, the three test syntaxes, guard clauses, and why `a && b || c` is not an if-else.
- QuotingSingle quotes, double quotes, backslashes and $'...', what each protects, and the unquoted-variable bugs they prevent, each shown failing and then fixed.
- Exit codesWhat exit statuses mean, how $? works, the reserved values 126, 127 and 128+N, PIPESTATUS, and how to choose codes for your own scripts.