What is the difference between test, [ and [[ ?

The open square bracket [ command (aka test command) and the [[ ... ]] test construct are used to evaluate expressions. [[ ... ]] works only in the Korn shell (where it originates), Bash, Zsh, and recent versions of Yash and busybox sh (if enabled at compilation time, and still very limited there especially in the hush-based variant), and is more powerful; [ and test are POSIX utilities (generally builtin). POSIX doesn't specify the [[ ... ]] construct (which has a specific syntax with significant variations between implementations) though allows shells to treat [[ as a keyword. Here are some examples:

[ "$variable" ] || echo 'variable is unset or empty!' >&2
[ -f "$filename" ] || printf 'File does not exist or is not a regular file: %s\n' "$filename" >&2

if [[ ! -e $file ]]; then
    echo "File doesn't exist or is in an inaccessible directory or is a symlink to a file that doesn't exist." >&2

if [[ $file0 -nt $file1 ]]; then
    printf '%s\n' "file $file0 is newer than $file1 (or $file1 is no accessible)"
    # (behaviour varies between shells if $file1 is not accessible)

To cut a long story short: test implements the old, portable syntax of the command. In almost all shells (the oldest Bourne shells are the exception), [ is a synonym for test (but requires a final argument of ]). Although all modern shells have built-in implementations of [, there usually still is an external executable of that name, e.g. /bin/[. POSIX defines a mandatory feature set for [, but almost every shell offers extensions to it. So, if you want portable code, you should be careful not to use any of those extensions.

[[ is a new, improved version of it, and it is a keyword rather than a program. This makes it easier to use, as shown below.

Although [ and [[ have much in common and share many expression operators like "-f", "-s", "-n", and "-z", there are some notable differences. Here is a comparison list:


new test [[

old test [


string comparison


\> (*)

[[ a > b ]] || echo "a does not come after b"


\< (*)

[[ az < za ]] && echo "az comes before za"

= (or ==)


[[ a = a ]] && echo "a equals a"



[[ a != b ]] && echo "a is not equal to b"

integer comparison



[[ 5 -gt 10 ]] || echo "5 is not bigger than 10"



[[ 8 -lt 9 ]] && echo "8 is less than 9"



[[ 3 -ge 3 ]] && echo "3 is greater than or equal to 3"



[[ 3 -le 8 ]] && echo "3 is less than or equal to 8"



[[ 5 -eq 05 ]] && echo "5 equals 05"



[[ 6 -ne 20 ]] && echo "6 is not equal to 20"

conditional evaluation


-a (**)

[[ -n $var && -f $var ]] && echo "$var is a file"


-o (**)

[[ -b $var || -c $var ]] && echo "$var is a device"

expression grouping


\( ... \) (**)

[[ $var = img* && ($var = *.png || $var = *.jpg) ]] &&
echo "$var starts with img and ends with .jpg or .png"

Pattern matching

= (or ==)

(not available)

[[ $name = a* ]] || echo "name does not start with an 'a': $name"

RegularExpression matching


(not available)

[[ $(date) =~ ^Fri\ ...\ 13 ]] && echo "It's Friday the 13th!"

(*) This is an extension to the POSIX standard; some shells may have it, others may not.

(**) The -a and -o operators, and ( ... ) grouping, are defined by POSIX but only for strictly limited cases, and are marked as deprecated. Use of these operators is discouraged; you should use multiple [ commands instead:

Special primitives that [[ is defined to have, but [ may be lacking (depending on the implementation):




entry (file or directory) exists


[[ -e $config ]] && echo "config file exists: $config"

file is newer/older than other file

-nt / -ot

[[ $file0 -nt $file1 ]] && echo "$file0 is newer than $file1"

two files are the same


[[ $input -ef $output ]] && { echo "will not overwrite input file: $input"; exit 1; } 



[[ ! -u $file ]] && echo "$file is not a setuid file"

But there are more subtle differences.

As a rule of thumb, [[ is used for strings and files. If you want to compare numbers, use an ArithmeticExpression, e.g.

# Bash
while (( i < 10 )); do ...

When should the new test command [[ be used, and when the old one [? If portability/conformance to POSIX or the BourneShell is a concern, the old syntax should be used. If on the other hand the script requires BASH, Zsh, or KornShell, the new syntax is usually more flexible, but not necessarily backwards compatible.

For reasons explained in the theory section below, any problem with an operator used with [[ is an unhandleable parse-time error that will cause bash to terminate, even if the command is never evaluated.

# Example of improper [[ usage.
# Notice that isSet is never even called.

 $ bash-3.2 <<\EOF
if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 2))); then
  isSet() { [[ -v $1 ]]; }
  isSet() { [[ ${1+_} ]]; }
bash-3.2: line 2: conditional binary operator expected
bash-3.2: line 2: syntax error near `$1'
bash-3.2: line 2: `  isSet() { [[ -v $1 ]]; }'

If backwards-compatibility were desired then [ -v should have been used instead. The only other alternatives would be to use an alias to conditionally expand during the function definition, or eval to defer parsing until the command is actually reached at runtime.

See the Tests and Conditionals chapter in the BashGuide.


The theory behind all of this is that [ is a simple command, whereas [[ is a compound command. [ receives its arguments as any other command would, but most compound commands introduce a special parsing context which is performed before any other processing. Typically this step looks for special reserved words or control operators specific to each compound command which split it into parts or affect control-flow. The Bash test expression's logical and/or operators can short-circuit because they are special in this way (as are e.g. ;;, elif, and else). Contrast with ArithmeticExpression, where all expansions are performed left-to-right in the usual way, with the resulting string being subject to interpretation as arithmetic.


BashFAQ/031 (last edited 2022-05-09 13:49:40 by 27)