opscape

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 ]

12 of 12 examples run in Debian 13.7 on 2026-10-31 · manual: test(1), bash(1), SHELL BUILTIN COMMANDS

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"
output
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: $?"
output
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"
output
= 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"
output
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
output
/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]}"
output
/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
output
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
output
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"
output
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"
output
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: $?"
output
/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"
output
/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 named 9 and 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 $var is 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).

  • test exists as a builtin in every shell and as /usr/bin/test and /usr/bin/[. The file tests and integer operators shown here are POSIX and behave the same on BSD and macOS; [[, == in [, =~ and -v are 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.

OptionWhat it does
-e FILETrue if FILE exists (of any type).
-f FILETrue if FILE exists and is a regular file.
-d FILETrue if FILE exists and is a directory.
-L FILETrue if FILE is a symbolic link (does not follow it).
-h FILESame as -L.
-s FILETrue if FILE exists and is larger than zero bytes.
-r FILETrue if FILE exists and is readable by you.
-w FILETrue if FILE exists and is writable by you.
-x FILETrue if FILE exists and is executable (searchable, for a directory).
-p FILETrue if FILE is a named pipe (FIFO).
-S FILETrue if FILE is a socket.
-b FILETrue if FILE is a block device.
-c FILETrue if FILE is a character device.
-t FDTrue if file descriptor FD is open on a terminal.
-z STRINGTrue if STRING is empty.
-n STRINGTrue if STRING is not empty.
-v NAMETrue if shell variable NAME is set (bash builtin only).
=Binary: true if the two strings are identical.
!=Binary: true if the two strings differ.
-eqBinary: true if the integers are equal.
-neBinary: true if the integers are not equal.
-ltBinary: true if the first integer is less than the second.
-leBinary: true if the first integer is less than or equal to the second.
-gtBinary: true if the first integer is greater than the second.
-geBinary: true if the first integer is greater than or equal to the second.
-ntBinary: true if the first file is newer than the second, or only the first exists.
-otBinary: true if the first file is older than the second, or only the second exists.
-efBinary: true if both names refer to the same file (same device and inode).
!Negate the expression that follows.

Related